@timkozlov/html-to-pdf
v1.0.2
Published
Convert a live DOM element into a vector PDF in the browser — real text and shapes, not a screenshot.
Maintainers
Readme
html-to-pdf
Convert a live DOM element into a vector PDF in the browser — real selectable text and crisp shapes, not a screenshot.
Instead of rasterizing (html2canvas-style), this library clones your element, lets the browser lay it out at the page width, and transcribes that layout into PDF primitives: background colors become filled rects, borders become vector strokes and fills, text becomes real positioned PDF text (searchable, selectable, crisp at any zoom), and images are embedded at their exact rects. Output files are small and print sharp.
Quick start
<script src="dist/html-to-pdf.iife.min.js"></script>
<script>
// the bundle is self-contained (jsPDF included)
await htmlToPdf(document.querySelector("#invoice"), {
output: "save",
filename: "invoice.pdf",
});
</script>Or as a module (jsPDF is a regular dependency here):
import { htmlToPdf } from "@timkozlov/html-to-pdf";
const blob = await htmlToPdf(element); // Promise<Blob>
const bytes = await htmlToPdf(element, { output: "arraybuffer" });
const doc = await htmlToPdf(element, { output: "jspdf" }); // the jsPDF instanceThe element you pass is never touched: a clone is laid out off-screen at the page's printable width and removed afterwards.
Options
| Option | Default | Meaning |
| --- | --- | --- |
| format | "a4" | "a4", "letter", or [widthPt, heightPt] |
| orientation | — | "portrait" / "landscape"; swaps the page dimensions |
| margins | 36 | pt; number or { top, right, bottom, left } |
| scale | 1 | Content zoom. 1 maps CSS px→pt at the browser-print ratio (16px text = 12pt). >1 prints bigger, <1 fits more per page |
| output | "blob" | "blob" | "save" | "arraybuffer" | "jspdf" |
| filename | "document.pdf" | Used by output: "save" |
| imageScale | 2 | Raster resolution multiplier vs. displayed CSS size |
| imageQuality | 0.92 | JPEG re-encode quality |
| colorFilter | — | (color, use) => color applied to every color drawn; see below |
Color correction (colorFilter)
Every color the renderer draws — backgrounds, borders, text, and each pixel of raster images — is passed through your function before it lands in the PDF. color is { r, g, b, a } (0–255 channels, alpha 0–1) and use is "fill" | "stroke" | "text" | "image". Return values are clamped; non-finite channels fall back to the original.
Useful for fax or low-quality printing, where subtle grays disappear and light tints turn to noise:
const grayscale = (c) => {
const y = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
return { r: y, g: y, b: y, a: c.a };
};
const faxContrast = (c) => {
const y = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
const boosted = 255 / (1 + Math.exp(-(y - 128) / 24));
return { r: boosted, g: boosted, b: boosted, a: c.a };
};
await htmlToPdf(el, { colorFilter: faxContrast, output: "save" });faxContrast pushes light colors toward white and dark colors toward black (a sigmoid around mid-gray) so dividers either disappear cleanly or print solid instead of dithering.
What renders (v1)
- Backgrounds —
background-color(incl. alpha), uniformborder-radius - Borders — per-side widths/colors,
solid(also used fordouble/groove/ridge/inset/outset),dashed,dotted, rounded uniform borders - Text — exact per-line placement from the browser's own layout: font size, weight (≥600 → bold), italic, color,
letter-spacing,text-alignincl. justify,text-transform, underline/overline/line-through,white-space: pre. Fonts map to the standard PDF families (Arial/Helvetica-likes → Helvetica, Georgia/Times-likes → Times, monospace → Courier) - Images —
<img>(PNG/JPEG/SVG sources) and<canvas>, placed in the content box withobject-fit/object-position(cover is clipped), JPEG stays JPEG, alpha preserved via PNG - Effects —
opacity(multiplied down the tree),overflow: hiddenclipping,visibility, multi-page splitting
Multi-page behavior
Content taller than one page is split geometrically at page boundaries: rectangles and vertical lines split exactly at the seam; images and rounded/dashed shapes are drawn clipped per page so their geometry is never distorted; text lines are never torn — each line goes wholly to the page containing its baseline (descenders may dip a few pt into the bottom margin).
Not yet supported
z-index/stacking-context reordering (paint order is document order) · background images & gradients · shadows · CSS transforms · custom font embedding (non-WinAnsi glyphs — Cyrillic/CJK/emoji — warn and render incorrectly) · ::before/::after/list markers · form control values (boxes render, text doesn't) · RTL text · rounded overflow clips · break-inside: avoid (page breaks are geometric) · position: fixed.
Cross-origin images need CORS (crossorigin="anonymous" + proper headers) or they are skipped with a warning.
Development
npm install
npx playwright install chromium # for browser tests
npm run build # dist/: IIFE bundle (jsPDF inlined) + ESM + .d.ts types
npm test # node unit tests + headless-Chromium tests (incl. PDF-byte assertions)
npm run dev # http://localhost:8000/examples/ — live side-by-side DOM vs PDF harnessThe examples/ harness renders each fixture in fixtures/ next to its generated PDF for visual comparison, with page format/margins/scale controls.
How it works
snapshot clone element → hidden sibling at page width → wait for fonts/images
walk computed styles + client rects → draw ops (px, clone-root coords)
text: per-character Range walk → line fragments → baseline from font metrics
paginate pure splitting of ops into pages (never re-derives geometry)
render jsPDF calls; single px→pt scale; text stretched (Tz) to match DOM width exactlyThe op stream between stages is what makes the geometry testable: browser tests assert emitted ops directly against getBoundingClientRect, and e2e tests tokenize the uncompressed PDF content streams and check final page coordinates to fractions of a point.
Roadmap
Custom TTF font embedding (removes the WinAnsi limit) · background images · break-inside: avoid pagination hints · list markers · a pdf-lib renderer backend.
