How to Fix Stretched Pages With jsPDF addHTML Page Splitting
Diagnose stretched jsPDF addHTML PDFs by checking versions, canvas dimensions, page splitting, CSS breaks, and cross-origin images.
A stretched or badly split PDF usually comes from a mismatch between the HTML layout, the canvas produced by html2canvas, and the jsPDF page size. Start by identifying the rendering path: addHTML is a legacy plugin method, while current jsPDF releases document the html() module. html2pdf.js is a separate wrapper with its own page-break configuration.
Do not copy options between these APIs until you have confirmed the installed versions and the method your code calls.
1. Identify the API and version first
Record the package versions and search the export code for the actual method:
doc.addHTML(...): legacy addHTML plugin, commonly seen with older jsPDF distributions.doc.html(...): the current jsPDF HTML module, which depends on html2canvas and may use DOMPurify for HTML strings. See the jsPDF HTML documentation.html2pdf().from(...).save(): html2pdf.js, with its ownpagebreakoptions. See the html2pdf.js page-break documentation.
// Browser console: inspect loaded versions where available
console.log(window.jspdf?.version || window.jsPDF?.version);
// npm projects
// npm ls jspdf html2canvas html2pdf.js
If the project is using the old plugin, keep the diagnosis below version-specific. If you can migrate, test the modern API in a separate path rather than changing several libraries and CSS rules at once.
2. Trace the dimensions through the pipeline
There are three dimensions to compare:
| Stage | What to inspect | Typical failure |
|---|---|---|
| HTML layout | Computed width, height, box sizing, and media-query state | A narrow export viewport causes different wrapping or a wide fixed element overflows. |
| Rendered canvas | canvas.width, canvas.height, and pixel ratio |
A tall raster is scaled to fit a page, making content look stretched. |
| PDF placement | Page width/height, image width/height, and x/y placement | The canvas aspect ratio is changed or slices are calculated against the wrong page size. |
const el = document.querySelector('#invoice');
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
console.table({
cssWidth: rect.width,
cssHeight: rect.height,
scrollWidth: el.scrollWidth,
scrollHeight: el.scrollHeight,
boxSizing: style.boxSizing,
overflow: style.overflow,
devicePixelRatio: window.devicePixelRatio,
viewportWidth: window.innerWidth
});
For a stable export, choose an explicit content width and preserve the aspect ratio when placing the canvas. Do not fix a stretched result by adding an arbitrary multiplier until you know which stage changed the dimensions.
3. Why legacy addHTML page splitting stretches or clips content
In the old addHTML plugin, enabling pagesplit causes a canvas taller than the PDF page to be divided into successive page-sized slices. This is raster slicing, not semantic HTML reflow. A slice can cut through a paragraph, table row, or image, and an incorrect page-height calculation can make the result appear enlarged or compressed.
// Legacy pattern: keep this only when the project really uses addHTML.
const doc = new jsPDF('p', 'mm', 'a4');
const element = document.querySelector('#invoice');
doc.addHTML(element, {
pagesplit: true,
// Keep the plugin's other options version-specific.
}, () => {
doc.save('invoice.pdf');
});
Use this checklist for a legacy export:
- Measure the element before calling
addHTML. - Inspect the generated canvas if the plugin exposes it, or temporarily render the same element with html2canvas to measure the result.
- Confirm the PDF orientation and paper size match the dimensions used for slicing.
- Place each slice using the same scale factor for width and height.
- Test with a short document and one deliberately tall document to separate scaling errors from page-boundary behavior.
4. A controlled canvas-to-PDF diagnostic
This standalone example helps determine whether the distortion occurs before or during PDF insertion. It uses html2canvas directly and preserves the canvas aspect ratio.
import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
const element = document.querySelector('#invoice');
const canvas = await html2canvas(element, {
// windowWidth controls layout and media queries.
windowWidth: element.scrollWidth,
scale: 2,
backgroundColor: '#ffffff'
});
console.table({ canvasWidth: canvas.width, canvasHeight: canvas.height });
const pdf = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imageHeight = (canvas.height * pageWidth) / canvas.width;
if (imageHeight <= pageHeight) {
pdf.addImage(canvas, 'PNG', 0, 0, pageWidth, imageHeight);
} else {
// Raster slicing: each page receives a proportional vertical crop.
const pageCanvas = document.createElement('canvas');
const pagePixelHeight = Math.floor(canvas.width * pageHeight / pageWidth);
pageCanvas.width = canvas.width;
pageCanvas.height = pagePixelHeight;
const ctx = pageCanvas.getContext('2d');
let offset = 0;
while (offset < canvas.height) {
ctx.clearRect(0, 0, pageCanvas.width, pageCanvas.height);
ctx.drawImage(canvas, 0, -offset);
const usedHeight = Math.min(pagePixelHeight, canvas.height - offset);
const renderedHeight = usedHeight * pageWidth / canvas.width;
pdf.addImage(pageCanvas, 'PNG', 0, 0, pageWidth, renderedHeight);
offset += pagePixelHeight;
if (offset < canvas.height) pdf.addPage();
}
}
pdf.save('diagnostic.pdf');
If this produces correct proportions, the distortion is probably in the legacy plugin’s sizing or page-slice calculation. If it is already wrong, inspect CSS layout, the export viewport, image loading, and canvas options.
5. The modern jsPDF html() path
For current jsPDF, use the HTML module and set container sizing deliberately. windowWidth affects layout and CSS media queries; it is different from the PDF page width and from the width at which the final image is placed.
import { jsPDF } from 'jspdf';
const element = document.querySelector('#invoice');
const pdf = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
await pdf.html(element, {
margin: [10, 10, 10, 10],
autoPaging: 'text',
html2canvas: {
windowWidth: element.scrollWidth,
scale: 2,
useCORS: true,
backgroundColor: '#ffffff'
},
callback: (doc) => doc.save('invoice.pdf')
});
Keep the export container responsive and avoid a CSS width that is much larger than the intended page. If a media query changes the document at export time, set windowWidth to the layout width you actually want and verify the computed styles in that state.
6. Page breaks with html2pdf.js
html2pdf.js has explicit page-break controls. These settings do not belong to legacy addHTML unless your specific plugin implements them.
import html2pdf from 'html2pdf.js';
const element = document.querySelector('#report');
const options = {
margin: 10,
filename: 'report.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: {
mode: ['css', 'legacy'],
before: '.start-new-page',
after: '.end-page',
avoid: ['table', 'figure', '.keep-together']
}
};
html2pdf().set(options).from(element).save();
Use CSS rules for predictable boundaries:
.start-new-page { break-before: page; }
.end-page { break-after: page; }
.keep-together { break-inside: avoid; }
@media print {
.report-header { break-after: avoid; }
}
The avoid-all mode can insert extra breaks to keep elements intact. Apply it selectively because large elements may move to a later page and leave unexpected whitespace. The legacy html2pdf__page-break class is also documented by html2pdf.js, but it is not a general jsPDF option.
7. Prevent common layout causes
- Set
box-sizing: border-boxon the export subtree so borders and padding are included in the intended width. - Replace viewport-relative widths such as
100vwwith a measured export width when scrollbars or mobile emulation change the result. - Give tables a predictable width and allow long text to wrap.
- Break or hide fixed-position navigation, chat controls, and sticky headers during capture.
- Wait for fonts and images before rendering. A late font swap changes line wrapping and total height.
- Do not use CSS transforms to scale the root export element unless you also account for the transformed bounding box.
8. Images, CORS, and blank regions
html2canvas can skip cross-origin images when browser canvas security rules would taint the canvas. Its FAQ describes CORS and proxy approaches. Confirm that images have loaded, are served with appropriate CORS headers, or are proxied before capture.
await Promise.all([...document.images].map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
A missing image is an input problem, not evidence that page splitting itself is wrong. Test the same document with images removed to isolate the variables.
9. Troubleshooting table
| Symptom | Likely cause | Fix |
|---|---|---|
| Everything is uniformly stretched | Canvas width and height were scaled by different factors. | Compute one proportional scale from canvas width to PDF page width. |
| Text wraps differently than in the browser | Export windowWidth or container width differs from the visible layout. |
Inspect computed styles and set an explicit windowWidth. |
| Pages contain large blank areas | avoid-all, oversized elements, or an incorrect page-height calculation. |
Use targeted avoid selectors and check the page and canvas units. |
| Rows or paragraphs are cut in half | Raster slicing has no semantic knowledge of HTML boundaries. | Use CSS page breaks with html2pdf.js or add explicit break markers. |
| Images are missing | Cross-origin restrictions or images not loaded at capture time. | Wait for images and configure CORS or a proxy as documented by html2canvas. |
| Only old code fails after an upgrade | Legacy addHTML options were applied to modern html(), or vice versa. |
Check package versions and rewrite options for the selected API. |
| PDF is huge and text cannot be selected | The HTML was rasterized into images. | Reduce canvas scale and image quality where acceptable, or choose a PDF generation path that preserves text. |
10. Performance, reliability, and cost considerations
- Large, high-scale canvases consume significant memory. Start with the smallest scale that preserves required print quality.
- Long pages require more raster memory and more page slices. Split very large reports at known sections when possible.
- Wait for fonts, images, and asynchronous content before capture to reduce nondeterministic output.
- Use deterministic viewport, timezone, locale, and data when PDFs are generated in CI.
- Cache static assets and avoid rendering hidden application screens that still contain large canvases.
- Raster-based HTML-to-PDF output can create large files and does not preserve selectable/searchable text in the same way as a text-aware PDF renderer.
11. A practical decision path
- Identify whether the code uses
addHTML,doc.html, or html2pdf.js. - Capture computed HTML dimensions and the export viewport.
- Measure the canvas dimensions and verify its aspect ratio.
- Confirm PDF page dimensions and proportional image placement.
- Apply page-break rules from the library actually in use.
- Test cross-origin images and asynchronous content separately.
- Only then tune scale, margins, and image quality.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server when the deliverable can be an image or PDF capture of a URL. Its capture flow accepts cookie and consent banners, removes more than 60 known consent platforms, newsletter popups, and chat widgets, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed.
Use the API documented at ScreenshotNeo docs:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The same service includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. It includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Is pagesplit a semantic page-break engine?
No. Legacy addHTML splitting slices a rendered canvas. It does not understand paragraphs, rows, or headings.
Should I change windowWidth or PDF page width?
Change windowWidth when the HTML layout or media queries are wrong. Change PDF page width or placement when the canvas is correct but insertion distorts it.
Why does a PDF look correct at one viewport and wrong at another?
Responsive CSS, scrollbar width, font loading, and viewport-dependent assets can all change the HTML dimensions before rasterization.
Can html2pdf.js preserve selectable text?
Its documented HTML-to-canvas workflow renders the page as an image, so text selection and searchability are limited and files can be large.
What information is needed to diagnose a specific file?
Provide the jsPDF, html2canvas, and html2pdf.js versions, the exact export method, page format, relevant HTML/CSS, and measured element and canvas dimensions.


