Work in progress — this reference is being written in the open. Unfinished pages are excluded from search engines.
Paged · IDML Reference
The renderer

The display list

The display list is Paged's intermediate representation — a flat, versioned stream of DisplayCommands that the compose stage emits and a rasterizer consumes, decoupling what to paint from how it gets painted.

Pro· reference

The display list is the renderer's intermediate representation: a flat, self-contained command stream that says what to paint, with no idea how it will be painted.

In short: Between laying out a page and turning it into pixels, the renderer emits a DisplayList — a PathBuffer of shared path data plus a flat Vec of DisplayCommands like FillPath, StrokePath, Image, and PushClip. The compose stage produces it; a rasterizer (WebGPU/Vello or CPU) consumes it. Because the list is small, versioned, and backend-agnostic, the same one drives a print-quality CPU render and a live WebGPU preview, and it doubles as a stable hand-off format for tooling. This page is the reference for what's in it and why it exists.

Why an intermediate representation exists

It would be possible to walk the laid-out scene and call a graphics API directly — fill here, stroke there. The renderer deliberately doesn't. Instead, compose emits a display list, and only then does a backend turn that list into pixels. That extra hop buys three things:

  • One scene, many backends. The list is the seam between what to paint and how. The WebGPU/Vello backend and the CPU (tiny-skia) backend consume the exact same list, so the live preview and the CI fidelity render agree by construction. See rendering backends.
  • Cheap re-painting. A built page caches its display list. The interactive canvas can re-rasterize a page at a new zoom, or snapshot it at low resolution for a page navigator, without re-running parse, model, layout, and compose.
  • A stable hand-off for tooling. The list is versioned precisely so it can be treated as a durable intermediate representation, not just an internal buffer.

The shape of a DisplayList

A DisplayList is small and flat. The commands carry indices into side tables rather than embedding heavy data inline, which keeps each command Copy-cheap and lets repeated shapes share storage.

Attribute · DisplayListType / valuesSupportNotes
pathsPathBufferSupportedAll path geometry, deduplicated. Commands reference paths by PathId; glyph outlines are interned once per (font_id, glyph_id) and reused across the page.
commandsVec<DisplayCommand>SupportedThe ordered draw stream. The rasterizer walks it front-to-back; paint order is list order.
gradientsVec<LinearGradient>SupportedLinear-gradient pool; a Paint::LinearGradient holds a GradientId into it.
radial_gradientsVec<RadialGradient>SupportedRadial-gradient pool; Paint::RadialGradient indexes here.
imagesVec<DecodedImage>SupportedDecoded RGBA8 pixels, one entry per distinct source URI; a DisplayCommand::Image holds an ImageId into it.
spot_inksVec<SpotInk>SupportedNamed spot inks (e.g. a PANTONE swatch), interned by name so spot-on-same-spot overprints can be detected at raster time.

Geometry is interned, not inlined

The PathBuffer is the reason a page full of text doesn't bloat. It hands out a PathId per stored shape and dedupes via a cache key: the letter "e" in a given font and size is tessellated to a PathData once, interned under its GlyphCacheKey, and every later "e" reuses the same PathId. Coordinates live in a path-local space; a command's Transform places that path on the page.

PathSegment = MoveTo | LineTo | QuadTo | CubicTo | Close
PathData    = { segments: Vec<PathSegment> }
PathId      = opaque index into a DisplayList's PathBuffer (not stable across lists)
Transform   = 2×3 affine [a b c d tx ty]  →  x' = a·x + c·y + tx,  y' = b·x + d·y + ty

Clips and fills both use the non-zero fill rule, matching IDML's path-geometry convention.

Colour is linear, with CMYK preserved

A Paint is Copy and one of four kinds:

Attribute · PaintType / valuesSupportNotes
Solid(Color)linear-RGB rgbaSupportedThe common case. All compositing happens in linear light; sRGB gamma conversion is the backend's job.
LinearGradient(GradientId)index into gradientsSupportedResolved against DisplayList::gradients.
RadialGradient(GradientId)index into radial_gradientsSupportedSame id space, resolved against DisplayList::radial_gradients.
Cmyk { c,m,y,k, rgb, spot }native CMYK + cached RGBSupportedNative CMYK channels preserved end-to-end for overprint; rgb is the pre-resolved display colour so ordinary draws paint identically to a Solid. spot=Some(id) routes a named ink to its own plane.

The commands

A DisplayCommand is one drawing instruction. Every variant carries a Transform that maps its path (or image) from local space onto the page. The core set:

