8.0 KiB
epiphany-render-svg — decisions and Pass 12 candidates
This file records (a) the QUICKSTART decisions Agent I made for the renderer,
and (b) ambiguities batched as Pass 12 candidates (spec/PASS12_BATCH.md)
rather than improvised in code.
Scope and status
epiphany-render-svg is the SVG renderer behind the Chapter 7 RenderIR
interface: it turns a ResolvedLayoutIR into well-formed SVG 1.1, drawing
glyphs from genuine Bravura SMuFL data either as inline outline <path>s
(GlyphMode::PathOutline) or as <text> set in an embedded subset font
(GlyphMode::EmbeddedFont). Per the QUICKSTART development pattern it was
golden-locked against the stub solver's output first, then against the real
epiphany-engrave solver once the score→real-notation pass and re-spacing
landed. The renderer consumes any solver's ResolvedLayoutIR; it proves
renderer faithfulness — resolved geometry preserved, provenance traced,
output XML-valid and deterministic — independent of engraving quality.
The non-overreach rule (Chapter 7 / QUICKSTART, Agent I)
The renderer makes SVG-encoding choices only and no engraving-semantic
choices. In the default archival mode every emitted element traces to a
ResolvedGlyph (and thus a score-graph source, via data-prov/data-source-kind)
or to a declared renderer wrapper (the <svg> root, the metadata comment, the
y-flip <g>, a per-layer <g>). Traces can be turned off
(RenderOptions::emit_provenance = false) for a smaller display-only SVG; that
is an explicit mode the metadata comment declares (archival → "carries a
data-prov trace", display-only → "provenance traces suppressed"), so a trace-free
SVG — including the empty canvas — announces itself rather than passing as
archival. A glyph with no bundled outline is surfaced as a diagnostic and drawn
as a visible bounding-box fallback <rect> — never silently dropped and never
invented. The acceptance harness (archival mode) asserts one drawn element per
glyph and one provenance trace per drawn element.
Implementation decisions (QUICKSTART "Decisions you'll need to make")
- Spelling / 2. Solver architecture — N/A here (Agent H;
epiphany-engrave). - Renderer SVG dialect — SVG 1.1 + inline presentation attributes. Maximum
portability for what is effectively a viewer/print artifact; no CSS, no SVG 2
features. (CSS styling can come later if needed.) Output validates under the
system
xmllint(libxml2) as well as the in-crate well-formedness checker. - Catalog / 5. Binary Format versioning — N/A (Agents K, J).
Local decisions
- Glyph rendering — two self-contained modes; inline outlines
(
GlyphMode::PathOutline) is the default and the verified reference. Path outlines make the SVG self-contained (it renders in any browser, image tool, or print pipeline with no font installed) and are byte-golden-locked, at the cost of file size — the QUICKSTART's recommendation.GlyphMode::EmbeddedFontinstead references each glyph by SMuFL codepoint with a<text>element drawn from an@font-face-embedded Bravura subset (only the ~33 named glyphs), so the SVG is still self-contained (the font travels in it) and the text is selectable, at a larger file size. The two modes anchor glyphs at the same origin (em = 4 staff spaces), so placement is consistent by construction; the embedded mode is structurally tested rather than byte-golden-locked, and exact rasterisation is the consumer's font renderer's, so path mode remains the pixel-verified one. - Embedded-font subset — generated, not a vendored binary. The subset is a
deterministic base64 OTF emitted into
src/font_subset_generated.rsbytools/extract_bravura_outlines.py --font-out, keeping the "only generated artifacts committed" rule (no font binary is vendored). It retains the font's OFL copyright/license name records (belt-and-suspenders withtools/OFL.txt). Caveat: unlike the geometry-only outlines, the binary subset's exact bytes depend on the fontTools version, which the generated header records. - Outline source — the official OFL
Bravura.otf, extracted reproducibly.tools/extract_bravura_outlines.pyfetches the font + SMuFLglyphnames.jsonand emitssrc/outlines_generated.rs. The font is not vendored; only the generated Rust is committed (tools/OFL.txtcarries the SIL Open Font License 1.1 under which the outlines are redistributed). Exactly the glyph set the v0 pipeline can name (layout-ir'sBRAVURA_METRICS) is bundled; a test asserts every pipeline glyph has an outline (so the table cannot silently fall behind). - Coordinate system — staff spaces, y-up, one global flip. Outlines are
extracted in staff-space units (SMuFL em = 4 staff spaces, Bravura
unitsPerEm = 1000 ⇒ 1 staff space = 250 font units), y-up (musical
convention). The whole document is wrapped in one
translate(-min_x, max_y) scale(1, -1)group, so every glyph is placed with a plaintranslate(x, y)and theviewBoxis0 0 W Hin staff spaces;width/heightcarry the px display scale. The renderer never bakes the flip into per-glyph data. - Determinism — fixed 4-decimal number formatting,
-0normalised to0. Outlineddata is fixed bundled text; every computed coordinate is formatted identically, so identical input yields byte-identical SVG. The acceptance harness golden-locks the full SVG and a machine snapshot (object/glyph/path/ provenance/layer/per-class/hard-constraint counts + XML validity). - No external XML dependency. The workspace is deliberately dependency-light;
xml::check_well_formedis a hand-rolled validator for the subset the renderer emits, and the acceptance test additionally cross-checks withxmllintwhen present so the "XML-validates" claim rests on a real parser too.
Pass 12 candidates
See spec/PASS12_BATCH.md (rows P12-I1, P12-I2, P12-I3). Most relevant here:
- P12-I1 (resolved by I-1/I-3) — the original stub-only renderer output was a structural placeholder, so the human-review visual-acceptance gate ("the SVG visually parses as standard music notation") was deferred until real engraving landed. The real notation pass and real-Engraver goldens now close that gate; the stub path remains locked as an interface/reference mode, not the visual deliverable.
- P12-I2 (resolved) — the stable layout-object id derivation (
MUSCLOID, Pass-11 item 2.6) is wired:epiphany-determinismreserves the built-inMUSCLOIDtag andlayout-irprovenance routes through it. The renderer traces provenance by the (nowMUSCLOID-tagged)stable_id; only thedata-provhex in the goldens changed (the ids are non-canonical).
Repeat glyphs bundled + repeat glyph class (E1, 2026-07-07)
The E1 repeat tranche added repeatLeft, repeatRight, repeatRightLeft,
and repeatDots to the extractor's NAMES set; outlines, companion
layout-ir metrics, and the embedded font subset were regenerated from the
same SHA-pinned bravura-1.392 (fontTools 4.63.0 preserved, so the subset
diff is content-only). GlyphClass gained a Repeat class (token
repeat) so snapshots and data-class attributes separate repeat signs
from plain barlines. Volta ending numerals arrive as timeSig digit glyphs
(there is still no free-text primitive — unchanged).
Curve primitive → stroked <path>, GlyphClass unaffected (E2, 2026-07-08)
A resolved Curve (slur) emits a stroked, unfilled cubic-bézier <path d="M.. C.." with data-kind="curve" and a curve_provenance_attrs trace — drawn
after strokes and before glyphs (a slur sits over the staff lines, under the
noteheads it joins), in its own layer-grouped loop parallel to the stroke
loop. content_bounds grows by each curve's control-point hull ± half-thickness
(a cubic's ink never bows past its hull). RenderStats gained curve_count;
the acceptance snapshot prints it and the provenance_count == glyph + stroke
invariant became + curve. GlyphClass is untouched — curves are not glyphs,
so they carry no data-class.