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:
- 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. - 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/::afterstring 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.