Easiest Way to Convert multi-page HTML to PDF Document

You are currently viewing Easiest Way to Convert multi-page HTML to PDF Document

There are many ways to generate PDFs in Nodejs using various popular PDFs libraries like jsPDF, pdf-lib, pdfmake, pdfkit, etc. These libraries are best to generate PDFs when you were supposed to create a PDF design template from scratch. But what you will do if you had already created these design templates in HTML? will you consider converting the HTML templates to these library-supported template design formats? are you just willing to convert this HTML markup directly to PDF? if your answer is Yes then this article is for you.

In this article, I’m going to show you, How you can convert HTML to PDF using Puppeteer, which is capable of generating single-page as well as multi-page PDFs without breaking HTML elements.

The generated PDFs appearance will be as same as the HTML web page, and you also do not need to modify or rewrite HTML markups and import fonts and assets separately for print.

Puppeteer is a Node library that provides a high-level API to control Chrome and Chromium by DevTools Protocol. It runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

pptr.dev

Requirements

  • Node run time
  • ExpressJs
  • Puppeteer

These are the basic requirements for creating a HTML to PDF print server.

npm init -y
npm i express puppeteer

After creating a new nodejs project and installing the expressjs and puppeteer library into the project, you just need to create three files server.js, lib/pdfGenerator.js, views/invoice.ejs which is given below :

./server.js

const express = require("express");
const pdfGenerator = require("./lib/pdfGenerator");

const server = express();
const port = 3000;

// '....'

// Set up View Engine
server.set('view engine', 'ejs');
server.set('views', __dirname + '/views');

// Set up V1 routes
server.get('/', async (req, res) => {
    const pdf = await pdfGenerator('http://localhost:3000/invoice');
    res.json({
        status: 'success',
        path: pdf
    })

});

server.get('/invoice', async (req, res) => {
    res.render('invoice')

});


// eslint-disable-next-line import/prefer-default-export
try {
    server.listen(port, () => {
        console.log(`Connected successfully on port ${port}`);
    });
} catch (error) {
    console.error(`Error occured: ${error.message}`);
}

In the above snippet, You just need to create an expressjs server script with two GET routes, which are /invoice used for invoice view and / used generating PDF.

./views/invoice.ejs

