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.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
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)