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

class Canvas: def __init__(self, width, height, background=(255, 255, 255)): self.width = width self.height = height self.pixels = [[background for _ in range(width)] for _ in range(height)] NAMED_COLORS = { 'red': (255,0,0), 'blue': (0,0,255), 'navy': (0,0,128), ... } DEFAULT_COLOR = (0, 0, 0) def parse_color(name): return NAMED_COLORS.get(name, DEFAULT_COLOR) def fill_rect_naive(canvas, rect, color): x0, y0 = int(rect.x), int(rect.y) x1, y1 = int(rect.x + rect.width), int(rect.y + rect.height) for y in range(y0, y1): for x in range(x0, x1): canvas.pixels[y][x] = color # no bounds checking at all

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

Verified directly — a rect extending off the LEFT edge silently corrupts the RIGHT edge instead
A 10×10 canvas, white. 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.
Verified directly — a rect extending past the BOTTOM or RIGHT edge genuinely crashes instead
The same 10×10 canvas, 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.
def fill_rect(canvas, rect, color): x0 = max(0, int(rect.x)) y0 = max(0, int(rect.y)) x1 = min(canvas.width, int(rect.x + rect.width)) y1 = min(canvas.height, int(rect.y + rect.height)) for y in range(y0, y1): for x in range(x0, x1): canvas.pixels[y][x] = color
Verified directly — one fix resolves both failure modes at once
Clipping 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

Verified directly — the actual pixel buffer matches Chapter 7's own color_at_point() simulation exactly
Chapter 7's own real red-A-containing-blue-B layout, rasterized onto a real 300×50 canvas: pixel (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 findingWhat it connects to
Negative rect coordinates being a real, expected input, not an edge caseChapter 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 causeChapter 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 simulationThe 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

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

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