본문으로 건너뛰기

SVG Text Import Model

Status: Active — describes the current import strategy.

Problem

SVG's <text> / <tspan> model is fundamentally different from Grida's text model. SVG text is a character-level positioning system with inline style runs, per-character coordinates, baseline shifts, and bidirectional layout. Grida's TextSpanNodeRec is a paragraph box — one string, one style, rendered by Skia's paragraph engine.

A lossless mapping is not possible without a rich-text / attributed-string model that Grida does not yet have.

Design Decision

One usvg TextChunk → one Grida TextSpanNodeRec. The SVG <text> element becomes a Grida GroupNodeRec that contains TextSpan children.

This is the same tradeoff Figma makes when importing SVG text: inline style variations within a single text run are flattened to the dominant style, and features like baseline-shift are dropped.

Why chunks, not spans

usvg parses <text> into chunks and spans:

  • A chunk is created at each absolute repositioning (x or y attribute on a <tspan>). Each chunk has a resolved (x, y) position and a single text string.
  • A span is a style run within a chunk (different fill, font-size, font-weight, etc.). Spans have start/end byte offsets into the chunk text but no position data — they flow inline from the chunk origin.

If we mapped each span to a TextSpan node, multiple spans within one chunk would all be placed at the same (x, y), causing them to overlap. We don't have per-span advance widths from the chunk/span API to compute inline offsets.

usvg does provide lower-level APIs (layouted(), flattened()) with per-glyph positions, but:

  • flattened() converts to path outlines — text is no longer editable.
  • layouted() provides per-glyph transforms but requires reimplementing glyph-level rendering, bypassing our paragraph engine.

Neither fits our goal of retaining editable text.

The mapping

SVG                         Grida Scene Graph
───────────────────────── ─────────────────────────────
<text> → GroupNodeRec (inherits text's transform)
chunk[0] (x, y) → TextSpanNodeRec (positioned at chunk x, y)
chunk[1] (x, y) → TextSpanNodeRec (positioned at chunk x, y)
... ...

Each TextSpan gets:

PropertySource
transformChunk's absolute (x, y) + text's relative transform from parent group
textChunk's full text string (all spans concatenated)
font_sizeFirst visible span's font_size
fillFirst visible span's fill paint
strokeFirst visible span's stroke paint
anchorChunk's text-anchor

What Works

SVG FeatureStatusNotes
Multiline via <tspan x="0" dy="1.2em">Each line becomes a separate chunk with correct y
Relative positioning (dx, dy)usvg folds dy into chunk y positions
Multiple <text> elementsEach is an independent Group
text-anchor (start/middle/end)Carried per chunk
Basic font properties (size, family)From first visible span
Fill / stroke colorsFrom first visible span

What Is Lost

SVG FeatureStatusRecovery path
Inline style variation within a line❌ LostRich text / attributed string model
Per-character x/y coordinate lists❌ LostPer-glyph positioning model
baseline-shift (super/subscript)❌ LostVertical offset per run
dx/dy staircase within a chunk⚠️ Partialusvg splits at absolute repos; relative-only offsets may be lost
font-weight/font-style variation❌ LostRich text model
letter-spacing, word-spacing❌ LostTextSpan style extension
text-decoration per span❌ LostPer-run decoration support
Nested <tspan> style nesting❌ FlattenedRich text model
Text on path (<textPath>)❌ Not supportedPath-text layout engine

Comparison with Figma

Figma makes a very similar choice when importing SVG:

  • Each <text> becomes a text node.
  • Per-chunk positioning is preserved.
  • Inline style runs within a single text chunk are flattened to one style.
  • baseline-shift (superscript/subscript) is dropped.
  • Per-character coordinate lists are dropped (text flows normally).

Our import produces the same level of fidelity.

Future: Rich Text Model

A future rich-text model would enable preserving inline style runs. The likely approach:

  • TextSpanNodeRec gains a Vec<StyledRange> or similar attributed-string representation.
  • Each range carries fill, font-size, font-weight, baseline-shift, etc.
  • The paragraph engine renders these as styled runs within a single paragraph layout.

This would recover most of the "Lost" items above. Per-character coordinate lists and text-on-path would still require dedicated support.

References

  • usvg text model: third_party/usvg/src/tree/text.rs
  • Text import code: crates/grida-canvas/src/svg/from_usvg_tree.rs (convert_text)
  • Text pack code: crates/grida-canvas/src/svg/pack.rs (append_text)
  • SVG text spec: https://www.w3.org/TR/SVG11/text.html