Converting html to pdf: how to dynamically add pages to pdf when html content is overflowing? (dynamic multiple page pdf)

Viewed 1778

I am using Handlebars and Puppeteer to compile html into dynamic .pdf invoices. Sometimes the invoices can get rather large (2-3 pages). As a result, the overflowing html content gets cut off, see this image: this image

I've made a github repo with all my code and problem you can check out here if you are interested (instructions to run the project in read.me).

So the way I am trying to solve this, is that after Handlebars rendered my template, I start a headless browser with puppeteer and set the rendered html to to page.

//renderTemplate is a function that reads the .hbs file and compiles the template to html.
const html = renderTemplate(data)

await page.setContent(html, {
  waitUntil: 'load'
})

I then keep track of the height with:

const heightTracker = await page.$(".height-tracker");
const heightTrackerDimensions = await heightTracker.boundingBox()

If this height exceeds, say 1100, then a new page has to be added and the last item in my orders array has to be cut from the html and pasted to a new page. The best I could come up with is something like this:

 if (heightTrackerDimensions.height > 1100) {
    let pageIndex = 0;
    let height = 0;

    let sections = await page.$$(".item");
    for (let i = 0; i < sections.length; i++) {
      const sectionDimensions = await sections[i].boundingBox();

      if (height + sectionDimensions.height > 1100) {
        pageIndex++;
        console.log(i)

        await page.evaluate(() => {
          let sections = document.querySelectorAll(`.item`)
          const page = document.querySelector('.page1')
          page.insertAdjacentHTML('afterend', `
              <div style="page-break-after: always;"></div>
              <div class="invoice page2">
               <div class="invoice-inner">
                 <div class="invoice-body">
                   <div class="main-column">
                    <div class="height-tracker">
                      ${sections[i].innerHTML}
                    </div>
                   </div>
                  <div class="secondary-column">
                 </div>
                </div>
               </div>
              </div>
          `
          )
          sections[i].remove()
        });
        height += sectionDimensions.height
      } else {
        height += sectionDimensions.height
      }
    }
  }

But this just doesn't work well and breaks once there are more items that are overflowing from page 1. Maybe I am over complicating to much. Maybe there are much simpler ways to achieve the same goal?

I am just breaking my head over this issue. How can I make it so that overflowing html content automatically gets added to a new pdf page with proper margin and padding? What other options do I have? I can't be the only one struggling with multi page pdf's with Puppeteer...

1 Answers

I've encountered the same issues with PDF generation, though not exactly in the same context.

I've played about with your code, and made a few changes, mainly to allow Puppeteer to create the pages as needed by itself, hopefully simplifying things:

async function printPDF() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

  const orders = [
    { item: "Completely new website design to your company style", price: "$300" },
    { item: "Conversions optimalization and management of conversions", price: "$300" },
    { item: "Implementation of content management system - Wordpress", price: "$300" },
    { item: "Installation of Premium Wordpress Theme yearly renewed (Divi)", price: "$300" },
    { item: "Responsive desktop / mobile / tablet", price: "$300" },
    { item: "Loadspeed optimized", price: "$300" },
    { item: "Free service contract (1 year)", price: "$300" },
    { item: "Media Manager", price: "$300" },
    { item: "Google Maps", price: "$300" },
    { item: "Contactforms", price: "$300" },
    { item: "Search Engine indexation", price: "$300" },
    { item: "Google Analytics / Google search console implementation", price: "$300" },
  ];

  const data = {
    invoiceNumber: "123123123",
    company: "Acme Corp",
    orders: orders,
    total: orders.reduce((sum, order) => sum + (+order.price.replace("$", "")) , 0)
  }

  page.on('console', consoleObj => console.log(consoleObj.text()));

  const html = renderTemplate(data)
  await page.emulateMediaType('print');
  await page.setContent(html, {
    waitUntil: 'load'
  })

  const pdf = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: {
        top: "10mm",
        right: "10mm",
        bottom: "10mm",
        left: "10mm"
    },
    format: "A4"
  })

  
  await browser.close();
  return pdf

}

I also updated the .invoice class in template.hbs (just to remove height and max-height items):

.invoice {
  background-color: white;
  position: relative;
  background-size: cover;
  display: flex;
  flex-direction: column;
  width: 210mm;
  /*height: 297mm; */
  min-width: 210mm;
  min-height: 297mm;
  max-width: 210mm;
  /*max-height: 297mm; */
  font-variant-ligatures: none !important;
  -webkit-font-smoothing: antialiased !important;
  text-rendering: geometricprecision !important;
  overflow: hidden;
  background-repeat: no-repeat;
  background-position: center center;
}

And the total amount:

<tr class="total">
    <td></td>
    <td>
        Total: ${{ total }}
    </td>
</tr>

An example is here: https://dropmefiles.com/xRuqY

Related