Skip to content

Painting and text

codingncaffeine edited this page Oct 5, 2026 · 13 revisions

Painting and text

The painter

An RGBA software canvas with source-over compositing. The painter walks the fragment tree in CSS 2.1's order: a box's background and borders, its in-flow children, its floats, then its text; positioned descendants by z-index at their stacking context's level. Solid borders, text decorations (underline, line-through), a clip rectangle that every write honors (overflow clipping, per axis — an axis left visible beside a clip one is not clipped), the scrollbars of the boxes that scroll (The shell), and the built-in look of form controls.

An inline box joins that order like any other when it needs a layer of its own — a z-index, a transparency, a transform, or position: sticky. Its fragment is otherwise only the paint behind its line: the words are in the block's own runs, and flagging the box without them would draw its background over the very words it belongs to. So at the end of each line every run and every box the line left behind is claimed by the innermost inline box that holds it and needs a layer, the boxes walked innermost first so that a nested one is packed before the box around it and travels inside it. A relative offset alone does not make a layer — the line's content already shifts in place, and leaving those boxes in the flow keeps the paint order a page expects.

Group transparency (css-color-4 §12): a box whose opacity is below one paints as a group. Everything inside it composites at full strength against the rest of itself first, and the whole is then laid over the page at that alpha — fading each piece as it is drawn is a different picture, because where two of them overlap the one underneath would show through the one above. The group is painted onto a copy of the backdrop it stands on and each pixel moved back over it by the alpha, which for content that composites only source-over — all of ours — is exactly the group's colour there, and reaches it without a premultiplied surface: a pixel the group never touched moves between two equal values and does not change. The buffer is the size of the group's own ink, held to whatever clip is in force. An inline box the flow broke around a block child fades that block with it.

Rounded corners (border-radius): each corner is a quarter ellipse with its own horizontal and vertical radius, from the shorthand's one-to-four values on each side of the slash or from the four longhands; percentages are of the border box's width and height, and radii that would overrun an edge all shrink by one factor (css-backgrounds-3 §5.2). The background color and every image layer are held to the curve of the box background-clip names, a picture is trimmed to the content edge's curve, and a box that clips its overflow in both axes clips what it holds along the padding box's curve. The borders are drawn as the ring between the border box's curve and the padding box's, each pixel taking the color of the side whose edge is nearest measured in that side's own width — which puts a seam between two colors on the corner's diagonal. A table in the collapsing border model keeps square corners, as the specification requires. The curves come out of a square root of an exactly-rounded quotient and are antialiased from sixteen samples a row, so the same pixels are drawn on every operating system.

