Test PDF outputs by converting per-page to images (`pdftocairo` / pdf2image / Poppler) and running pixel-diff (pixelmatch / Resemble.js / Pillow `ImageChops`) against approved baselines. Per-page-range targeting, threshold tuning, font-substitution warnings, byte-stable PDF metadata stripping (CreationDate, /ID); references/ carry cross-engine HTML→PDF regression (Chromium `page.pdf()` / WeasyPrint / wkhtmltopdf per-engine baselines, engine-agreement tests, font-embedding checks, engine-version pinning). Use when a product generates invoices, contracts, or regulatory filings whose layout must not shift, when a PDF template, font pack, or generation library is about to change, or when swapping / upgrading the PDF engine.
69
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Companion reference for pdf-snapshot-tester. Consult when migrating from
one HTML→PDF engine to another (wkhtmltopdf → Chromium, wkhtmltopdf →
WeasyPrint), when shared templates render through more than one engine, or
after an engine version upgrade (Chromium revs change PDF output; WeasyPrint
major versions break layout subtly).
Different engines produce different output for the same input - fonts embed
differently, @page support varies, page-break algorithms differ. Tests
verify the chosen engine produces the expected output AND (optionally) that
two engines agree on the critical pages.
Chromium via Playwright:
npm install -D @playwright/testconst browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(loadInvoiceHTML('inv_001'));
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
});
await writeFile('out/chromium.pdf', pdf);WeasyPrint (per the WeasyPrint docs; requires Python 3.10+):
pip install weasyprintfrom weasyprint import HTML
HTML(string=html_str, base_url="https://localhost:3000/").write_pdf("out/weasyprint.pdf")
# CLI: weasyprint invoice.html out/weasyprint.pdfwkhtmltopdf (no longer actively maintained; verify suitability):
apt-get install -y wkhtmltopdf
wkhtmltopdf --page-size A4 \
--margin-top 20mm --margin-right 20mm \
--margin-bottom 20mm --margin-left 20mm \
--enable-local-file-access \
invoice.html out/wkhtmltopdf.pdfEach engine gets its own baseline set - don't expect engines to be identical to each other. The pixel-diff mechanics are SKILL.md's job:
import pytest
from pathlib import Path
ENGINES = ["chromium", "weasyprint", "wkhtmltopdf"]
@pytest.mark.parametrize("engine", ENGINES)
def test_invoice_per_engine(engine, tmp_path):
actual = generate_invoice(engine, "inv_001", tmp_path)
baseline_dir = Path(f"tests/pdf-baselines/{engine}/inv_001")
assert_pdf_matches(actual, baseline_dir, threshold=0.005)For pages where layout MUST be identical across engines (regulatory filings, forms with strict positioning), compare extracted positions with tolerance - never pixel-perfect across engines:
def test_form_field_positions_agree_across_engines():
chromium_fields = extract_form_fields(generate("chromium"))
weasyprint_fields = extract_form_fields(generate("weasyprint"))
for field_name, chrome_pos in chromium_fields.items():
weasy_pos = weasyprint_fields[field_name]
# Allow ~2mm tolerance
assert abs(chrome_pos.x - weasy_pos.x) < 5
assert abs(chrome_pos.y - weasy_pos.y) < 5pdfinfo -list-embedded-fonts out/chromium.pdfdef test_required_fonts_embedded(engine):
fonts = list_embedded_fonts(generate("invoice", engine))
assert "InterVariable" in fonts or any("Inter" in f for f in fonts)
# System fallbacks indicate a font miss
assert "Times" not in fonts
assert "Helvetica" not in fontsCapture which @page features each engine handles for your templates (verify per current engine version - features evolve; per MDN Paged Media, "marks" / "bleeds" support is browser-limited):
| Feature | Chromium | WeasyPrint | wkhtmltopdf |
|---|---|---|---|
@page :first / :left / :right | partial | full | none |
running() headers | none | full | none |
target-counter() | none | full | none |
bleeds, marks | none | partial | none |
Engine upgrades change output - pin in CI; bump intentionally with baseline updates in the same PR:
- name: Install WeasyPrint
run: pip install weasyprint==68.1
- name: Install Playwright (with pinned Chromium)
run: |
npm install -D @playwright/test@1.50.0
npx playwright install --with-deps chromium| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Same baseline for all engines | Output differs per engine | Per-engine baseline sets |
| Skip font-embedding check | OS-default fonts substitute silently | pdfinfo -list-embedded-fonts assertion |
| Test only the chosen engine during a migration | Migration sandbagged | Per-engine baselines for both engines |
| Auto-bump engine version in CI | Output silently shifts | Pin versions |
| Compare engines pixel-perfect | They differ naturally; test always fails | Cross-engine = positions + counts with tolerance |
print-stylesheet-tests - CSS print-media verification (pre-PDF)