Home > OS >  PDFKit split text into two equal columns while using for loop
PDFKit split text into two equal columns while using for loop

Time:06-05

Im trying to use PDFKit to generate a simple pdf, for the most part the pdf works but albeit in a very non useful way, what i have is a deck building API that takes in a number of cards, each of these objects i want to export to a pdf, its as simple as displaying their name, but as it is, the pdf only renders one card at a time, and only on one line, what id like to happen is to get it to split the text into columns so itd look similar to this.

column 1 | column 2
c1         c8
c2         c9
c3         c10
c4         c(n)

here is my code so far,

module.exports = asyncHandler(async (req, res, next) => {
  try {
    // find the deck
    const deck = await Deck.findById(req.params.deckId);

    // need to sort cards by name
    await deck.cards.sort((a, b) => {
      if (a.name < b.name) {
        return -1;
      } else if (a.name > b.name) {
        return 1;
      } else {
        return 0;
      }
    });
    // Create a new PDF document
    const doc = new PDFDocument();

    // Pipe its output somewhere, like to a file or HTTP response
    doc.pipe(
      fs.createWriteStream(
        `${__dirname}/../../public/pdf/${deck.deck_name}.pdf`
      )
    );

    // Embed a font, set the font size, and render some text
    doc.fontSize(25).text(`${deck.deck_name} Deck List`, {
      align: "center",
      underline: true,
      underlineColor: "#000000",
      underlineThickness: 2,
    });
    // We need to create two columns for the cards
    // The first column will be the card name
    // The second column will continue the cards listed
    const section = doc.struct("P");
    doc.addStructure(section);
    for (const card of deck.cards) {
      doc.text(`${card.name}`, {
        color: "#000000",
        fontSize: 10,
        columns: 2,
        columnGap: 10,
        continued: true,
      });
    }
    section.end();

    // finalize the PDF and end the response
    doc.end();
    res.status(200).json({ message: "PDF generated successfully" });
  } catch (error) {
    console.error(error);
    res.status(500).json({
      success: false,
      message: `Server Error - ${error.message}`,
    });
  }
});

At Present this does generate a column order like i want, however theres and extreme caveat to this solution and that is, if the card text isnt very long, the next card will start on that same line, it'd be useful if i could find a way to make the text take up the full width of that row, but i havent seen anything to do that with.

CodePudding user response:

I think the problem is that you're relying on PDFKit's text "flow" API/logic, and you're having problems when two cards are not big enough to flow across your columns and you get two cards in one column.

I'd say that what you really want is to create a table—based on your initial text sample.

PDFKit doesn't have a table API (yet), so you'll have to make one up for yourself.

Here's an approach where you figure out the dimensions of things:

  • the page size
  • the size of your cells of text (either manually choose for yourself, or use PDFKit to tell you how big some piece of text is)
  • margins

Then you use those sizes to calculate how many rows and columns of your text can fit on your page.

Finally you iterate of over columns then rows for each page, writing text into those column-by-row "coordinates" (which I track through "offsets" and use to calculate the final "position").

const PDFDocument = require('pdfkit');
const fs = require('fs');

// Create mock-up Cards for OP
const cards = [];
for (let i = 0; i <  100; i  ) {
    cards.push(`Card ${i   1}`);
}

// Set a sensible starting point for each page
const originX = 50;
const originY = 50;

const doc = new PDFDocument({ size: 'LETTER' });

// Define row height and column widths, based on font size; either manually,
//  or use commented-out heightOf and widthOf methods to dynamically pick sizes
doc.fontSize(24);
const rowH = 50;  // doc.heightOfString(cards[cards.length - 1]);
const colW = 150; // doc.widthOfString(cards[cards.length - 1]);  // because the last card is the "longest" piece of text

// Margins aren't really discussed in the documentation; I can ignore the top and left margin by
// placing the text at (0,0), but I cannot write below the bottom margin

const pageH = doc.page.height;
const rowsPerPage = parseInt((pageH - originY - doc.page.margins.bottom) / rowH);
const colsPerPage = 2;

var cardIdx = 0;
while (cardIdx < cards.length) {
    var colOffset = 0;
    while (colOffset < colsPerPage) {
        const posX = originX   (colOffset * colW);
        var rowOffset = 0;
        while (rowOffset < rowsPerPage) {
            const posY = originY   (rowOffset * rowH);

            doc.text(cards[cardIdx], posX, posY);
            cardIdx  = 1;

            rowOffset  = 1;
        }
        colOffset  = 1;
    }

    // This is hacky, but PDFKit adds a page by default so the loop doesn't 100% control when a page is added;
    // this prevents an empty trailing page from being added
    if (cardIdx < cards.length) {
        doc.addPage();
    }
}

// Finalize PDF file
doc.pipe(fs.createWriteStream('output.pdf'));
doc.end();

When I run that I get a PDF with 4 pages that looks like this:

enter image description here

Changing colW = 250 and colsPerPage = 3:

enter image description here

  • Related