Shadows (css-backgrounds-3 §6.1, css-text-decor-4 §7): box-shadow and text-shadow, each a list drawn from the last up so that the first is on top. An outer box shadow is the border box's shape moved by its offset and grown by its spread — a corner's radius grows by the spread, less for a small corner on a rectangular box so that a nearly sharp corner stays sharp, and fully for a nearly elliptical one (the specification's outset-adjusted radius) — blurred, and drawn under the background and only outside the border box. An inset shadow shades the padding box but for a hole its shape cuts, the hole's edge blurred, over the background and under the borders. A text shadow is the run and its decoration drawn apart in the shadow's place, blurred, and laid under the text, on horizontal and vertical lines alike. The blur is a Gaussian of half the blur radius, approximated by three box blurs each way as SVG's feGaussianBlur is, the same one the canvas's shadows use. A shadow written without a color, or with currentcolor, takes the color of the element it falls on — an inherited text shadow included.

A from-scratch PNG encoder writes the pictures: its zlib container and its fixed-Huffman deflate with LZ77 are written in the repository, and the unit tests re-decode the output through an independent inflate written for the test.

Backgrounds (css-backgrounds-3): the color under the border box (or the box background-clip names), then the background-image layers from the last up — a picture by url() fetched once and decoded, or a linear-gradient() (an angle, or to a side or corner) or radial-gradient() (circle or ellipse, the four extents, at a position), repeating or not, with color stops at written or evenly spread positions — each sized by background-size (cover, contain, lengths, auto keeping a picture's ratio) against its background-origin box, placed by background-position, tiled by background-repeat (repeat; space fits as many whole tiles as it can and shares the room left over between them so the first and last touch the edges; round stretches the tile until a whole number fills the area, and an auto side follows so a picture keeps its shape), and clipped to its background-clip box; the background shorthand with its layers. The root element's whole background becomes the canvas's — its pictures cover the surface, not just the box — and when the root has none at all the body's goes there instead, laid out as if it had been written on the root. Not yet: background-attachment, background-clip: text.

Text

Text is drawn in your installed fonts, through three pieces written here:

Piece What it does
TrueType reader TTF, OTF and TTC files: head, maxp, hhea, hmtx, loca, glyf with composite glyphs, cmap formats 0, 4, 6 and 12, name, OS/2, and the kerning — the pair positioning of every GPOS lookup the kern feature names (by pairs or by classes, through extension lookups too), or the kern table's horizontal pairs when GPOS names none; and colour — a glyph's picture from a bitmap strike (CBDT under its CBLC index, the PNG image formats 17, 18 and 19 through index formats 1 to 5; sbix's PNG graphics) or its layers (COLR version 0 over CPAL's first palette). Every offset is bounds-checked, and the reader is fuzzed.
CFF reader The CFF table of CFF-flavored OpenType — the gsfonts on a Linux desktop, Noto's CJK families, most of macOS: the Type 2 charstring interpreter whole (the path operators, the flex operators, hint masks, local and global subroutines, the arithmetic operators, endchar's accent composition through the charset and the Standard Encoding), CID-keyed fonts through their FDArray and FDSelect, the font matrix. A charstring's cubic curves are rewritten as the quadratic form the rasterizer draws, split until each piece is within a unit. Bounded in points and instructions per glyph; fuzzed with the reader.
Rasterizer Outlines flattened in 26.6 fixed point, nonzero winding, 4×4 grayscale coverage, integer arithmetic only — no libm in the raster path. Synthesized bold and oblique. A small-size contrast curve lifts partial coverage below 14 px. No hinting. Each subsample row tests only the edges that cross it, and the scan's work is counted before it starts: an outline that would cost more than a glyph eight ems square at the size drawn draws nothing, so a hostile font's box cannot turn a glyph at text size into millions of pixels. Fuzzed with the reader.
Font manager Catalogues the operating system's font directories by their naming tables alone (hundreds of faces in tens of milliseconds), matches CSS font-family, generic families, weight and slant, and falls back by coverage — by OS/2 Unicode-range bits, then by cmap — when a page's font lacks a character. Page fonts from @font-face answer first.

Every pair of glyphs the same face draws side by side is kerned as the font's tables say, when text is measured and again when it is painted, so a run's measured width and its drawn width stay one number; font-kerning: none leaves every pair at its advances. A pair that spans two faces, or a line's two pieces, is not kerned, and where a line ends is settled before kerning is (see Layout).

A colour font draws its glyphs as pictures or as layers. A picture is decoded once per size from the strike nearest that size and drawn scaled from the strike's ppem to the text's, set where the strike's metrics put it; a layered glyph is each of its layers' outlines filled with its palette colour, a layer that asks for the foreground taking the text's own. Neither is emboldened or slanted. A character drawn as a picture by default — one with Unicode's Emoji_Presentation, a smiling face or a rocket — goes to a colour face when the machine has one for it, before any face on the stack with a black-and-white glyph, the way Firefox and Chrome choose; a text-presentation character such as ☺ stays with the text face and its colour.

The last fallback is Sashfold Mono, an original monospace face authored as stroke tables in the repository (ASCII, Latin-1 symbols, composed diacritics, arrows, the chrome's glyphs), which draws an honest box for anything no font on the machine has. It can be written out as a TrueType file (gen_font), and a test holds the outline rasterizer to reproducing the stroke rasterizer's pixels exactly.

Determinism

The reference tests render with the built-in face alone, so renders are byte-identical across compilers and operating systems — enforced in CI by PNG goldens compared byte for byte on Linux, Windows and macOS. See Testing.

Images

Format Decoder
PNG Every color type, bit depth and filter, the interlaced layout (Adam7), PLTE and tRNS, CRC-checked critical chunks
GIF LZW, tables, the transparent index, interlace; the first frame of an animation
JPEG Baseline and progressive (spectral selection and successive approximation), an integer inverse DCT, chroma subsampling with bilinear upsampling, restart markers
BMP The core header and the info headers through V5, 1 to 32 bits per pixel, palettes, the bit-field masks, RLE4 and RLE8, bottom-up and top-down
ICO A directory of BMP-style entries with the AND mask, or PNG entries; the entry nearest the size wanted — the tab's icon size for a page's favicon
SVG The subset below, drawn at the picture's declared size — a small one at up to four times it, reported as its density, so it keeps its edges when shown large

Each decoder is written in the repository and fuzzed. Pictures are checked against the operating system's own codecs as a world check: GIF pixel-identical, JPEG within two or three levels per channel.

SVG

src/svg renders an <svg> element's content through a rasterizer of its own — paths flattened to polygons and scan-converted at 4×4 samples per pixel by the nonzero or even-odd rule, strokes built as the polygons of their outline with butt, round and square caps, miter, round and bevel joins, and dashes. What it draws:

  • The <path> data grammar whole (every command, relative and absolute, the smooth curves, arcs as cubics), <rect> with rounded corners, <circle>, <ellipse>, <line>, <polyline>, <polygon>.
  • <g>, the transform attribute (matrix, translate, scale, rotate about a point, skew), nested <svg> viewports and <symbol> through <use>, each fitted by its viewBox and preserveAspectRatio and clipped to its viewport; <defs> never drawn directly.
  • fill and stroke as none, a color, currentColor or a url(#id) with a fallback; fill-opacity, stroke-opacity, opacity, fill-rule, stroke-width (percentages of the viewport's diagonal), the cap, join, miter limit and dash properties — from the cascade, so a page's stylesheet styles an inline picture and an SVG's own <style> is a stylesheet of the document, with every presentation attribute an author declaration below them.
  • Linear and radial gradients with their stops, gradientUnits, gradientTransform, spreadMethod and href inheritance; clipPath by the union of its shapes, in user or bounding-box units.
  • <text> and <tspan> in the page's font stack at the transform's scale, from x/y/dx/dy and text-anchor, drawn level whatever the transform turns; <image> from a data: URL, fitted like a viewport.

Inline in a page, an <svg> is a replaced box (Layout) whose picture is drawn at the box's size, so it is as sharp as the page. Not in the subset yet: markers, patterns, masks, filters, foreignObject, text on a path, animation, and a group's opacity composited as a group rather than per shape — see Not written yet.

Choosing and placing pictures

  • srcset, sizes and <picture> per the standard: densities from widths over the slot size, the smallest candidate at or above 1× else the largest, <source> elements taken in order by media condition and by type — a type the engine cannot decode (WebP, AVIF) is passed over for the fallback.
  • A chosen picture's density scales its intrinsic size, so a 400-pixel picture chosen for a 200 px slot lays out 200 px wide.
  • Pictures are fetched through the page's session (at most 64 per page, 8 MB each) and drawn scaled with integer arithmetic; a missing picture reserves its box, an unsized one shows its alt text.

Seeing what the rasterizer does

sashfold --font-sampler sheet.png                 # Sashfold Mono
sashfold --font-sampler sheet.png --font Arial.ttf
sashfold --font-info some-font.ttc
sashfold --font-list

Clone this wiki locally