
Apr 22, 2025 / 456 views / 15 minutes read
A couple of years ago, I was working on a financial management system for an institution that dealt with a significant amount of paperwork. They needed to export various forms and reports as PDFs, each with a different structure. While I encouraged them to adopt a digital workflow and reduce paper usage, they insisted on keeping printable PDFs as part of their operations. At the time, I knew that generating PDFs from HTML was relatively straightforward. However, challenges arose when dealing with pagination, especially when data exceeded a single page size (A4, A5, or other sizes). Some scenarios involved tables with dynamic row counts and unpredictable cell content lengths. Imagine a table with four columns and an unpredictable number of rows, where each cell could contain content of any length! Consequently, some pages might contain a table with four rows, while others might have more or fewer. In short, I spent a considerable amount of time exploring different options, and back then, the choices were limited. I ultimately used the html2pdf.js package, which utilizes jsPDF. In this article, I won't delve into the complexities of implementing such intricate scenarios. Instead, I'll cover the essential aspects that will enable developers to effectively use this package for their own requirements.
I'm using TypeScript and Angular framework here, but you can adapt the code to other JavaScript/TypeScript frameworks like React, Next.js, or Vue.js. The code examples are available in this repository on my GitHub account here.
You can create a new Angular project using the Angular CLI:
ng new <projectname>
Then, answer the follow-up questions according to your project's needs. To use the html2pdf.js package, you'll need to install it:
npm install html2pdf.js --save-dev
Alternatively, you can add:
"html2pdf.js": "0.10.1"
to your package.json file and then run command bellow to install the package.
npm install
Next, you'll need to define the html2pdf.js types by creating an html2pdf.js.d.ts file in the src/types directory:
declare module 'html2pdf.js' { const html2pdf: any; export default html2pdf; }
With these configurations in place, you can import and use the package in your components like this:
import html2pdf from "html2pdf.js";
To configure the print area size, it's crucial to determine the user's screen DPI. Based on the DPI and the desired paper size, you can then calculate the appropriate print area and page dimensions. Paper orientation also needs to be considered. To achieve this, I defined a hidden element within the component:
<div id='testdiv' style='height: 1in; left: -100%; position: absolute; top: -100%; width: 1in;'></div>
Then, I created a function to calculate the print area size based on the user's screen DPI:
private _calculateDisplayDpi(): void { const testDiv = document.getElementById('testdiv'); if (testDiv) { const dpi_x = testDiv.offsetWidth; const dpi_y = testDiv.offsetHeight; switch (dpi_x) { case 72: this.paper.pageWidth = 595; break; case 96: this.paper.pageWidth = 794; break; case 150: this.paper.pageWidth = 1240; break; case 300: this.paper.pageWidth = 2480; break; } switch (dpi_y) { case 72: this.paper.pageHeight = 842; break; case 96: this.paper.pageHeight = 1123; break; case 150: this.paper.pageHeight = 1754; break; case 300: this.paper.pageHeight = 3508; break; } this.paper.dpiClass = `${this.paper.orientation}-${dpi_x}`; this.paper.dpiHeight = dpi_y; this.paper.dpiWidth = dpi_x; } }
This function retrieves the testDiv element and uses its offsetWidth and offsetHeight properties to determine the DPI. The paper object, a component field, stores the relevant properties:
interface IPaper { orientation: string; dpiClass: string; dpiHeight: number | null; dpiWidth: number | null; pageHeight: number | null; pageWidth: number | null; } paper: IPaper = { orientation: "portrait", dpiClass: "", dpiHeight: null, dpiWidth: null, pageHeight: null, pageWidth: null };
Based on the calculated DPI and orientation, I created CSS classes to define the paper and print area sizes accordingly. Here’s an example for A4 in various DPIs:
div.paper.portrait-96 { width: 794px; height: 1123px; } div.paper.landscape-96 { height: 794px; width: 1123px; } div.print-area.portrait-96 { width: 794px; } div.print-area.landscape-96 { width: 1123px; }
You can extend this for A5 and other paper sizes as needed. View the full CSS in the
In your component's template, you can apply these classes dynamically:
<div class="pdfContainer" id="pdf"> <div class="print-area" [ngClass]="paper.orientation + '-' + paper.dpiHeight"> <div class="paper" [ngClass]="paper.orientation + '-' + paper.dpiHeight"> <i class="fakharnia-logo logo"></i> </div> </div> </div>
Here's a table summarizing the height and width values for different DPIs:
| Size | 72 PPI | 96 PPI | 150 PPI | 300 PPI |
|---|---|---|---|---|
| A4 | 595 x 842 | 794 x 1123 | 1240 x 1754 | 2480 x 3508 |
| A5 | 420 x 595 | 559 x 794 | 874 x 1240 | 1748 x 2480 |
When dealing with unpredictable data amounts or complex page layouts, you'll need to manage page breaks and new pages. Since the data volume per page was unpredictable, looping with ngFor alone wasn’t sufficient.
My solution involved using the DOM to monitor the print zone and dynamically adjust content placement. For each data insertion, I calculated the paper canvas height. If the content overflowed, I removed the last element and inserted a new page. While I won't provide the specific implementation details here, I'll outline the key concepts. The jsPDF library offers three CSS properties for controlling page breaks: before, after, and avoid. You can apply these classes to elements to force a page break before (before), after (after), or prevent a page break within (avoid) them.
To manage complex scenarios, I defined four methods: _createElement, insertChildrenToNode, removeLastChildFromNode, and _canvasOverflowed.
_createElement method: This method generates an HTML element with a specified tag, ID, CSS classes, and content, returning the created element.
insertChildrenToNode method: This method takes a node and a list of children as input and appends each child to the node.
removeLastChildFromNode method: This method is the inverse of the previous one. It removes a specified child element from a given node. This is used when content overflows a page, allowing the overflowing element to be removed, a new page generated, and the element re-inserted on the new page.
_canvasOverflowed method: This method is crucial for determining whether the page content exceeds the printable area. It checks the page offset based on the paper orientation, display DPI, and paper size. It returns true if the content overflows and false otherwise.
By combining these four methods, you can effectively manage complex data scenarios. Here's my implementation of these methods:
private _createElement(tag: string, id: string | null, classList: string[] | null = null, innerHtml: string | null = null): HTMLElement { const element = document.createElement(tag); if (id) { element.id = id; } if (classList) { element.classList.add(...classList); } if (innerHtml) { element.innerHTML = innerHtml; } return element; }
private insertChildrenToNode = (node: any) => (...children: any) => children.forEach((child: any) => node.appendChild(child));
private removeLastChildFromNode = (node: any) => (child: any) => node.removeChild(child);
private _canvasOverflowed(canvas: HTMLElement): boolean { let limitHeight: number; switch (this.paper.dpiHeight) { case 72: limitHeight = this.paper.orientation === "portrait" ? 842 - 135 : 595 - 135; break; case 96: limitHeight = this.paper.orientation === "portrait" ? 1123 - 135 : 794 - 135; break; case 150: limitHeight = this.paper.orientation === "portrait" ? 1754 - 135 : 1240 - 135; break; case 300: limitHeight = this.paper.orientation === "portrait" ? 3508 - 135 : 2480 - 135; break; default: return true; } return canvas.offsetHeight > limitHeight; }
Important Note: These code snippets are tailored to my specific use case and haven't been thoroughly optimized. There might be more efficient and robust solutions for handling complex data. I encourage you to explore and refine these approaches based on your needs.
If your data is relatively simple, like a one-page invoice, you can design the page with appropriate CSS and proceed with PDF generation.
Once you've configured everything, you can implement the PDF export functionality. Here's how I implemented the export function to generate and preview the PDF in a new browser tab:
onExportPdf(): void { const pageBreak = { mode: 'css', before: '.before', after: '.after', avoid: '.avoid' }; const options = { filename: "", image: { type: 'jpeg', quality: 1, margin: 0 }, html2canvas: { scale: 2, dpi: this.paper.dpiHeight, logging: true, scrollX: 0, scrollY: -window.scrollY }, pagebreak: pageBreak, jsPDF: { format: [this.paper.pageWidth, this.paper.pageHeight], orientation: this.paper.orientation, putOnlyUsedFonts: true, precision: 1, unit: "px" } }; const element = document.getElementById('pdf'); options.filename = `PDF_${Math.random() * 10}`; const pdf = new html2pdf(); pdf.set(options).from(element).toPdf().get('pdf').then((pdf: any) => { pdf.setProperties({ title: `Generate PDF` }); const pdfData = pdf.output('arraybuffer'); const pdfBlob = new Blob([pdfData], { type: 'application/pdf' }); const blobURL = URL.createObjectURL(pdfBlob); window.open(blobURL, '_blank'); }); }
For a comprehensive guide to the configuration options, refer to html2pdf.js documentation.
In some cases, the jsPDF documentation might also be helpful.
This article doesn't cover every possible question or scenario. My intention was to demonstrate that generating PDFs from HTML is indeed feasible. I've successfully implemented this approach in multiple projects, and my clients have been highly satisfied with the results. It's worth noting that I encountered significant challenges when dealing with documents exceeding 25 pages (A4 size). The limitations of the HTML canvas became apparent, requiring me to split the PDF when its size exceeded the canvas's capacity. This process resulted in browser freezes for large files. To overcome these issues, I implemented logic to split the PDF into multiple parts and process them sequentially. However, for very large files, switching to server-side PDF generation or a Web Worker approach might be more scalable.
I hope this article helps you implement PDF generation with more confidence. Feel free to explore the full code on GitHub and adapt it to your own use cases.