A Software Rasterizer: Display List to Pixels
Building a Web Browser Engine: Layout & Rendering
Chapter 8 · A Software Rasterizer: Display List to Pixels
Chapter 7 produced a real, ordered list of paint commands. This chapter executes it — a real pixel buffer, filled one command at a time, that can be checked pixel by pixel and written out as an actual, viewable image file. This is the literal end of the pipeline this entire two-course project has been building toward.
A Canvas, a Color Table, and the Naive First Attempt
A real, legitimate concern going in: rects computed by earlier chapters routinely have negative x/y — a margin box's own expand_rect_by (Chapter 2) subtracts edge.left/edge.top, and that can go negative. What actually happens when fill_rect_naive meets one turns out to be worse than a simple crash.
Two Different Failure Modes, One Root Cause
fill_rect_naive(canvas, Rect(-2, 0, 4, 1), red). Row 0 afterward: columns 0 and 1 are correctly red — the part of the rect genuinely on-canvas. But columns 8 and 9 are ALSO red. canvas.pixels[0][-2] and [0][-1] are completely valid Python indexing — negative indices wrap to count from the end of the list — so the naive rasterizer silently painted the far right edge of the canvas, which the intended rect never touched at all. Not a crash: a real, silent, visually confusing corruption of pixels that have nothing to do with the rect that caused it.
fill_rect_naive(canvas, Rect(8, 8, 5, 5), blue). This one raises a real IndexError: list assignment index out of range the moment y reaches 10. The identical root cause — no bounds checking — produces two genuinely different failure modes depending on which direction the rect overruns: silent corruption on the negative side, a hard crash on the positive side.
x0/y0/x1/y1 against the canvas's own bounds before the fill loop ever starts means canvas.pixels is never indexed with a negative or out-of-range coordinate at all. The same Rect(-2, 0, 4, 1) now leaves columns 8 and 9 correctly untouched. The same Rect(8, 8, 5, 5) no longer crashes, and correctly paints only the pixels that genuinely sit on-canvas.
Rasterizing Chapter 7's Own Real Example, and Writing a Real Image File
(50, 25) — inside both boxes — comes out (0, 0, 255), genuine blue. Pixel (250, 25) — inside A only — comes out (255, 0, 0), genuine red. The real, physically-filled pixel buffer agrees exactly with the abstract simulation Chapter 7 used to verify paint order.
save_ppm() writes the canvas out as a real, valid, dependency-free image — the plain-text PPM (P3) format: a three-line header, then one R G B line per pixel. No image library needed, and the result is a genuinely viewable file.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Negative rect coordinates being a real, expected input, not an edge case | Chapter 2's own expand_rect_by — margin boxes routinely produce negative x/y, meaning this chapter's own bug wasn't hypothetical; it was guaranteed to be hit by real output from earlier chapters |
| Two different failure modes from one root cause | Chapter 3's own overflow bug and Chapter 5's own line-overflow bug — a recurring pattern across this whole course: skipping a bounds check doesn't fail loudly and uniformly, it fails differently depending on which direction the bound is violated |
| A real pixel buffer matching Chapter 7's own abstract simulation | The literal completion of this course's own stated four-stage pipeline from Chapter 1 — parse (Course 1) → style (Course 1) → layout → paint → this chapter — an actual image, not just a data structure claiming to represent one |
Hands-On Exercises
Call parse_color('cornflowerblue') — a real CSS color keyword, but one not present in this chapter's own simplified NAMED_COLORS table. Confirm it falls back to a sensible default rather than raising an error, and explain why a hand-built color table can never realistically cover every one of CSS's 140+ named colors.
Fill a 10×10 canvas with a rect that sits entirely outside the canvas's own bounds on all sides, e.g. Rect(20, 20, 5, 5). Confirm every single pixel on the canvas remains at its original background color, and explain why this doesn't require a special "rect is fully off-canvas" check anywhere in fill_rect's own code.
Reproduce this chapter's own two failure modes directly: call fill_rect_naive with Rect(-2, 0, 4, 1) on a 10×10 canvas and inspect row 0's own pixel values at columns 8 and 9; separately, call fill_rect_naive with Rect(8, 8, 5, 5) and confirm it raises IndexError. For each case, explain in your own words why Python's own indexing rules produce that SPECIFIC outcome (silent corruption vs. a crash) rather than the other way around.
Chapter 8 Quick Reference
- Canvas: a real 2D pixel buffer, plus a simplified named-color table with a black fallback for unrecognized names
- Real bug found and fixed: no bounds clipping produces TWO different failure modes from the same root cause — negative coordinates silently wrap and corrupt the opposite edge of the canvas; positive out-of-range coordinates genuinely crash with
IndexError - The fix: clip a rect's own bounds against the canvas's own bounds before the fill loop ever runs — one change resolves both failure modes
- Verified: the real, physically-rasterized pixel buffer matches Chapter 7's own abstract paint-order simulation exactly, on the same real example
- A real, dependency-free image file: the PPM (P3) format needs no library at all — a genuinely viewable image, written directly to disk
- Next chapter: Stacking, Paint Order & Overlapping Elements — the cases where DOM nesting order and real visual paint order genuinely diverge