Domotion

Text rendering

Path outlines, native <text>, and when each is the right call.

Text is the part of an HTML capture most likely to look subtly wrong if you treat it carelessly — fonts shape differently in different engines, sub-pixel positioning matters, and certain scripts (Arabic, Devanagari, complex Indic) require contextual shaping. Domotion handles all of this; this page explains how, and what you can choose to influence.

Path mode (default)

By default, Domotion converts every captured glyph into an SVG <path> using fontkit to extract outlines from the matching system font. Identical glyphs are emitted once into <defs> and referenced via <use href="#g-XX"> across the document, so text-heavy captures don't bloat linearly with character count.

This mode is the default for two reasons:

  1. Cross-browser consistency. The shapes are baked into the SVG; Firefox, Safari, and Chromium all paint the same thing. With native <text> you'd see slight differences from each engine's font hinting.
  2. Self-containment. Path mode embeds the visible glyphs directly. The SVG doesn't need an external font file, doesn't trigger a network request, and renders identically when emailed, pasted into a doc, or opened offline.

Trade-off: path data is bigger than text. A typical hero captures at ~25–60 KB in path mode versus ~10 KB if it were native text. For most marketing / docs use cases that's well worth it.

Per-character anchoring

The renderer pins each glyph at the exact viewport-relative x position that Chromium painted it (the xOffsets array on each text segment). It does not round to integer pixels — Chromium uses sub-pixel positioning, and rounding accumulates ~0.5 px of drift per character, which becomes visible across long lines of monospaced text.

For the rare case where xOffsets isn't available (input values, certain pseudo-element content), the renderer falls back to fontkit advance widths from the run start.

Run-based shaping for fallback fonts

When a captured line mixes scripts ("Hello مرحبا"), Chromium uses different fonts for each script. Domotion detects fallback runs and shapes each contiguous fallback run as a unit using font.layout(runText) — which means Arabic initial / medial / final forms, Devanagari ligature clusters, and Thai mark-on-base positioning all survive the conversion to SVG paths.

Bidirectional text (mixed LTR / RTL) is handled by bidi-js, which performs paired-bracket mirroring on RTL embedding levels before the text is shipped to the path renderer.

Decoration metrics

Underlines and strike-throughs are positioned and sized using the font's own typographic tables — post.underlinePosition, post.underlineThickness, and the OS/2.yStrikeoutPosition / yStrikeoutSize entries — rather than as a fraction of font size. This matches what Chromium consults, so decorations land in the same place in the SVG as in the original render.

What gets rasterized

Path mode covers everything that can be expressed as font outlines. Anything else falls back to a screenshot of that region:

  • Color-bitmap glyphs (emoji on macOS, certain symbol fonts). Each unique glyph is screenshotted once and embedded as a base64 PNG, then deduplicated for repeats.
  • Color emoji within a text run. The line still shapes as paths; the emoji glyph gets a per-character <image> overlay at the captured position.
  • Pseudos with bitmap-only content. If the entire ::before/::after string is bitmap glyphs, the whole pseudo region is one screenshot.

This is invisible from the API surface — you just get a slightly larger SVG.

Test-mode flags (developers only)

Domotion's test runners expose a --mode flag with three values: css (native <text>), path (the default described above), and font (embed a subsetted woff2). These exist for visual-regression diffing across rendering strategies, not as user-facing configuration. The public elementTreeToSvg() entry point always emits path-mode output today.

If you have a use case where you want native <text> output (smaller files, accepting cross-engine font differences) or embedded-font output (smaller than path, identical across engines but larger than CSS), please file an issue. The internal renderer supports both modes; exposing them is a question of API design, not implementation.

Next

Animation model explains how multiple captured frames are stitched into one animated SVG.