HTML to PDF API: How to Get Pixel-Perfect Output
September 5, 2026


Why HTML-to-PDF Output Breaks (Even in a Real Browser)
Every HTML-to-PDF API does the same fundamental thing: takes a live browser render and freezes it into a fixed, paginated document. That's why fidelity breaks even when the underlying engine is a real browser. A webpage is a fluid, continuously reflowing surface; a PDF is a rigid grid of fixed-size pages. Something has to give, and the gap between what you see on screen and what lands in the PDF is where most bugs live.
A headless browser PDF isn't a screenshot glued into a document — it's a full re-render under print rules, at a specific viewport, with a specific set of assets loaded (or not) at the moment the snapshot happens. Get any one of those variables wrong and you get a PDF that technically "works" but doesn't match the source page: missing backgrounds, wrong fonts, cut-off tables, blank sections where JavaScript hadn't finished rendering. Each failure mode maps to a specific rendering setting, and each one has a specific fix. If you're building or evaluating an html to pdf api, understanding those settings is the difference between a tool that "sort of works" and one that reliably produces a pixel-perfect pdf from html.
The Settings That Actually Control Pixel-Perfect Output
Underneath most modern conversion tools sits the same machinery: Chrome DevTools Protocol's Page.printToPDF, usually accessed through Puppeteer's page.pdf() or Playwright's PDF generation API. Older tools like WeasyPrint or wkhtmltopdf use their own rendering engines, which is why they diverge even further from real browser output — they don't run a full Chromium layout and JS engine. Whichever stack you're on, output fidelity comes down to a handful of concrete levers.
Print CSS and @page Rules
Browsers apply a separate @media print stylesheet and honor the CSS @page rule for margins, size, and page-box behavior — but headless Chrome's support for @page is narrower than what you'd expect from a browser's print preview dialog. @page margins in Chrome's printToPDF are largely overridden by the API's own margin parameters unless you explicitly pass preferCSSPageSize: true. Developers frequently write a valid @page { size: A4; margin: 2cm; } rule, test it in Ctrl+P, and then get a completely different layout from the API because that flag wasn't set. Design your print stylesheet assuming headless Chrome will apply your @page rules only when told to prefer them.
Wait Conditions: Rendering Before You Print
If the PDF snapshot fires before the page has finished painting, you get blank sections, missing charts, or half-loaded images — the print equivalent of scraping a page too early. The fix is the same discipline used in dynamic content scraping: don't rely on the load event alone. For pages with async data fetching, wait on a network-idle condition, a specific DOM selector appearing, or a custom "ready" signal your app sets after rendering completes. Web fonts are a common casualty too — if document.fonts.ready hasn't resolved before the snapshot, you'll get fallback fonts instead of your intended typeface.
Viewport, Scale, and Device Pixel Ratio
The viewport width you render at determines which CSS breakpoints apply, so a layout that looks right at 1440px can break awkwardly once squeezed into a fixed paper size. Set the viewport explicitly to match your intended print layout rather than trusting a default. Separately, deviceScaleFactor controls image and rendering sharpness — a factor of 1 on a high-DPI source image can produce a soft, blurry PDF, while bumping it to 2 sharpens raster content at the cost of a larger file. Viewport and DPI are independent knobs: one governs layout, the other governs crispness.
Backgrounds, Fonts, and Images
Background colors, gradients, and background-images vanish by default because browsers historically excluded backgrounds from print output to save ink — headless Chrome preserves that default. The fix is the printBackground flag; set it to true and Chrome will render CSS backgrounds and box-shadows as they appear on screen. Web fonts need to actually finish loading (see the wait-condition point above) and should be embedded or linked via standard @font-face rules reachable at render time — self-hosted fonts are more reliable than relying on a third-party font CDN that might be slow or blocked.
Headers, Footers, and Page Breaks
Consistent headers, footers, and page numbers across a multi-page PDF are handled through headerTemplate and footerTemplate options, which accept HTML snippets and support special classes like pageNumber and totalPages. These render independently of your main document flow, so content-driven page breaks won't affect them. For controlling where breaks happen inside the body, use CSS break-inside: avoid on tables, figures, and card-like blocks, plus break-before/break-after on section boundaries — this is what stops a table row or paragraph from being sliced in half across two pages.
Common Pixel-Perfection Bugs and Quick Fixes
- PDF missing background colors/images —
printBackgroundisn't enabled; turn it on. - PDF cut off content mid-table or mid-paragraph — add
break-inside: avoidto the element; check margins vs.@pagesize mismatches. - PDF wrong font / fallback font — fonts weren't loaded before the snapshot; wait on
document.fonts.readyand confirm font URLs are reachable. - Blank sections or missing charts — the wait condition fired too early; switch from
loadto a network-idle or selector-based wait. - Layout fine on screen, broken in PDF — viewport width at render time doesn't match the print layout you designed for; set it explicitly.
- @page margins ignored —
preferCSSPageSizewasn't passed; the API's own margin parameters are overriding your CSS.
Generating a Pixel-Perfect PDF via API
Applying all of this manually means juggling Chrome flags, Puppeteer wait logic, and font hosting on your own infrastructure — or you can call one endpoint. A typical request to a generate-pdf-from-url API looks like this:
POST https://api.browsevra.com/v1/pdf
{
"url": "https://example.com/invoice/1234",
"printBackground": true,
"preferCSSPageSize": true,
"waitUntil": "networkidle0",
"viewport": { "width": 1240, "deviceScaleFactor": 2 },
"margin": { "top": "1cm", "bottom": "1cm" },
"headerTemplate": "",
"footerTemplate": "Page of "
}
This single call handles the wait condition, background rendering, DPI, and footer page numbers discussed above — no headless Chrome instance to patch, scale, or keep alive. For readers newer to the underlying tech, this overview of headless Chrome APIs covers what's happening under the hood.
Frequently Asked Questions
Why does my PDF look different from the actual webpage?
The most common causes are a missing printBackground flag, a premature wait condition that snapshots the page before JS or fonts finish loading, and a viewport width that doesn't match the layout you designed for. Each is independently controllable, so mismatches are almost always a settings problem, not a rendering-engine limitation.
How do I add headers and footers to a PDF generated from HTML?
Use the headerTemplate and footerTemplate parameters available in Puppeteer, Playwright, and most headless-chrome-based PDF APIs. They accept raw HTML and support special classes like pageNumber and totalPages for automatic page numbering, and render independently from your main page content.
Does Chrome's @page CSS rule actually work in headless mode?
Yes, but only partially by default — headless Chrome's printToPDF overrides @page margins and size with its own API parameters unless you explicitly pass preferCSSPageSize: true. Without that flag, your CSS-defined page size and margins will be silently ignored.
How do I stop images and backgrounds from disappearing in my PDF?
Set printBackground: true in your PDF generation request, since browsers exclude backgrounds from print output by default to save ink. Also confirm images have fully loaded before the snapshot fires by using a network-idle wait condition rather than the basic load event.
Can I control where page breaks happen in an HTML to PDF export?
Yes, using standard CSS properties like break-inside: avoid on tables, images, or card elements, and break-before/break-after on section boundaries. These prevent content blocks from being split awkwardly across two pages without needing custom pagination logic.
What's the fastest way to generate PDFs from HTML at scale?
Calling a managed HTML-to-PDF API rather than self-hosting Puppeteer or Playwright infrastructure removes the need to manage Chrome flags, scaling, and font hosting yourself. A single API request can apply print background, viewport, wait conditions, and header/footer templates in one call.
Skip the CDP flag debugging and the Chrome infrastructure upkeep — check the Docs for the exact PDF endpoint parameters, and the Pricing page for usage-based costs, then call one endpoint from browsevra instead of maintaining your own rendering stack.