---
enabled: true
touches: [crates/layout/Cargo.toml, crates/layout/src/bin/paint.rs, crates/dom/src/computed.rs, roadmap.md]
depends-on: []
---
# Visual proof: rasterize the fragment tree to a PNG
`roadmap.md` → "Visual proof": "nothing in the engine has ever drawn a pixel" — every
measurement so far is a JSON diff of geometry (`getBoundingClientRect`) and computed style
against the three-browser oracle, never against pixels. The goal it states: "take the
fragment tree `layout()` already produces and turn it into a picture on screen (or, at
minimum, in a saved image) — a blank page first, then a box with a background color and a
border... This is demo-ware, explicitly not production: no text, no images, no
interactivity, no compositing beyond flat rectangles, no accessibility tree, no input
handling. One box rendered correctly is success." The same section notes "A software
rasterizer writing a PNG (e.g. via the `image` crate, walking border-box rects to filled
rectangles) is very likely simpler and faster to demo than wiring up `wgpu`/`vello` plus a
window — but the exact mechanism is an implementation decision for the track that builds
this, not fixed here." This spec makes that decision: a software rasterizer via the `image`
crate, no window.
Scope. In: two deliverables, each a PNG file at a fixed path under `target/visual-proof/`
(`target/` is already gitignored build output, not a committed asset). "Blank page" is an
800×600 canvas (`fragment_check.rs`'s own `VIEWPORT_WIDTH` and the dump format's
`Viewport { width: 800, height: 600 }`) filled with one background color and nothing else,
produced by running the real pipeline (`Document::parse` → `BoxTree::build` → `layout()`)
on an empty document and confirming there is nothing to paint, not by skipping the pipeline
and writing a blank buffer directly. "A box" is a single `
` with an explicit
`background-color` and a `border`, laid out and painted at its actual fragment rect: the
properties painted are exactly `background-color` and the four `border-*-color`/
`border-*-width` values, nothing else. Out: this is not a generated seam. No model batch,
no candidate-admission loop, no corpus of its own, no provenance record — hand-written
engineering directly against the existing substrate, the same shape as
`specs/device-pixel-rounding.md`'s disclaimer for `fragment.rs`; a future reader must not
assume the batch-loop machinery this repo uses for queued CSS seams is in play here. Also
out: an on-screen window — the roadmap's own phrasing ("on screen (or, at minimum, in a
saved image)") makes a saved image sufficient, and a window pulls in an entire second
category of complexity (`winit`, an event loop) the demo-ware framing does not require; a
saved PNG is the actual deliverable this spec builds, and no future reader should treat a
window as still owed. Also out: text metrics, images, `border-radius`, shadows, transforms,
any corner rounding or overlap logic beyond two nested flat rectangles, and reusing
`crates/harness/src/cases.rs`'s recipe corpus for color — that corpus carries no color
today (confirmed: no recipe sets `background`/`border-color`, and its own `RESET` rule
zeroes `border: 0`), and coloring it is its own scope creep this track does not take on.
Build:
- `crates/layout/Cargo.toml` — add `image` (latest 0.25-series) as a dependency, the
workspace's first. No other crate references a raster or PNG-writing crate today.
- `crates/dom/src/computed.rs` — four new accessors, `background_color` and
`border_{top,right,bottom,left}_color`, mirroring `border_top_width`'s existing pattern
(`style.clone_border_top_width()` gated on `style.clone_border_top_style()
.none_or_hidden()`, lines 381–401): read `style.clone_background_color()`/
`style.clone_border_*_color()`, resolve to an RGBA quad, and zero out (fully transparent)
a border side whose style is `none`/`hidden`, matching the CSSOM rule
`comparable_style`'s existing border-width entries already apply. `computed.rs`'s own doc
comment says color accessors are net-new to this module (there are currently zero); this
is the one place the exact Stylo 0.21.0 API name for the computed color type needs
confirming against the crate directly, since no vendored source was available to check
here — the shape (a `clone_*` accessor returning a Stylo computed-color type with an RGBA
resolution path) is not in doubt, only the exact type/method name.
- `crates/layout/src/bin/paint.rs` — new binary, mirroring `fragment_check.rs`'s/
`style_check.rs`'s shape (a `main()` with `--case`/`--out` args, no shared library module
needed given the scope). Pipeline: `Document::parse(html)` → `BoxTree::build(&document)`
→ `fragment::layout(&document, &tree, 800.0)`, exactly `fragment_check.rs`'s
`engine_dump()`'s first four lines, reused verbatim rather than inventing a second entry
point. Two literal HTML documents, hand-built in this file (not `cases.rs`):
- `blank`: ``.
- `box`: a single reset rule (`* { margin:0; padding:0; box-sizing:border-box; }`) plus
one `
`. `box-sizing:border-box` makes the fragment's border-box exactly
200×100, so the expected paint rect is the literal declared size, not something that
needs computing from content-box + border independently.
Rasterization: an `image::RgbImage` (or `RgbaImage`) 800×600, filled with a fixed
background color (white), then `Fragment::walk_absolute(0.0, 0.0, f)` walks every
fragment's absolute rect in the same order the tree was built (parent before children —
correct paint order for non-overlapping boxes, sufficient given "no compositing beyond
flat rectangles"). For each `(node, x, y, w, h)`: look up `background_color`/
`border_*_color`/`border_*_width` via the new `computed.rs` accessors on that `node`;
if any border side has nonzero width and non-transparent color, fill the full border-box
rect `(x, y, w, h)` with that side's color first (uniform-border case: one rect for all
four sides, since the milestone's test document sets one shorthand and all four sides
match); then fill the inner rect — `(x + border_left, y + border_top, w - border_left -
border_right, h - border_top - border_bottom)` — with `background-color`, on top. A fully
transparent `background-color` (CSS initial value) leaves the canvas's existing pixels
untouched rather than painting white over what a parent already painted. Write the
canvas to `target/visual-proof/{blank,box}.png` via `image`'s PNG encoder.
Gates:
- `cargo test --workspace` passes.
- `cargo run --release -p layout --bin paint -- --case blank` exits 0 and produces
`target/visual-proof/blank.png`; a test reads it back with `image::open`, asserting
800×600 dimensions and that every pixel equals the fixed background color — this is what
makes "produces a valid image" checkable rather than eyeballed.
- `cargo run --release -p layout --bin paint -- --case box` exits 0 and produces
`target/visual-proof/box.png`; a test reads it back and asserts, at pixel coordinates
derived from the same fragment rect the binary painted from (not a hand-computed or
independently-guessed coordinate): the interior point (rect center) equals red
`(255, 0, 0)`, and a point inside the border band (e.g. rect origin + 2px in both axes,
inside the 10px border) equals blue `(0, 0, 255)`.
- No requirement to move or re-check `fragment_check --random`'s 216/227 floor: this track
reads `Fragment`'s existing `node`/`offset_x`/`offset_y`/`width`/`height` fields through
`walk_absolute`, which already exists, and adds no field and no changed computation to
`fragment.rs` itself — paint properties are read independently through new `computed.rs`
accessors, not through the fragment tree. The floor is unaffected and this spec does not
re-run `fragment_check` as a gate.
Afterwards, update `roadmap.md` → "Visual proof" with what landed: a software rasterizer
via the `image` crate (`crates/layout/src/bin/paint.rs`), producing saved PNGs rather than
an on-screen window, and the blank-page and one-box results, the way
`specs/floats-bfc.md`'s own closing paragraph updates its roadmap section.