Skip to content
SnapDOMTry SnapStudioGitHub★ 8K
Official plugin · MIT

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).

How it works

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.

OptionDefaultWhat it does
formValuestrueInclude the values typed in inputs and the selected option. Passwords become bullets.
shadowtrueRead text inside open shadow roots.
breakAvoidtrueMeasure blocks so page breaks avoid cutting them.
outlinetrueTurn headings into bookmarks. A selector string picks other elements; false turns it off.
formstrueMeasure 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

OptionDefaultWhat 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'.
margin24Margin on every side, in points. Ignored on page: 'fit'.
repeatHeaderstrueRepeat 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.

OptionDefaultWhat it does
imagetrueDraw 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.
quality0.92JPEG quality. It only matters when JPEG wins, which on normal pages is rare.
backgroundColornoneFill 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

OptionDefaultWhat it does
texttrueAdd the invisible text layer, so the text can be selected, searched and copied.
linkstrueMake links clickable. Same-page anchors jump inside the PDF.
debugTextfalsePaint 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

OptionDefaultWhat it does
embedFontstrueEmbed the page's web fonts, only the letters the document uses. 'full' embeds whole fonts; false none.
vectorTextfalseDraw 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

OptionDefaultWhat it does
outlinetrueBookmarks from headings. Set it on pdf().
taggedtrueTag headings, paragraphs, lists, tables and figures for screen readers.
langfrom the pageDocument 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

OptionDefaultWhat it does
fieldstrueTurn 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

OptionDefaultWhat it does
headernoneText, a function, { left, center, right, size, font, color, rule, gap } or an element.
footernoneThe same, at the bottom.
pageNumbersfalsetrue 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

OptionDefaultWhat it does
covernoneAn element captured as the first page.
tocnoneA table of contents from the headings. true, a title, or { title, levels, size, font, indent, dots, label, color, leaderColor }.
backnoneAn element captured as the last page.
watermarknoneText 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

OptionDefaultWhat it does
metanone{ 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

OptionDefaultWhat it does
downloadfalseA file name, or true for snapdom.pdf. The Blob is still returned.
deflatetrueCompress the PDF's internal streams.

Progress, cancel and report

OptionDefaultWhat it does
onProgressnoneCalled with { phase, done, total } as pages are made. total is null until the page count is known.
signalnoneAn AbortSignal. Aborting stops the export with an AbortError.
onReportnoneCalled 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.

Try it hereLive PDF export

Editable source

Generated output

Nothing generated yet.

Edit the source, then generate a PDF.

Code for this demo