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