Export a searchable PDF.
Capture a live element, add selectable text and links, and paginate it into a PDF in your browser.
Install
npm install @zumer/snapdom @zumer/snapdom-pdf
Requires SnapDOM 3.4.0 or newer within 3.x. Source and MIT license. Without a bundler, map both packages in an import map and
serve the whole @zumer/snapdom-pdf folder, because its index.js imports
files from src/.
<script type="importmap">
{
"imports": {
"@zumer/snapdom": "/vendor/snapdom.mjs",
"@zumer/snapdom-pdf": "/vendor/snapdom-pdf/index.js"
}
}
</script>
Quick start
import { snapdom } from '@zumer/snapdom'
import { pdf } from '@zumer/snapdom-pdf'
snapdom.plugins(pdf())
const shot = await snapdom(document.querySelector('#report'))
const blob = await shot.toPdf({ page: 'a4', download: 'report.pdf' })
Register the plugin before you capture. A capture made without it has no
toPdf(). To use it on one capture only, pass it there:
snapdom(el, { plugins: [pdf()] }).
toPdf() returns a PDF Blob. download also saves the file.
shot.to('pdf', options) does the same as toPdf(options).
The page image plus a PDF text layer
The page image is SnapDOM's capture. The plugin places a second, invisible text layer over it using positions derived from SnapDOM's final serialized clone. No headless browser or separate layout engine rebuilds the page.
By default, the image preserves the capture’s appearance while the text layer adds selection, search and copy. Paper sizes add pagination; optional fields keep supported controls fillable. Vector text, headers and footers, encryption and structured reports are configured below.
Options on pdf()
Set these options on pdf(): they decide which text, controls and pagination hints
the plugin reads from the captured artifact. A later toPdf() call cannot change them.
| Option | Default | What it does |
|---|---|---|
| formValues | true | Include the values typed in inputs and the selected option. Passwords become bullets. |
| shadow | true | Read text inside open shadow roots. |
| breakAvoid | true | Measure blocks so page breaks avoid cutting them. |
| outline | true | Turn headings into bookmarks. A selector string picks other elements; false turns it off. |
| forms | true | Measure form controls so the PDF can have fillable fields. |
Every other option works in either place. On pdf() it becomes the default for every
export; in toPdf() it applies to that export and wins.
Pages
| Option | Default | What it does |
|---|---|---|
| page | 'fit' | 'fit' makes one page the size of the capture. Paper sizes: 'a3' to 'a6', 'b4', 'b5', 'letter', 'legal', 'tabloid', 'executive', or [width, height] in points. |
| orientation | 'portrait' | 'portrait' or 'landscape'. |
| margin | 24 | Margin on every side, in points. Ignored on page: 'fit'. |
| repeatHeaders | true | Repeat a table's <thead> on each page the table continues to. |
Page breaks avoid table rows, list items, figures, quotes, images and anything with
break-inside: avoid. CSS page breaks are respected. A block taller than a page still
has to split.
Image and file size
What you see in the PDF is an image of the capture, with invisible text on top so it can be selected and searched.
| Option | Default | What it does |
|---|---|---|
| image | true | Draw the captured image. With false, only PDF text and annotations remain; use vectorText: true with embedded fonts to make eligible text visible. |
| codec | 'auto' | Try JPEG and lossless and keep the smaller file, per image. 'jpeg' or 'flate' forces one. |
| quality | 0.92 | JPEG quality. It only matters when JPEG wins, which on normal pages is rare. |
| backgroundColor | none | Fill transparent areas with a colour. Usually makes the file smaller. |
Sharpness and size come from the capture: snapdom(el, { scale, dpr }). Both multiply
the image's pixels and leave the page layout alone. The image keeps that many pixels per CSS pixel
at the size it is printed. A capture enlarged to fill the paper is drawn at the enlarged size, so
it stays as sharp as on screen. dpr defaults to the screen of the machine that
exports, so set it, for example to 2, to get the same file everywhere.
Text and links
| Option | Default | What it does |
|---|---|---|
| text | true | Add the invisible text layer, so the text can be selected, searched and copied. |
| links | true | Make links clickable. Same-page anchors jump inside the PDF. |
| debugText | false | Paint the text layer red, to check where it sits. |
Links work for http:, https:, mailto: and
tel:. Any other link is left out with a warning.
Fonts and sharp text
| Option | Default | What it does |
|---|---|---|
| embedFonts | true | Embed the page's web fonts, only the letters the document uses. 'full' embeds whole fonts; false none. |
| vectorText | false | Draw the text as real letters instead of pixels: sharp at any zoom. |
The PDF can only embed fonts the capture carries, so capture with
snapdom(el, { embedFonts: true }). System fonts have no file to embed. Copying text
works the same with or without embedded fonts.
vectorText needs embedded fonts. Right-to-left and complex scripts stay as image.
With image: false it gives a very small text-only PDF, without borders, backgrounds or
pictures.
Bookmarks and accessibility
| Option | Default | What it does |
|---|---|---|
| outline | true | Bookmarks from headings. Set it on pdf(). |
| tagged | true | Tag headings, paragraphs, lists, tables and figures for screen readers. |
| lang | from the page | Document language, for example 'en' or 'es-MX'. |
The PDF is marked accessible (PDF/UA-1) only when it qualifies: every font embedded, a language,
a meta.title, alt text on every image and fields: false. There is no
option to force it. When something is missing, the report says what.
Fillable forms
| Option | Default | What it does |
|---|---|---|
| fields | true | Turn form controls into fields you can fill in the PDF. false keeps their values as plain text. |
Text inputs, textareas, checkboxes, radio buttons and selects become fields with their name, value and label. Read-only, disabled, required and maxlength carry over. Password fields are written empty. Buttons, sliders, colour pickers and file inputs stay as image. The PDF never contains scripts or submit actions.
Header, footer and page numbers
| Option | Default | What it does |
|---|---|---|
| header | none | Text, a function, { left, center, right, size, font, color, rule, gap } or an element. |
| footer | none | The same, at the bottom. |
| pageNumbers | false | true writes "1 / 4". { format, align, size, font, color } changes it. |
await shot.toPdf({
page: 'a4',
header: { left: 'North Studio', right: 'Delivery report' },
pageNumbers: { format: (page, pages) => `Page ${page} of ${pages}` },
})
These need a paper size and do nothing on page: 'fit'. Text is real PDF text. An
element is captured as an image, so its text cannot be selected.
Cover, contents, back page and watermark
| Option | Default | What it does |
|---|---|---|
| cover | none | An element captured as the first page. |
| toc | none | A table of contents from the headings. true, a title, or { title, levels, size, font, indent, dots, label, color, leaderColor }. |
| back | none | An element captured as the last page. |
| watermark | none | Text or an element over every page. { text, opacity, angle, size, font, color } or { element, opacity, angle, fit }. |
Cover, contents and back page need a paper size. The watermark also works on
page: 'fit'.
Metadata
| Option | Default | What it does |
|---|---|---|
| meta | none | { title, author, subject, keywords, creator, created, modified }. Viewers show the title instead of the file name. |
Dates are written only when you pass them. Pass new Date() when you want today. The
same captured state with the same options produces identical unencrypted bytes. Encryption uses fresh randomness for each export.
Password
await shot.toPdf({
page: 'a4',
encrypt: { userPassword: 'ada-1815', ownerPassword: 'owner-key' },
})
encrypt protects the file with AES-256. userPassword is required.
ownerPassword defaults to it. permissions takes print,
modify, copy, annotate, fillForms,
assemble and printHighRes, all true by default.
Only the password protects the file. Permissions are requests that a viewer may ignore. Encryption needs https or localhost, and an encrypted export is different every time.
Output
| Option | Default | What it does |
|---|---|---|
| download | false | A file name, or true for snapdom.pdf. The Blob is still returned. |
| deflate | true | Compress the PDF's internal streams. |
Progress, cancel and report
| Option | Default | What it does |
|---|---|---|
| onProgress | none | Called with { phase, done, total } as pages are made. total is null until the page count is known. |
| signal | none | An AbortSignal. Aborting stops the export with an AbortError. |
| onReport | none | Called once the PDF is done, with what happened: pages, size, fields, fonts and warnings. |
await shot.toPdf({
page: 'a4',
onProgress: ({ done, total }) => bar.update(done, total),
signal: controller.signal,
onReport: (report) => console.log(report.pages, report.warnings),
})
Each warning in the report has a code, such as link-unresolved or
text-clipped, so a test can check for them without reading messages.
When something looks wrong
The plugin warns in the console with a [snapdom-pdf] prefix: a block split across
pages, a link without a target, text cut off. The same warnings arrive in the report.
A grey box where an image should be means the capture could not read that image. It shows up in
the capture's result.warnings as image-fallback.
Export a PDF with cross-origin images explains
the fix.
If the PDF does not match the page, report a small example with the capture options, browser and warning codes.
Limits
- Text is an image unless you use
vectorText, so a deep zoom shows the capture's resolution. - Closed shadow roots cannot be read.
- Date inputs are written in ISO format, not as the browser shows them.
- An element used as header, footer or watermark has no selectable text. A repeated table header has no links.
- The full test suite runs in Chromium. Firefox and WebKit are checked for the main features.
Questions
Is the text really selectable, or is it OCR?
It is selectable without OCR. Eligible text is placed from browser geometry in SnapDOM's final serialized clone, so structural exclusions and geometry-preserving replacements stay aligned with the capture. Ambiguous paint effects are omitted fail-safe. Download a sample and copy a paragraph.
Does it need a server?
The exporter itself runs in the browser and returns a Blob; it does not upload
the PDF. SnapDOM may still fetch resources used by the page, or use a proxy if your application
configured one.
Which browsers?
The complete fixture suite is Chromium-only. Both plugin builds must also pass the release artifact smoke in Chromium, Firefox and WebKit. That supports the plugin/API contract in all three; it does not pretend every complex layout has the same three-engine coverage.