Attribute · DisplayCommand (paint)Type / valuesSupportNotes
FillPath{ path_id, paint, transform }SupportedFill a path. The single most common command — every glyph and frame fill is one.
StrokePath{ path_id, paint, stroke, transform }SupportedStroke a path; stroke width is in pt in document space (not a scaled derivation of the points).
Image{ image_id, transform }SupportedPlace a decoded RGBA8 image; the unit pixel grid maps to the page via transform.
FillPathBlend{ path_id, paint, transform, blend_mode }SupportedA fill composited with a non-Normal blend mode. Normal fills stay on the fast FillPath path.
FillPathOverprint / StrokePathOverprint{ path_id, paint, [stroke,] transform }Parsed, not yet renderedCMYK overprint composited as a per-channel darken — a visually-correct RGB approximation for the common dark-on-light cases, not yet a true per-channel CMYK composite.

State and grouping commands bracket ranges of draws:

Attribute · DisplayCommand (state / grouping)Type / valuesSupportNotes
PushClip / PopClip{ path_id, transform } / ( )SupportedMask subsequent draws to the intersection of pushed clips (non-zero, anti-aliased). Mismatched pops drop to the base state.
PushLayer / PopLayer{ bounds, effect, blend_mode, opacity } / ( )SupportedRender contained draws into an offscreen buffer, apply the layer effect, then composite back. The structural successor to BeginBlendGroup for effect-driven layers.
BeginBlendGroup / EndBlendGroup{ bounds, blend_mode, opacity } / ( )SupportedTransparency-group semantics: blend / partial opacity applied at the group composite, not per fill. Kept for the per-frame backdrop-bypass path.

Effect stamps render decorations relative to a path. By convention, behind-the-shape stamps (shadows, outer glow) are emitted before the fill; on-top stamps (inner shadow / glow, bevel, satin) after:

Attribute · DisplayCommand (effects)Type / valuesSupportNotes
DropShadow / PathShadow{ path_id, transform, shadow }SupportedOffset, blurred shadow behind a shape. PathShadow is the glyph/arbitrary-path flavour; DropShadow the rect-stamp flavour.
InnerShadow / InnerGlow{ path_id, transform, params }SupportedSoft stamp painted on the inside of the path, composited on top of the fill.
OuterGlow{ path_id, transform, params }SupportedSoft halo carved against the path exterior, behind the fill.
BevelEmboss{ path_id, transform, params }SupportedHeight-map-derived highlight/shadow tints on the path interior. Full fidelity on the CPU backend; stubbed on Vello (see backends).
Satin{ path_id, transform, params }SupportedTwo offset blurred stamps subtracted to a wave mask, tinted and composited on the interior.
Feather / DirectionalFeather / GradientFeather{ path_id, transform, params }SupportedSoft alpha falloff replacing the path's hard edge — uniform, per-edge, or gradient-driven. Emitted in place of the fill.

How the list is built — and read back

The compose stage appends commands as it walks the scene; helper emitters (emit_rect, emit_paragraph, emit_image_at, …) push the right command plus any interned path. Order matters: the rasterizer paints commands in list order, so paint order is list order.

A few accessors make the list usable as data, not just as a draw buffer. DisplayCommand::transform_mut returns the placement transform of any variant, which post-emit passes (such as vertical justification) use to translate a whole range of commands without inspecting each variant. The pool tables each have an indexed getter (gradient, image, spot_ink) so a consumer can resolve an id back to its data.

Frequently asked questions

Is the display list the same as a PDF or an SVG? No, though it sits at a similar level. It's an in-memory, Rust-native command stream purpose-built for this renderer's compose-to-raster seam: linear-RGB colour, interned glyph paths, native-CMYK paints, and effect stamps that map onto our rasterizers. It is not a file format you'd hand to another tool, though it's versioned so it could serve as a stable hand-off.

Why are PathIds described as "not stable across lists"? Because a PathId is just an index into one DisplayList's PathBuffer. Two different pages intern their own glyph outlines starting from zero, so the same PathId value means different shapes in different lists. Always resolve a PathId against the list that produced it.

Why is colour stored in linear RGB? Because compositing — blending, blurring, gradient interpolation — is only correct in linear light. The display list keeps everything linear and leaves the final linear-to-sRGB gamma conversion to the rasterizer, so both backends converge on the same result.

Do both backends support every command? Almost. The WebGPU/Vello backend covers the common set — fills, strokes, images, clips, blend groups — and approximates the soft effects. A couple of commands (BevelEmboss, and true per-channel CMYK overprint) are only fully realized on the CPU backend today. The rendering-backends page has the honest matrix.

On this page