<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>PDF Print - Ubercompute</title>
     <link href=https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css rel=stylesheet>
     <link href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css rel=stylesheet>
     <script src=https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js></script>
    <style>
      body{background-color:#eee}.invoice-card{margin-top:5rem}.invoice-container{break-after:page}.fs-12{font-size:12px}.fs-15{font-size:15px}.name{margin-bottom:-2px}.product-details{margin-top:13px}
    </style>
  </head>
  <body
    class="snippet-body"
  >
    <div class="container mt-5 mb-5">
      <% [...Array(4).keys()].forEach((_, index) => { %>
          <div class="d-flex justify-content-center row invoice-container">
        <div class="col-md-12 invoice-card">
          <div class="receipt bg-white p-3 rounded">
            <img src=https://i.imgur.com/zCAnG06.png width=120>
            <h4 class="mt-2 mb-3">Your order is confirmed!</h4>
            <h6 class=name>Hello John,</h6>
            <span class="fs-12 text-black-50">your order has been confirmed and will be shipped in two
              days</span>
            <hr>
            <div class="d-flex flex-row justify-content-between align-items-center order-details">
              <div>
                <span class="d-block fs-12">Order date</span><span class=font-weight-bold>12 March 2020</span>
              </div>
              <div>
                <span class="d-block fs-12">Order number</span><span class=font-weight-bold>OD44434324-<%index%></span>
              </div>
              <div>
                <span class="d-block fs-12">Payment method</span><span class=font-weight-bold>Credit card</span><img class="ml-1 mb-1" src=https://i.imgur.com/ZZr3Yqj.png width=20>
              </div>
              <div>
                <span class="d-block fs-12">Shipping Address</span><span class="font-weight-bold text-success">New Delhi</span>
              </div>
            </div>
            <hr>
            <div class="d-flex justify-content-between align-items-center product-details">
              <div class="d-flex flex-row product-name-image">
                <img class=rounded src=https://i.imgur.com/GsFeDLn.jpg width=80>
                <div class="d-flex flex-column justify-content-between ml-2">
                  <div>
                    <span class="d-block font-weight-bold p-name">Ralco formal shirts for men</span><span class=fs-12>Clothes</span>
                  </div>
                  <span class=fs-12>Qty: 1pcs</span>
                </div>
              </div>
              <div class=product-price>
                <h5>$70</h5>
              </div>
            </div>
            <div class="d-flex justify-content-between align-items-center product-details">
              <div class="d-flex flex-row product-name-image">
                <img class=rounded src=https://i.imgur.com/b7Ve3fJ.jpg width=80>
                <div class="d-flex flex-column justify-content-between ml-2">
                  <div>
                    <span class="d-block font-weight-bold p-name">Ralco formal Belt for men</span><span class=fs-12>Accessories</span>
                  </div>
                  <span class=fs-12>Qty: 1pcs</span>
                </div>
              </div>
              <div class=product-price>
                <h6>$50</h6>
              </div>
            </div>
            <div class="mt-5 amount row">
              <div class="d-flex justify-content-center col-md-6">
                <img src=https://i.imgur.com/AXdWCWr.gif width=250 height=100>
              </div>
              <div class=col-md-6>
                <div class=billing>
                  <div class="d-flex justify-content-between">
                    <span>Subtotal</span><span class=font-weight-bold>$120</span>
                  </div>
                  <div class="d-flex justify-content-between mt-2">
                    <span>Shipping fee</span><span class=font-weight-bold>$15</span>
                  </div>
                  <div class="d-flex justify-content-between mt-2">
                    <span>Tax</span><span class=font-weight-bold>$5</span>
                  </div>
                  <div class="d-flex justify-content-between mt-2">
                    <span class=text-success>Discount</span><span class="font-weight-bold text-success">$25</span>
                  </div>
                  <hr>
                  <div class="d-flex justify-content-between mt-1">
                    <span class=font-weight-bold>Total</span><span class="font-weight-bold text-success">$165</span>
                  </div>
                </div>
              </div>
            </div>
            <span class=d-block>Expected delivery date</span><span class="font-weight-bold text-success">12 March 2020</span><span class="d-block mt-3 text-black-50 fs-15">We will be sending a shipping confirmation email when the item is
              shipped!</span>
            <hr>
            <div class="d-flex justify-content-between align-items-center footer">
              <div class=thanks>
                <span class="d-block font-weight-bold">Thanks for shopping</span><span>Amazon team</span>
              </div>
              <div class="d-flex flex-column justify-content-end align-items-end">
                <span class="d-block font-weight-bold">Need Help?</span><span>Call - 974493933</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      <% }) %>
    </div>
    <script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.bundle.min.js" ></script>
  </body>
</html>

The above ejs markup further compiles to the HTML and produces a markup of four invoices. If you want to break a page after a certain element then do not forget to attach break-after: page; style rules to that element.

./lib/pdfGenerator.js

const puppeteer = require("puppeteer")

const pdfGenerator = async (link) => {
    const pdfPath = Date.now()
    const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox'],
    });
    
    const page = await browser.newPage();
    await page.goto(link);
    await page.emulateMediaType('screen'); // to get media styles
    await page.pdf({
        format: 'a4',
        printBackground: true,
        path: `${pdfPath}.pdf`,
    });
    await browser.close();
    return `/${pdfPath}.pdf`;
}

module.exports = pdfGenerator;

In the PdfGenerator.js, You’ve to lunch the puppeteer browser with a headless flag and go to the desired URL on the new page. finally, you can generate PDF files by using the page.pdf() method.

While generating a PDF, always specify the path in the pdf method option otherwise it won’t be saved to the disk.

you can check the full range of options available for the pdf method of the puppeteer library.

You can find complete source code of above article on github https://github.com/onexabhishek/html-to-pdf-nodejs .

Share and Enjoy !

Shares

This Post Has 2 Comments

  1. Twicsy

    Hi there would you mind stating which blog platform you’re working with?
    I’m going to start my own blog in the near future but I’m having a hard time selecting between BlogEngine/Wordpress/B2evolution and Drupal.
    The reason I ask is because your design and style seems different then most blogs
    and I’m looking for something unique. P.S My apologies for being off-topic but I had to ask!

    1. Abhishek Singh

      Go with WordPress

Leave a Reply