Skip to content

WPT Compliance Gallery

phpdftk renders HTML and SVG to PDF, and we verify it against the Web Platform Tests reftest corpus — the same suite the browser engines are held to. This gallery puts the raw test, our render, and the WPT reference next to each other so you can judge compliance visually, not just from a pass-rate number.

Each cell is one test: our render as a thumbnail, a pass/fail badge, and the pixel absolute-error (AE) against the reference. Click a cell to open the side-by-side detail view with the full renders and a link to the upstream test source. Filter by verdict, AE band, cluster, or test flag (ahem font, image, script).

scripts/build-wpt-gallery.php walks a --filter glob slice of the corpus and, per test:

  1. renders our output (test → PDF → PNG) through the real harness pipeline,
  2. locates and renders the WPT reference (sibling -ref.* or <link rel="match">), the same way the pass-rate harness does,
  3. scores the pair with ImageMagick (compare -metric AE, honoring the test’s <meta name="fuzzy"> tolerance),
  4. downscales thumbnails and records a manifest row.
Terminal window
# one cluster, reftests only, 30-test cap
php scripts/build-wpt-gallery.php --filter='css/css-backgrounds/background-*' \
--only-reftests --limit=30
# the full corpus, sharded across CI runners
php scripts/build-wpt-gallery.php --filter='css/**' --shard=3/16

Output lands in docs/generated/wpt-gallery/ (gitignored): a manifest.json plus img/<slug>.{ours,ref}.png full renders and .thumb.png thumbnails.

The renderer emits 1 CSS pixel as 1 PDF point, and never rounds a coordinate: ContentStream::num() formats every real at %.10f, so layout geometry reaches the page at full precision. That makes the rasteriser the pipeline’s only rounding site, and it settles two questions.

How big a device pixel is. A point is 1/72”, so Rasteriser renders at 72 dpi and one CSS pixel becomes exactly one device pixel — the geometry WPT reftests are authored against. Sampling denser would cancel for the reftest comparison itself, since both sides scale together, but it does not cancel for a fixture’s <meta name="fuzzy"> totalPixels budget: that is an absolute count of differing pixels, and at 96 dpi each one was being asked to cover (96/72)² = 1.78× the pixels its author allowed for.

Which device pixel an edge belongs to. Ghostscript expands path fills by half a device pixel before scan-converting them, so a re … f paints every pixel it touches, while shadings, image XObjects and glyphs are resolved at the pixel centre. An edge in the upper half of a pixel therefore lands in one column when a rectangle draws it and the next column right when a gradient, an image or a glyph draws it. A reftest reaches the same picture down two different paint paths by construction — a gradient with a hard stop against the solid blocks of its reference — so the disagreement surfaces as a whole-colour seam one pixel wide and fails an exact-match comparison. The rasteriser zeroes the fill adjustment (0 0 .setfilladjust2), so the whole pipeline rounds one way: pixel centre.

The cost is dropout protection. A fill thinner than a device pixel that clears every pixel centre now paints nothing. At one device pixel per CSS pixel anything a full CSS pixel wide or wider always covers exactly one centre whatever its sub-pixel phase, so the exposure is limited to explicitly sub-pixel rules.

{
"schema": 1,
"generatedAt": "2026-07-30T12:00:00+00:00",
"filter": "css/css-backgrounds/background-*",
"shard": "3/16",
"count": 30,
"summary": { "pass": 23, "fail": 7, "noRef": 0, "renderError": 0, "unknown": 0 },
"tests": [
{
"testId": "css/css-backgrounds/background-clip-content-box-001",
"source": "css/css-backgrounds/background-clip-content-box-001.html",
"assert": "…the <meta name=assert> text…",
"ours": "img/…ours.png",
"oursThumb": "img/…ours.thumb.png",
"reference": "img/…ref.png",
"referenceThumb": "img/…ref.thumb.png",
"referenceSource": "css/css-backgrounds/…-ref.html",
"engines": {},
"verdict": "pass",
"ae": { "reference": 0.0011 },
"flags": ["image"]
}
]
}

The engines map is reserved for the cross-browser oracle (Blink / Gecko / WebKit print-to-PDF), which the detail view will surface as extra columns in a later phase.