--- title: PDF Generation using jsPDF + html2canvas authors: joanne-w --- ## Overview This document provides a guide on how to generate PDF in javascript using the libraries `jsPDF` and `html2canvas`. Our goals includes convert HTML content into PDF file with the ability to customize the layout and handle page splitting. ## Prerequisites You only need to install the following libraries to complete today's goal. ```shell yarn add jspdf yarn add html2canvas ``` ## Getting Started with jsPDF :::info jsPDF ***jsPDF*** is a library that allows us to generate PDF using javascript ::: To create a PDF file using jsPDF, simply import `jsPDF`, add contents onto the document, then save it. ```javascript import { jsPDF } from "jspdf"; // Default export is a4 paper, portrait, using millimeters for units const pdf = new jsPDF(); pdf.text("Hello world!", 10, 10); pdf.save("a4.pdf"); ``` #### addPage() Adds (and ***transfers the focus to***) new page to the PDF document. ```javascript const pdf = new jsPDF(); // focus is on the first page pdf.addPage(); // now focus is on the second page ``` #### addImage(imageData, format, x, y, width, height, alias, compression, rotation) ```javascript pdf.addImage( imageData, // imageData - DataUrl, CanvasElement ... 'PNG', // format - 'PNG', 'JPEG' ... x, // x - x position against left edge of the page y, // y - y position against upper edge of the page imgWidth, // width - width of the image imgHeight, // height - height of the image ); ``` ## Getting Started with html2canvas :::info html2canvas ***html2canvas*** is a library that allows us to capture screenshot of html elements and convert it to canvas ::: `html2canvas` traverses through the DOM and returns a `Promise` with the generated canvas ```javascript import html2canvas from 'html2canvas'; html2canvas(document.body).then(function(canvas) { document.body.appendChild(canvas); }); ``` ## Convert HTML to PDF To convert HTML to PDF file, the progress involves two steps: 1. convert the HTML file to canvas using `html2canvas` 2. add the canvas image to PDF file in the appropriate position The example below demonstrate how to generate a single page PDF: ```javascript /** constants */ // a4 size [595.28,841.89] const A4_HEIGHT = 841.89; const A4_WIDTH = 595.28; const WIDTH_MARGIN = 10; const HEIGHT_MARGIN = 10; const PAGE_HEIGHT = A4_HEIGHT - 2 * HEIGHT_MARGIN; /** create pdf instance */ const pdf = new jsPDF('p', 'pt', 'a4'); // orientation, unit, format /** convert html to canvas * note that html2canvas returns a Promise */ const body = document.getElementById('#content-id'); const canvas = await html2canvas(body as HTMLElement); /** calculate the imgWidth, imgHeight to print on PDF * so it can scale in equal proportions*/ const canvasWidth = canvas.width; const canvasHeight = canvas.height; const imgWidth = A4_WIDTH - 2 * WIDTH_MARGIN; // max image width const imgHeight = (imgWidth / canvasWidth) * canvasHeight; // scale height in equal proportion /** print pageImg on to pdf */ const pageImg = canvas.toDataURL('image/png', 1.0); const usedHeight = HEIGHT_MARGIN; pdf.addImage( pageImg, // img DataUrl 'PNG', WIDTH_MARGIN, // x - position against the left edge of the page usedHeight, // y - position against the upper edge of the page imgWidth, imgHeight, ); /** save the pdf */ pdf.save(`myPDF.pdf`); ``` ### Split the content into multiple pages If the HTML content exceeds the height of a single page, we need to split the content into multiple pages. ```javascript /** constants */ // a4 size [595.28,841.89] const A4_HEIGHT = 841.89; const A4_WIDTH = 595.28; const WIDTH_MARGIN = 10; const HEIGHT_MARGIN = 10; const PAGE_HEIGHT = A4_HEIGHT - 2 * HEIGHT_MARGIN; /** create pdf instance */ const pdf = new jsPDF('p', 'pt', 'a4'); // orientation, unit, format /** convert html to canvas */ const body = document.getElementById('#content-id'); const canvas = await html2canvas(body as HTMLElement); /** calculate the imgWidth, imgHeight to print on PDF * so it can scale in equal proportions*/ const canvasWidth = canvas.width; const canvasHeight = canvas.height; const imgWidth = A4_WIDTH - 2 * WIDTH_MARGIN; const imgHeight = (imgWidth / canvasWidth) * canvasHeight; /** print pageImg on to pdf */ const pageImg = canvas.toDataURL('image/png', 1.0); let position = HEIGHT_MARGIN; if (imgHeight > PAGE_HEIGHT) { // need multi page pdf let heightUnprinted = imgHeight; while (heightUnprinted > 0) { pdf.addImage( pageImg, 'PNG' WIDTH_MARGIN, position, imgWidth, imgHeight ); // draw the margin top and margin bottom if needed pdf.setFillColor(255, 255, 255); pdf.rect(0, 0, A4_WIDTH, HEIGHT_MARGIN, 'F'); // margin top pdf.rect(0, A4_HEIGHT - HEIGHT_MARGIN, A4_WIDTH, HEIGHT_MARGIN, 'F'); // margin bottom heightUnprinted -= PAGE_HEIGHT; position -= PAGE_HEIGHT; // next vertical placement // add another page if there's more contents to print if (heightUnprinted > 0) pdf.addPage(); } } else { // print single page pdf } /** save the pdf */ pdf.save(`myPDF.pdf`); ``` > But charts and tables might be cut. What can we do? > > ![](https://git.toolsfdg.net/raw/gist/joanne-w/8ff10f8d5fac303619e5244520de6fb2/raw/525c07d47b6c46762f1bc2e5446b8ac3ec91a82e/page-cut.png) ### Recommend: Solve the page cutoff issue To ensure that charts are not cut off, it is recommended to define ***the smallest printable unit*** of DOM elements. Generate individual canvases for each unit and then print each of them on PDF. This also allows us to have better control over the layout of the PDF document. ```javascript /** define your group of elements to print */ const elementIds = ['#content-id', '#content-id2', '#content-id3']; const canvases = await Promise.all( elementIds.map(id => { const element = document.getElementById(id); return html2canvas(element as HTMLElement); } ); const pdf = new jsPDF('p', 'pt', 'a4'); canvases.forEach(canvas => { // print canvas onto pdf // check if print the canvas will exceed page height // if exceed page height, addPage(); // otherwise, addImage() with proper x, y position }); pdf.save('myPDF.pdf'); ``` ## Tips and Tricks ### Remove unwanted DOM elements from generated PDF In some cases, certain DOM elements (e.g. input fields, buttons) are unnecessary and should be excluded from the generated PDF. To remove these elements, add the `data-html2canvas-ignore` attribute to the html element. `html2canvas` will ignore these elements during canvas conversion. ```jsx <Flex data-html2canvas-ignore> <SearchInput value={searchKeywords.value} size='sm' placeholder='Search Name' onChange={onChangeSearchKeywords} ></SearchInput> </Flex> ``` ### Add watermark to PDF The idea of how to add watermark to PDF is quite simple. Keep in mind that a watermark is **just another image**. So, first, create a watermark image. You can use any tools or methods to generate the watermark. Then, utilize the `PDF.addImage()` method to add watermark to each page on your PDF file. ```javascript const addWatermark = () => { const watermarkImg = getWatermarkImg(); pdf.addImage( watermarkImg, 'PNG', 0, 0, WATERMARK_WIDTH, WATERMARK_HEIGHT, ); }; // ... for example, add the watermark before add a new page addWatermark(); pdf.addPage(); ``` ### Keep some margins for each PDF page To make the PDF look nicer, we may want to keep some margins for each PDF page. This can be achieved by adjusting the positioning and draw custom margin block. ```javascript pdf.addImage( pageImg, 'PNG' WIDTH_MARGIN, position, imgWidth, imgHeight ); // draw the margin top and margin bottom if needed pdf.setFillColor(255, 255, 255); pdf.rect(0, 0, A4_WIDTH, HEIGHT_MARGIN, 'F'); // margin top pdf.rect(0, A4_HEIGHT - HEIGHT_MARGIN, A4_WIDTH, HEIGHT_MARGIN, 'F'); // margin bottom ``` ## Reference - [jsPDF](https://raw.githack.com/MrRio/jsPDF/master/docs/index.html) - [html2canvas](https://html2canvas.hertzen.com/documentation)