Agent I-4c: embedded @font-face glyph mode (a second self-contained renderer)
A render mode that references each glyph by its SMuFL codepoint via `<text>`,
drawn from an `@font-face`-embedded Bravura subset, alongside the default inline
`<path>` outlines. The SVG stays self-contained (the font travels in it) and the
text is selectable, at a larger file size.
- GlyphMode::EmbeddedFont: the SVG declares the font once via
<defs><style>@font-face{...}</style></defs>, then emits one
<text transform="translate(x y) scale(1 -1)" font-size="4" ...>&#xNNNN;</text>
per glyph. Geometry is consistent with PathOutline by construction: same
origin, a per-glyph counter-flip cancels the outer y-flip, and the SMuFL em is
four staff spaces. A new `text_count` stat; unbundled glyphs still fall through
to the visible bbox rect + diagnostic. The metadata comment declares which mode
produced the SVG (and, on the empty canvas too, via a shared `glyph_note`).
Path mode stays the byte-golden-locked, pixel-verified reference; the embedded
mode is structurally tested (well-formed XML — also under `xmllint`; one
@font-face; one <text>/codepoint per glyph; provenance preserved; determinism).
- The subset is a GENERATED artifact, not a vendored binary: a deterministic
base64 OTF emitted into src/font_subset_generated.rs by
`tools/extract_bravura_outlines.py --font-out` (the same SHA-pinned 1.392 font
the outlines come from). `recalcTimestamp=False` keeps the source font's fixed
head.modified so the bytes are reproducible across runs, not just within one.
- OFL compliance: the subset is a Modified Version, so its PRIMARY font name is
renamed off the Reserved Font Name "Bravura" to "EpiphanyBravuraSubset" in BOTH
naming structures an OTF carries — the SFNT `name` table AND the CFF (Name INDEX
+ top-dict FullName/FamilyName). The copyright/trademark/license records, which
name Bravura as attribution, are kept; the renderer references the renamed
family in @font-face and <text>. The generator reparses the saved bytes and
fails if the reserved name leaks into a primary record, and validates the cmap
covers every glyph.
- Machine-locked payload: the generator emits the decoded length and a BLAKE3-256
(the workspace's sole hash) of the font bytes; a render-svg test base64-decodes
the payload (no new runtime dep) and asserts the length, the OTTO signature, the
BLAKE3, and — parsing the SFNT name table and CFF Name INDEX — that neither
primary name is the reserved name. Adds an epiphany-determinism dev-dep.
The demo example gains `--glyph-mode=path|embedded`; lib/README/DECISIONS document
the two modes, the subset's OFL rename, and the regeneration command (--font-out +
the blake3 dependency). PathOutline goldens are byte-unchanged; the outlines stay
byte-identical. Full gate green: build, fmt, clippy, 585 tests, conformance scale 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
@ -125,6 +125,7 @@ name = "epiphany-render-svg"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"epiphany-core",
|
||||
"epiphany-determinism",
|
||||
"epiphany-engrave",
|
||||
"epiphany-layout-ir",
|
||||
"epiphany-testkit",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Agent I's SVG renderer behind the Epiphany RenderIR interface (spec Chapter 7): turns a ResolvedLayoutIR into well-formed SVG 1.1, drawing each glyph as a genuine Bravura SMuFL outline <path>. A renderer, not an engraver — it makes SVG-encoding choices only, never engraving-semantic ones."
|
||||
description = "Agent I's SVG renderer behind the Epiphany RenderIR interface (spec Chapter 7): turns a ResolvedLayoutIR into well-formed SVG 1.1 using genuine Bravura SMuFL path outlines or an embedded-font text mode. A renderer, not an engraver — it makes SVG-encoding choices only, never engraving-semantic ones."
|
||||
|
||||
[dependencies]
|
||||
# The renderer consumes the Chapter 7 ResolvedLayoutIR / RenderIR and the glyph
|
||||
|
|
@ -21,3 +21,5 @@ epiphany-engrave.workspace = true
|
|||
# their input from.
|
||||
epiphany-testkit.workspace = true
|
||||
epiphany-core.workspace = true
|
||||
# BLAKE3 (the workspace's sole hash) for the embedded-font content-integrity test.
|
||||
epiphany-determinism.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
# epiphany-render-svg — decisions and Pass 12 candidates
|
||||
|
||||
This file records (a) the Phase-2 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.
|
||||
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 phase status
|
||||
## Scope and status
|
||||
|
||||
`epiphany-render-svg` is one renderer behind the Chapter 7 `RenderIR` interface:
|
||||
it turns a `ResolvedLayoutIR` into well-formed **SVG 1.1**, drawing each glyph as
|
||||
a genuine Bravura SMuFL outline `<path>`. Per the QUICKSTART development pattern
|
||||
it is built and **golden-locked against the stub solver's output first**, before
|
||||
the real engraving solver and the score→real-notation engraving pass land. The
|
||||
stub returns the constrained IR's geometry verbatim — a structural projection,
|
||||
not yet real notation (each layout object becomes one arbitrary glyph in a row) —
|
||||
so this phase proves the renderer is *correct and faithful* (genuine outlines,
|
||||
provenance preserved, output XML-valid and deterministic), independent of
|
||||
engraving quality. The renderer already consumes any solver's `ResolvedLayoutIR`,
|
||||
so when the real `epiphany-engrave` solver lands, the visible result improves with
|
||||
no renderer change (the demo binary's `--solver=stub|real` flag exercises both).
|
||||
`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)
|
||||
|
||||
|
|
@ -46,13 +44,25 @@ glyph and one provenance trace per drawn element.
|
|||
|
||||
### Local decisions
|
||||
|
||||
- **Glyph rendering — inline genuine Bravura outline `<path>`s
|
||||
(`GlyphMode::PathOutline`), the default and only mode this phase.** The
|
||||
QUICKSTART's recommendation: path outlines make the SVG self-contained (it
|
||||
renders in any browser, image tool, or print pipeline with no font installed),
|
||||
at the cost of file size. An embedded-`@font-face` mode is a documented future
|
||||
option, intentionally **not stubbed** so the interface does not lie about a
|
||||
capability that is absent.
|
||||
- **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::EmbeddedFont` instead
|
||||
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.rs` by
|
||||
`tools/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 with `tools/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.py` fetches the font + SMuFL `glyphnames.json`
|
||||
and emits `src/outlines_generated.rs`. The font is **not vendored**; only the
|
||||
|
|
@ -82,12 +92,12 @@ glyph and one provenance trace per drawn element.
|
|||
|
||||
See `spec/PASS12_BATCH.md` (rows P12-I1, P12-I2, P12-I3). Most relevant here:
|
||||
|
||||
- **P12-I1** — the constrained IR is a structural placeholder, so the rendered
|
||||
stub output is *not yet recognizable notation*. The QUICKSTART's human-review
|
||||
visual-acceptance gate ("the SVG visually parses as standard music notation")
|
||||
is therefore a **next-phase** gate, met once real engraving lands; this phase's
|
||||
gate is renderer correctness/faithfulness. Recorded so the visual gate is not
|
||||
mistaken for already-met.
|
||||
- **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** — stable layout-object id derivation (`MUSCLOID`, Pass-11 item 2.6,
|
||||
deferred to Agent I) is still unwired: the determinism crate exposes no
|
||||
`MUSCLOID` tag and is frozen. The renderer traces provenance by the existing
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@
|
|||
|
||||
Agent I's **SVG renderer** behind the Epiphany `RenderIR` interface (spec
|
||||
Chapter 7): turns a `ResolvedLayoutIR` into well-formed **SVG 1.1**, drawing each
|
||||
glyph as a **genuine Bravura SMuFL outline** `<path>`. It is the visible end of
|
||||
the v0 `Score → layout IR` pipeline.
|
||||
glyph from **genuine Bravura SMuFL** data — inline outline `<path>`s by default
|
||||
(`GlyphMode::PathOutline`), or `<text>` set in an `@font-face`-embedded Bravura
|
||||
subset (`GlyphMode::EmbeddedFont`). It is the visible end of the v0
|
||||
`Score → layout IR` pipeline.
|
||||
|
||||
## Status: renderer against the stub solver
|
||||
## Status
|
||||
|
||||
This phase builds and golden-locks the renderer against the **stub solver's**
|
||||
output (the QUICKSTART development pattern), before the real engraving solver and
|
||||
the score→real-notation engraving pass land. The stub returns the IR geometry
|
||||
verbatim — a structural projection (each object becomes one arbitrary glyph in a
|
||||
row), not yet recognizable notation — so what is proven here is **renderer
|
||||
correctness and faithfulness**: real outlines, provenance preserved, output
|
||||
XML-valid and deterministic. The renderer consumes any solver's output, so the
|
||||
picture improves with no renderer change once `epiphany-engrave` lands.
|
||||
The `Score → layout IR → SVG` pipeline renders **recognizable notation** — clefs,
|
||||
noteheads at clef-relative staff positions, accidentals, key/time signatures,
|
||||
rests, barlines, and the staff lines and stems that connect them. Output is
|
||||
golden-locked against **both** the interface-only stub solver and Agent I's real
|
||||
`epiphany-engrave` solver (whose horizontal spacing pass re-spaces the glyphs),
|
||||
and the layout round-trip (criterion 6) runs through both. What the renderer
|
||||
itself guarantees, independent of engraving quality: real Bravura glyphs,
|
||||
provenance preserved to the score graph, output XML-valid and deterministic. The
|
||||
renderer consumes any solver's `ResolvedLayoutIR`.
|
||||
|
||||
## Demo
|
||||
|
||||
|
|
@ -26,6 +29,10 @@ cargo run -p epiphany-render-svg --example render_fixture -- \
|
|||
# Drive Agent I's engrave solver instead, to bisect renderer-vs-solver:
|
||||
cargo run -p epiphany-render-svg --example render_fixture -- \
|
||||
ten_measure_single_staff --solver=real > out.svg
|
||||
|
||||
# Use the embedded-font glyph mode (<text> + @font-face) instead of inline paths:
|
||||
cargo run -p epiphany-render-svg --example render_fixture -- \
|
||||
ten_measure_single_staff --glyph-mode=embedded > out.svg
|
||||
```
|
||||
|
||||
Fixtures: `ten_measure_single_staff`, `valid_score_rich`, `valid_score`. Stats and
|
||||
|
|
@ -42,19 +49,29 @@ println!("{}", out.svg);
|
|||
```
|
||||
|
||||
`render` is pure and deterministic. `RenderOptions` controls SVG-encoding choices
|
||||
only (display scale, margin, provenance attributes) — nothing that changes
|
||||
engraving.
|
||||
only (display scale, margin, provenance attributes, and `glyph_mode` — inline
|
||||
`PathOutline` vs `EmbeddedFont`) — nothing that changes engraving.
|
||||
|
||||
## Bundled outlines
|
||||
## Bundled Bravura data
|
||||
|
||||
The glyph outlines in `src/outlines_generated.rs` are extracted from the official
|
||||
OFL `Bravura.otf` by `tools/extract_bravura_outlines.py`. The font is not
|
||||
vendored; only the generated Rust is committed. Bravura is © Steinberg Media
|
||||
Technologies GmbH under the SIL Open Font License 1.1 (`tools/OFL.txt`); the
|
||||
extracted outlines are redistributed under the same license. To regenerate:
|
||||
Two generated artifacts come from the official OFL `Bravura.otf` via
|
||||
`tools/extract_bravura_outlines.py` — the font is **not vendored**, only the
|
||||
generated Rust is committed:
|
||||
|
||||
- `src/outlines_generated.rs` — the inline glyph outlines (geometry-only, so
|
||||
byte-stable across fontTools versions);
|
||||
- `src/font_subset_generated.rs` — a base64 OTF **subset** (just the pipeline's
|
||||
glyphs) for `GlyphMode::EmbeddedFont`. As a Modified Version, its primary font
|
||||
name is renamed off the Reserved Font Name "Bravura" per the OFL; a content
|
||||
BLAKE3 + decoded length are committed alongside as an integrity lock.
|
||||
|
||||
Bravura is © Steinberg Media Technologies GmbH under the SIL Open Font License 1.1
|
||||
(`tools/OFL.txt`); both artifacts are redistributed under the same license. To
|
||||
regenerate both (the subset step also needs the `blake3` package):
|
||||
|
||||
```sh
|
||||
cd crates/epiphany-render-svg/tools
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools
|
||||
python3 extract_bravura_outlines.py > ../src/outlines_generated.rs
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools blake3
|
||||
python3 extract_bravura_outlines.py --font-out ../src/font_subset_generated.rs \
|
||||
> ../src/outlines_generated.rs
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,20 +10,22 @@
|
|||
//!
|
||||
//! ```text
|
||||
//! cargo run -p epiphany-render-svg --example render_fixture -- \
|
||||
//! ten_measure_single_staff [--solver=stub|real] [--seed=N] [--no-provenance] \
|
||||
//! > out.svg
|
||||
//! ten_measure_single_staff [--solver=stub|real] [--seed=N] \
|
||||
//! [--glyph-mode=path|embedded] [--no-provenance] > out.svg
|
||||
//! ```
|
||||
//!
|
||||
//! The `--solver` flag selects the interface-only stub (`stub`, the default this
|
||||
//! phase) or Agent I's engrave solver (`real`); keeping the renderer working
|
||||
//! against both is how a renderer-vs-solver bug is bisected with a one-flag
|
||||
//! change (QUICKSTART, Agent I, "Development pattern").
|
||||
//! change (QUICKSTART, Agent I, "Development pattern"). The `--glyph-mode` flag
|
||||
//! selects inline outline `<path>`s (`path`, default) or the embedded-font
|
||||
//! `<text>` mode (`embedded`).
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use epiphany_engrave::Engraver;
|
||||
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig, StubSolver};
|
||||
use epiphany_render_svg::{render, RenderOptions};
|
||||
use epiphany_render_svg::{render, GlyphMode, RenderOptions};
|
||||
|
||||
const FIXTURES: &str = "ten_measure_single_staff, valid_score_rich, valid_score";
|
||||
const SOLVERS: &str = "stub, real";
|
||||
|
|
@ -33,6 +35,7 @@ fn main() -> ExitCode {
|
|||
let mut solver = String::from("stub");
|
||||
let mut seed: u64 = 0x000A_11CE;
|
||||
let mut emit_provenance = true;
|
||||
let mut glyph_mode = GlyphMode::PathOutline;
|
||||
|
||||
for arg in std::env::args().skip(1) {
|
||||
if let Some(v) = arg.strip_prefix("--solver=") {
|
||||
|
|
@ -42,6 +45,16 @@ fn main() -> ExitCode {
|
|||
Ok(n) => seed = n,
|
||||
Err(_) => return fail(&format!("invalid --seed value: {v}")),
|
||||
}
|
||||
} else if let Some(v) = arg.strip_prefix("--glyph-mode=") {
|
||||
glyph_mode = match v {
|
||||
"path" => GlyphMode::PathOutline,
|
||||
"embedded" => GlyphMode::EmbeddedFont,
|
||||
other => {
|
||||
return fail(&format!(
|
||||
"unknown --glyph-mode {other:?}; known: path, embedded"
|
||||
))
|
||||
}
|
||||
};
|
||||
} else if arg == "--no-provenance" {
|
||||
emit_provenance = false;
|
||||
} else if arg == "--help" || arg == "-h" {
|
||||
|
|
@ -80,6 +93,7 @@ fn main() -> ExitCode {
|
|||
&report.layout,
|
||||
&RenderOptions {
|
||||
emit_provenance,
|
||||
glyph_mode,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
|
|
@ -89,9 +103,10 @@ fn main() -> ExitCode {
|
|||
report.status
|
||||
);
|
||||
eprintln!(
|
||||
"glyphs={} paths={} fallback_rects={} provenance={} layers={} hard_constraints={} well_formed={}",
|
||||
"glyphs={} paths={} texts={} fallback_rects={} provenance={} layers={} hard_constraints={} well_formed={}",
|
||||
out.stats.glyph_count,
|
||||
out.stats.path_count,
|
||||
out.stats.text_count,
|
||||
out.stats.fallback_rect_count,
|
||||
out.stats.provenance_count,
|
||||
out.stats.layer_count,
|
||||
|
|
@ -108,9 +123,11 @@ fn main() -> ExitCode {
|
|||
|
||||
fn usage() {
|
||||
eprintln!(
|
||||
"usage: render_fixture <fixture> [--solver=stub|real] [--seed=N] [--no-provenance]\n\
|
||||
"usage: render_fixture <fixture> [--solver=stub|real] [--seed=N] \
|
||||
[--glyph-mode=path|embedded] [--no-provenance]\n\
|
||||
fixtures: {FIXTURES}\n\
|
||||
solvers: {SOLVERS} (default: stub)"
|
||||
solvers: {SOLVERS} (default: stub)\n\
|
||||
glyph-mode: path, embedded (default: path)"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,20 @@
|
|||
//! Agent I's **SVG renderer** behind the Epiphany `RenderIR` interface (spec
|
||||
//! **Chapter 7** §"RenderIR"): it turns a
|
||||
//! [`ResolvedLayoutIR`](epiphany_layout_ir::ResolvedLayoutIR) into well-formed
|
||||
//! **SVG 1.1**, drawing each glyph as a **genuine Bravura SMuFL outline**
|
||||
//! `<path>`. It is the visible end of the v0 `Score → layout IR` pipeline: from a
|
||||
//! **SVG 1.1**, drawing each glyph from **genuine Bravura SMuFL** data: inline
|
||||
//! outline `<path>`s by default, or `<text>` set in an `@font-face`-embedded
|
||||
//! subset. It is the visible end of the v0 `Score → layout IR` pipeline: from a
|
||||
//! resolved layout, produce an image a musician would recognise.
|
||||
//!
|
||||
//! ## Scope of this phase (renderer-against-stub)
|
||||
//! ## Scope and status
|
||||
//!
|
||||
//! Per the QUICKSTART development pattern (`spec/PHASE2_QUICKSTART.md`, Agent I),
|
||||
//! the renderer is built and golden-locked against the **stub solver's** output
|
||||
//! first, before the real engraving solver lands. The stub returns the
|
||||
//! constrained IR's geometry verbatim — a structural projection, not yet real
|
||||
//! notation — so this phase proves the renderer is *correct and faithful* (every
|
||||
//! glyph drawn from its real Bravura outline, provenance preserved, output
|
||||
//! XML-valid and deterministic), independently of engraving quality. The real
|
||||
//! [`epiphany_engrave`](../epiphany_engrave/index.html) solver and the
|
||||
//! score→real-notation engraving pass are the next phase; the renderer already
|
||||
//! consumes any solver's `ResolvedLayoutIR`.
|
||||
//! the renderer was golden-locked against the **stub solver's** output first, then
|
||||
//! against the real [`epiphany_engrave`](../epiphany_engrave/index.html) solver
|
||||
//! once real notation and re-spacing landed. The renderer consumes any solver's
|
||||
//! [`ResolvedLayoutIR`](epiphany_layout_ir::ResolvedLayoutIR): it preserves the
|
||||
//! resolved geometry, provenance traces, XML validity, deterministic output, and
|
||||
//! glyph-mode choice without making engraving-semantic decisions.
|
||||
//!
|
||||
//! ## What it draws, and the non-overreach rule
|
||||
//!
|
||||
|
|
@ -31,12 +29,20 @@
|
|||
//!
|
||||
//! ## Font availability
|
||||
//!
|
||||
//! The default and only mode this phase is [`GlyphMode::PathOutline`] — inline
|
||||
//! outlines, so the SVG is self-contained and needs no font installed
|
||||
//! (QUICKSTART, Agent I, recommendation). An embedded-`@font-face` mode is a
|
||||
//! future option; it is intentionally not implemented yet rather than stubbed
|
||||
//! dishonestly.
|
||||
//! Two self-contained modes ([`GlyphMode`]):
|
||||
//!
|
||||
//! * [`GlyphMode::PathOutline`] (default) inlines genuine Bravura outlines as
|
||||
//! `<path>`s — no font dependency, byte-golden-locked, the pixel-verified
|
||||
//! reference (QUICKSTART, Agent I, recommendation).
|
||||
//! * [`GlyphMode::EmbeddedFont`] references glyphs by SMuFL codepoint via a
|
||||
//! `<text>` element and an `@font-face`-embedded Bravura *subset* (the same
|
||||
//! SHA-pinned font the outlines come from, base64 in `font_subset_generated`,
|
||||
//! regenerated by `tools/extract_bravura_outlines.py --font-out`). Still
|
||||
//! self-contained — the font travels in the SVG — and text-selectable, at the
|
||||
//! cost of a larger file; glyph placement is consistent with the path mode by
|
||||
//! construction, while exact rasterisation is the consumer's font renderer's.
|
||||
|
||||
mod font_subset_generated;
|
||||
mod outline;
|
||||
mod outlines_generated;
|
||||
mod svg;
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ pub fn bundled_glyph_count() -> usize {
|
|||
BRAVURA_OUTLINES.len()
|
||||
}
|
||||
|
||||
/// The SMuFL codepoint of a bundled glyph name, if bundled. Useful for a future
|
||||
/// embedded-font rendering mode (which references glyphs by codepoint) and for
|
||||
/// The SMuFL codepoint of a bundled glyph name, if bundled. Used by the
|
||||
/// embedded-font render mode (which references glyphs by codepoint) and for
|
||||
/// debugging glyph identity.
|
||||
pub fn smufl_codepoint(name: &str) -> Option<u32> {
|
||||
outline(name).map(|o| o.codepoint)
|
||||
|
|
@ -74,6 +74,151 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// A minimal RFC-4648 base64 decoder for the integrity test (the crate has no
|
||||
/// base64 dependency); skips non-alphabet bytes, stops at padding.
|
||||
fn decode_base64(s: &str) -> Vec<u8> {
|
||||
fn val(c: u8) -> Option<u8> {
|
||||
match c {
|
||||
b'A'..=b'Z' => Some(c - b'A'),
|
||||
b'a'..=b'z' => Some(c - b'a' + 26),
|
||||
b'0'..=b'9' => Some(c - b'0' + 52),
|
||||
b'+' => Some(62),
|
||||
b'/' => Some(63),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let (mut buf, mut bits) = (0u32, 0u32);
|
||||
for &c in s.as_bytes() {
|
||||
if c == b'=' {
|
||||
break;
|
||||
}
|
||||
let Some(v) = val(c) else { continue };
|
||||
buf = (buf << 6) | u32::from(v);
|
||||
bits += 6;
|
||||
if bits >= 8 {
|
||||
bits -= 8;
|
||||
out.push((buf >> bits) as u8);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_font_payload_is_a_locked_valid_otf() {
|
||||
use crate::font_subset_generated::{
|
||||
BRAVURA_SUBSET_BLAKE3, BRAVURA_SUBSET_LEN, BRAVURA_SUBSET_OTF_BASE64,
|
||||
};
|
||||
let bytes = decode_base64(BRAVURA_SUBSET_OTF_BASE64);
|
||||
// Length + signature: a truncated or non-OTF payload fails here, not later
|
||||
// in a consumer's font engine.
|
||||
assert_eq!(
|
||||
bytes.len(),
|
||||
BRAVURA_SUBSET_LEN,
|
||||
"embedded font length changed; regenerate font_subset_generated.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
&bytes[..4],
|
||||
b"OTTO",
|
||||
"embedded font is not a CFF OpenType (OTTO) font"
|
||||
);
|
||||
// Content lock: any byte-level corruption flips the BLAKE3 (the workspace's
|
||||
// sole hash), even one that preserves the length.
|
||||
let digest = epiphany_determinism::blake3_256(&bytes);
|
||||
let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(
|
||||
hex, BRAVURA_SUBSET_BLAKE3,
|
||||
"embedded font content hash changed; regenerate font_subset_generated.rs"
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads an sfnt table slice by 4-byte tag from a decoded OTF.
|
||||
fn sfnt_table<'a>(font: &'a [u8], tag: &[u8; 4]) -> Option<&'a [u8]> {
|
||||
let num_tables = u16::from_be_bytes(font.get(4..6)?.try_into().ok()?) as usize;
|
||||
for i in 0..num_tables {
|
||||
let rec = 12 + i * 16; // after the 12-byte sfnt header
|
||||
if font.get(rec..rec + 4)? == tag {
|
||||
let off =
|
||||
u32::from_be_bytes(font.get(rec + 8..rec + 12)?.try_into().ok()?) as usize;
|
||||
let len =
|
||||
u32::from_be_bytes(font.get(rec + 12..rec + 16)?.try_into().ok()?) as usize;
|
||||
return font.get(off..off + len);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The SFNT `name` table's family record (nameID 1), decoded from the first
|
||||
/// record carrying it (UTF-16BE for Windows/Unicode platforms, Latin-1 for Mac).
|
||||
fn sfnt_family_name(name_table: &[u8]) -> Option<String> {
|
||||
let count = u16::from_be_bytes(name_table.get(2..4)?.try_into().ok()?) as usize;
|
||||
let storage = u16::from_be_bytes(name_table.get(4..6)?.try_into().ok()?) as usize;
|
||||
for i in 0..count {
|
||||
let r = 6 + i * 12;
|
||||
let platform = u16::from_be_bytes(name_table.get(r..r + 2)?.try_into().ok()?);
|
||||
let name_id = u16::from_be_bytes(name_table.get(r + 6..r + 8)?.try_into().ok()?);
|
||||
if name_id != 1 {
|
||||
continue;
|
||||
}
|
||||
let len = u16::from_be_bytes(name_table.get(r + 8..r + 10)?.try_into().ok()?) as usize;
|
||||
let off = u16::from_be_bytes(name_table.get(r + 10..r + 12)?.try_into().ok()?) as usize;
|
||||
let raw = name_table.get(storage + off..storage + off + len)?;
|
||||
return Some(if platform == 1 {
|
||||
raw.iter().map(|&b| b as char).collect()
|
||||
} else {
|
||||
raw.chunks_exact(2)
|
||||
.filter_map(|p| char::from_u32(u32::from(u16::from_be_bytes([p[0], p[1]]))))
|
||||
.collect()
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The CFF table's font name — the first entry of its Name INDEX (whose offsets
|
||||
/// are 1-based from the byte preceding the object data).
|
||||
fn cff_font_name(cff: &[u8]) -> Option<String> {
|
||||
let hdr_size = *cff.get(2)? as usize; // CFF header: major, minor, hdrSize, offSize
|
||||
let count = u16::from_be_bytes(cff.get(hdr_size..hdr_size + 2)?.try_into().ok()?) as usize;
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let off_size = usize::from(*cff.get(hdr_size + 2)?);
|
||||
let off_base = hdr_size + 3;
|
||||
let read = |i: usize| -> Option<usize> {
|
||||
let s = off_base + i * off_size;
|
||||
let mut v = 0usize;
|
||||
for k in 0..off_size {
|
||||
v = (v << 8) | usize::from(*cff.get(s + k)?);
|
||||
}
|
||||
Some(v)
|
||||
};
|
||||
let data_base = off_base + (count + 1) * off_size - 1;
|
||||
let s = cff.get(data_base + read(0)?..data_base + read(1)?)?;
|
||||
Some(String::from_utf8_lossy(s).into_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_font_presents_no_reserved_primary_name() {
|
||||
use crate::font_subset_generated::BRAVURA_SUBSET_OTF_BASE64;
|
||||
let bytes = decode_base64(BRAVURA_SUBSET_OTF_BASE64);
|
||||
// An OTF carries two naming structures; the OFL restricts the *primary name*
|
||||
// of a Modified Version, so both must be the non-reserved subset family, never
|
||||
// the bare Reserved Font Name "Bravura". (Attribution records may, and do,
|
||||
// still name Bravura — those are not the primary name.)
|
||||
let name_tbl = sfnt_table(&bytes, b"name").expect("name table present");
|
||||
assert_eq!(
|
||||
sfnt_family_name(name_tbl).as_deref(),
|
||||
Some("EpiphanyBravuraSubset"),
|
||||
"SFNT family name (nameID 1) must be the non-reserved subset family"
|
||||
);
|
||||
let cff = sfnt_table(&bytes, b"CFF ").expect("CFF table present");
|
||||
assert_eq!(
|
||||
cff_font_name(cff).as_deref(),
|
||||
Some("EpiphanyBravuraSubset"),
|
||||
"CFF Name INDEX must be the non-reserved subset family"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_have_finite_bounds_and_nonempty_paths() {
|
||||
for o in BRAVURA_OUTLINES {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ use std::fmt::Write as _;
|
|||
|
||||
use epiphany_layout_ir::{BoundingBox, Provenance, ResolvedGlyph, ResolvedLayoutIR, Transform2D};
|
||||
|
||||
use crate::outline::outline;
|
||||
use crate::font_subset_generated::{
|
||||
BRAVURA_SUBSET_FAMILY, BRAVURA_SUBSET_MIME, BRAVURA_SUBSET_OTF_BASE64,
|
||||
};
|
||||
use crate::outline::{outline, smufl_codepoint};
|
||||
use crate::xml::{check_well_formed, escape_attr};
|
||||
|
||||
/// How glyphs are drawn.
|
||||
|
|
@ -50,9 +53,18 @@ pub enum GlyphMode {
|
|||
/// Inline genuine Bravura outline `<path>`s (default). Self-contained: the
|
||||
/// SVG renders in any viewer with no font dependency (QUICKSTART, Agent I:
|
||||
/// "inline path outlines for golden fixtures and the demonstrable
|
||||
/// deliverable").
|
||||
/// deliverable"). This is the byte-golden-locked, pixel-verified reference
|
||||
/// mode.
|
||||
#[default]
|
||||
PathOutline,
|
||||
/// Reference each glyph by its SMuFL codepoint with a `<text>` element, drawn
|
||||
/// from an `@font-face`-embedded subset of Bravura (the same SHA-pinned font
|
||||
/// the outlines come from, base64 in `font_subset_generated`). The result is
|
||||
/// self-contained — the font travels in the SVG — and text-selectable, at the
|
||||
/// cost of a larger file. Glyph placement is consistent with
|
||||
/// [`GlyphMode::PathOutline`] by construction (same origin, em = 4 staff
|
||||
/// spaces); exact glyph rasterisation is then the consumer's font renderer's.
|
||||
EmbeddedFont,
|
||||
}
|
||||
|
||||
/// Renderer configuration. SVG-encoding choices only — nothing here changes
|
||||
|
|
@ -157,8 +169,12 @@ pub struct Diagnostic {
|
|||
pub struct RenderStats {
|
||||
/// Glyphs in the resolved layout (the renderer's input objects).
|
||||
pub glyph_count: usize,
|
||||
/// `<path>` elements emitted (glyphs drawn from a bundled outline).
|
||||
/// `<path>` elements emitted (glyphs drawn from a bundled outline, the
|
||||
/// default [`GlyphMode::PathOutline`]).
|
||||
pub path_count: usize,
|
||||
/// `<text>` elements emitted (glyphs set in the embedded font, the
|
||||
/// [`GlyphMode::EmbeddedFont`] mode). Zero in the default path mode.
|
||||
pub text_count: usize,
|
||||
/// Fallback `<rect>` elements emitted (glyphs with no bundled outline).
|
||||
pub fallback_rect_count: usize,
|
||||
/// `<line>` elements emitted (one per resolved stroke: staff line, stem, …).
|
||||
|
|
@ -209,13 +225,14 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut
|
|||
Some(b) => b,
|
||||
// Empty layout: a minimal, valid, honest empty canvas.
|
||||
None => {
|
||||
let svg = empty_svg(options.emit_provenance);
|
||||
let svg = empty_svg(options.glyph_mode, options.emit_provenance);
|
||||
let well_formed = check_well_formed(&svg).is_ok();
|
||||
debug_assert!(well_formed);
|
||||
return RenderOutput {
|
||||
stats: RenderStats {
|
||||
glyph_count: 0,
|
||||
path_count: 0,
|
||||
text_count: 0,
|
||||
fallback_rect_count: 0,
|
||||
stroke_count: 0,
|
||||
provenance_count: 0,
|
||||
|
|
@ -248,6 +265,7 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut
|
|||
.collect();
|
||||
|
||||
let mut path_count = 0;
|
||||
let mut text_count = 0;
|
||||
let mut fallback_rect_count = 0;
|
||||
let mut stroke_count = 0;
|
||||
let mut provenance_count = 0;
|
||||
|
|
@ -263,15 +281,26 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut
|
|||
num(height),
|
||||
);
|
||||
// Declared metadata wrapper (a comment — honest about what this is, including
|
||||
// whether provenance traces are present: suppressing them is an explicit
|
||||
// display-only choice the output announces rather than dropping silently).
|
||||
// how glyphs are drawn and whether provenance traces are present: suppressing
|
||||
// them is an explicit display-only choice the output announces, not drops).
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; \
|
||||
geometry is the resolved layout verbatim, no engraving performed here; \
|
||||
{} -->",
|
||||
" <!-- epiphany-render-svg: {}; geometry is the resolved layout \
|
||||
verbatim, no engraving performed here; {} -->",
|
||||
glyph_note(options.glyph_mode),
|
||||
provenance_note(options.emit_provenance),
|
||||
);
|
||||
// In embedded-font mode, declare the Bravura subset once via `@font-face`; the
|
||||
// `<text>` glyphs below reference it by its (non-reserved) family name (see the
|
||||
// font subset's own header for its provenance and the OFL terms it carries).
|
||||
if options.glyph_mode == GlyphMode::EmbeddedFont {
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <defs><style>@font-face {{ font-family: \"{}\"; \
|
||||
src: url(\"data:{};base64,{}\") format(\"opentype\"); }}</style></defs>",
|
||||
BRAVURA_SUBSET_FAMILY, BRAVURA_SUBSET_MIME, BRAVURA_SUBSET_OTF_BASE64,
|
||||
);
|
||||
}
|
||||
// The single y-flip wrapper: staff-space/y-up world -> SVG y-down.
|
||||
let _ = writeln!(
|
||||
s,
|
||||
|
|
@ -338,21 +367,42 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut
|
|||
} else {
|
||||
String::new()
|
||||
};
|
||||
match outline(name) {
|
||||
Some(o) => {
|
||||
// The drawn element depends on the mode: an inline outline `<path>`
|
||||
// (the self-contained default) or a `<text>` referencing the embedded
|
||||
// Bravura by SMuFL codepoint. Both anchor at the same `(x, y)` origin,
|
||||
// so the two modes are geometrically consistent. `None` (no bundled
|
||||
// outline / no codepoint) falls through to the visible bbox rect.
|
||||
let element = match options.glyph_mode {
|
||||
GlyphMode::PathOutline => outline(name).map(|o| {
|
||||
path_count += 1;
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <path d=\"{}\" transform=\"{}\" fill=\"{}\"{}{}/>",
|
||||
format!(
|
||||
"<path d=\"{}\" transform=\"{}\" fill=\"{}\"{}{}/>",
|
||||
o.path, placement, fill, opacity, prov,
|
||||
);
|
||||
)
|
||||
}),
|
||||
GlyphMode::EmbeddedFont => smufl_codepoint(name).map(|cp| {
|
||||
text_count += 1;
|
||||
// The font glyph is drawn upright by a per-glyph counter-flip
|
||||
// (`scale(1 -1)`, innermost) cancelling the outer y-flip; the
|
||||
// em is four staff spaces (SMuFL), so `font-size="4"`.
|
||||
format!(
|
||||
"<text transform=\"{placement} scale(1 -1)\" \
|
||||
font-family=\"{BRAVURA_SUBSET_FAMILY}\" font-size=\"4\" \
|
||||
fill=\"{fill}\"{opacity}{prov}>&#x{cp:X};</text>",
|
||||
)
|
||||
}),
|
||||
};
|
||||
match element {
|
||||
Some(el) => {
|
||||
let _ = writeln!(s, " {el}");
|
||||
}
|
||||
None => {
|
||||
// No outline: surface it and draw the IR bounding box so the
|
||||
// missing glyph is visible, not silently absent.
|
||||
// Unrenderable in this mode: surface it and draw the IR
|
||||
// bounding box so the missing glyph is visible, not silent.
|
||||
fallback_rect_count += 1;
|
||||
diagnostics.push(Diagnostic {
|
||||
message: "no bundled Bravura outline; drew bounding-box fallback"
|
||||
message: "no bundled Bravura glyph for this name; drew \
|
||||
bounding-box fallback"
|
||||
.to_owned(),
|
||||
glyph: Some(name.to_owned()),
|
||||
});
|
||||
|
|
@ -383,6 +433,7 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut
|
|||
stats: RenderStats {
|
||||
glyph_count: resolved.glyphs.len(),
|
||||
path_count,
|
||||
text_count,
|
||||
fallback_rect_count,
|
||||
stroke_count,
|
||||
provenance_count,
|
||||
|
|
@ -572,6 +623,16 @@ fn colour(rgba: u32) -> (String, String) {
|
|||
(fill, opacity)
|
||||
}
|
||||
|
||||
/// The glyph-mode clause of the metadata comment: which drawing strategy produced
|
||||
/// the SVG. Shared by the main render and [`empty_svg`] so the declared mode
|
||||
/// boundary is the same on the empty path.
|
||||
fn glyph_note(mode: GlyphMode) -> &'static str {
|
||||
match mode {
|
||||
GlyphMode::PathOutline => "glyphs are genuine Bravura SMuFL outlines inlined as paths",
|
||||
GlyphMode::EmbeddedFont => "glyphs are Bravura SMuFL codepoints set in the embedded font",
|
||||
}
|
||||
}
|
||||
|
||||
/// The provenance-state clause of the metadata comment. Archival mode declares
|
||||
/// traces present; display-only mode declares them suppressed — so a trace-free
|
||||
/// SVG (including the empty canvas) announces itself rather than passing as
|
||||
|
|
@ -585,13 +646,15 @@ fn provenance_note(emit_provenance: bool) -> &'static str {
|
|||
}
|
||||
|
||||
/// A minimal, valid empty SVG for a layout with nothing to draw — still declaring
|
||||
/// its provenance state, so an empty trace-free render is honest like a full one.
|
||||
fn empty_svg(emit_provenance: bool) -> String {
|
||||
/// its glyph mode and provenance state, so an empty render is honest like a full
|
||||
/// one (the metadata is the same declared boundary on both paths).
|
||||
fn empty_svg(glyph_mode: GlyphMode, emit_provenance: bool) -> String {
|
||||
format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\" viewBox=\"0 0 1 1\">\n\
|
||||
\x20\x20<!-- epiphany-render-svg: empty resolved layout (no glyphs); {} -->\n\
|
||||
\x20\x20<!-- epiphany-render-svg: empty resolved layout (no glyphs); {}; {} -->\n\
|
||||
</svg>\n",
|
||||
glyph_note(glyph_mode),
|
||||
provenance_note(emit_provenance),
|
||||
)
|
||||
}
|
||||
|
|
@ -780,6 +843,77 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_font_mode_sets_text_from_the_embedded_subset() {
|
||||
let layout = stub_layout(11);
|
||||
let out = render(
|
||||
&layout,
|
||||
&RenderOptions {
|
||||
glyph_mode: GlyphMode::EmbeddedFont,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
out.is_well_formed(),
|
||||
"embedded-font SVG must be well-formed"
|
||||
);
|
||||
|
||||
// The font is declared exactly once via @font-face with the base64 subset,
|
||||
// under its non-reserved family name (OFL: not the Reserved Font Name).
|
||||
assert_eq!(out.svg.matches("@font-face").count(), 1);
|
||||
assert!(out.svg.contains("font-family: \"EpiphanyBravuraSubset\""));
|
||||
assert!(!out.svg.contains("font-family: \"Bravura\""));
|
||||
assert!(out.svg.contains("data:font/otf;base64,"));
|
||||
|
||||
// Every glyph is a `<text>` (the stub names only bundled glyphs), none a
|
||||
// path or a fallback rect, and each carries a SMuFL codepoint reference.
|
||||
assert_eq!(out.stats.text_count, layout.glyphs.len());
|
||||
assert_eq!(out.stats.path_count, 0);
|
||||
assert_eq!(out.stats.fallback_rect_count, 0);
|
||||
assert!(out.diagnostics.is_empty());
|
||||
assert_eq!(out.svg.matches("<text ").count(), layout.glyphs.len());
|
||||
assert_eq!(out.svg.matches("&#x").count(), layout.glyphs.len());
|
||||
|
||||
// The metadata comment declares the embedded-font mode, and provenance is
|
||||
// preserved exactly as in path mode.
|
||||
assert!(out.svg.contains("set in the embedded font"));
|
||||
assert_eq!(
|
||||
out.stats.provenance_count,
|
||||
layout.glyphs.len() + layout.strokes.len()
|
||||
);
|
||||
assert!(out.svg.contains("data-prov="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_font_mode_is_deterministic_and_draws_every_glyph() {
|
||||
let layout = stub_layout(7);
|
||||
let a = render(
|
||||
&layout,
|
||||
&RenderOptions {
|
||||
glyph_mode: GlyphMode::EmbeddedFont,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
let b = render(
|
||||
&layout,
|
||||
&RenderOptions {
|
||||
glyph_mode: GlyphMode::EmbeddedFont,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(a.svg, b.svg, "embedded-font render must be deterministic");
|
||||
// Every glyph is accounted for (text or fallback rect), none dropped — the
|
||||
// same no-silent-drop contract as path mode.
|
||||
assert_eq!(
|
||||
a.stats.text_count + a.stats.fallback_rect_count,
|
||||
a.stats.glyph_count
|
||||
);
|
||||
// The default path mode draws no `<text>`; the modes do not bleed.
|
||||
let path = render(&layout, &RenderOptions::default());
|
||||
assert_eq!(path.stats.text_count, 0);
|
||||
assert!(!path.svg.contains("@font-face"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_layout_renders_a_valid_empty_canvas() {
|
||||
let layout = ResolvedLayoutIR {
|
||||
|
|
@ -806,6 +940,17 @@ mod tests {
|
|||
);
|
||||
assert!(suppressed.is_well_formed());
|
||||
assert!(suppressed.svg.contains("provenance traces suppressed"));
|
||||
|
||||
// The empty canvas also declares its glyph mode (the same boundary as a
|
||||
// full render), so an empty embedded render is not mistaken for a path one.
|
||||
let empty_embedded = render(
|
||||
&layout,
|
||||
&RenderOptions {
|
||||
glyph_mode: GlyphMode::EmbeddedFont,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
assert!(empty_embedded.svg.contains("set in the embedded font"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="824.5643" height="110.24" viewBox="0 0 82.4564 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines inlined as paths; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.0599 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="2ef8d2dba955aee38147023b72bdabb7" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="888.7701" height="110.24" viewBox="0 0 88.877 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines inlined as paths; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.065 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="2ef8d2dba955aee38147023b72bdabb7" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="279.5357" height="110.24" viewBox="0 0 27.9536 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines inlined as paths; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.0599 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="bd56ad529a36a7bbf2b4c343886b55e5" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="393.3" height="110.24" viewBox="0 0 39.33 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines inlined as paths; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.065 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="bd56ad529a36a7bbf2b4c343886b55e5" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -60,7 +60,85 @@ def load():
|
|||
verify(names_bytes, NAMES_SHA256, "glyphnames.json")
|
||||
font = TTFont(io.BytesIO(font_bytes))
|
||||
names = json.loads(names_bytes)
|
||||
return font, names
|
||||
return font, names, font_bytes
|
||||
|
||||
|
||||
# The subset is a Modified Version under the OFL, so its primary user-facing name
|
||||
# must NOT be the Reserved Font Name "Bravura" (OFL §"Reserved Font Name"). The
|
||||
# copyright/trademark/license records (which name Bravura as attribution) are kept.
|
||||
SUBSET_FAMILY = "EpiphanyBravuraSubset"
|
||||
RESERVED_FONT_NAME = "Bravura"
|
||||
|
||||
|
||||
def subset_font_b64(font_bytes, codepoints):
|
||||
"""A deterministic, OFL-renamed base64 OTF subset of `font_bytes`.
|
||||
|
||||
For the renderer's `GlyphMode::EmbeddedFont` `@font-face` data-URI. SMuFL is
|
||||
accessed by codepoint (no shaping), so layout features are dropped; the result
|
||||
is small and byte-stable for a given fontTools version. Returns
|
||||
`(family, b64, decoded_len, blake3_hex)`.
|
||||
"""
|
||||
import io, base64, blake3
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.subset import Subsetter, Options
|
||||
# `recalcTimestamp=False` keeps the source font's fixed `head.modified` instead
|
||||
# of stamping "now" on save, so the subset bytes are reproducible across runs
|
||||
# (not just within one process), making the BLAKE3 lock stable per fontTools
|
||||
# version.
|
||||
sub = TTFont(io.BytesIO(font_bytes), recalcTimestamp=False)
|
||||
opts = Options()
|
||||
opts.layout_features = [] # codepoint access only; no GSUB/GPOS shaping
|
||||
opts.name_IDs = ["*"] # keep name records incl. the OFL copyright/license
|
||||
opts.notdef_outline = True
|
||||
opts.recalc_bounds = True
|
||||
ss = Subsetter(options=opts)
|
||||
cps = sorted(set(codepoints))
|
||||
ss.populate(unicodes=cps)
|
||||
ss.subset(sub)
|
||||
|
||||
# OFL reserved-name compliance: rename the primary user-facing name off the
|
||||
# Reserved Font Name in BOTH naming structures an OTF carries — the SFNT `name`
|
||||
# table AND the CFF table's own name (the CFF Name INDEX and the top dict's
|
||||
# FullName/FamilyName). Copyright/trademark/license records (which name Bravura
|
||||
# as attribution) are left intact.
|
||||
name = sub["name"]
|
||||
for rec in list(name.names):
|
||||
if rec.nameID in (1, 4, 6, 16):
|
||||
name.setName(SUBSET_FAMILY, rec.nameID, rec.platformID, rec.platEncID, rec.langID)
|
||||
elif rec.nameID == 3:
|
||||
name.setName(rec.toUnicode().replace(RESERVED_FONT_NAME, SUBSET_FAMILY),
|
||||
3, rec.platformID, rec.platEncID, rec.langID)
|
||||
cff = sub["CFF "].cff
|
||||
cff.fontNames[0] = SUBSET_FAMILY # the CFF Name INDEX
|
||||
topdict = cff.topDictIndex[0]
|
||||
for key in ("FullName", "FamilyName"): # CFF top-dict display names
|
||||
if key in topdict.rawDict:
|
||||
setattr(topdict, key, SUBSET_FAMILY)
|
||||
|
||||
# Coverage guard: the subset cmap MUST map every requested codepoint, or the
|
||||
# embedded font would render tofu for a glyph the pipeline names.
|
||||
cmap = sub.getBestCmap()
|
||||
missing = [f"U+{cp:04X}" for cp in cps if cp not in cmap]
|
||||
if missing:
|
||||
sys.exit(f"subset cmap is missing codepoints: {missing}")
|
||||
|
||||
buf = io.BytesIO()
|
||||
sub.save(buf)
|
||||
raw = buf.getvalue()
|
||||
|
||||
# Compliance guard: reparse the *saved* bytes and confirm no primary name in
|
||||
# either structure is still the Reserved Font Name (the copyright/trademark
|
||||
# attribution may, and should, still mention Bravura).
|
||||
check = TTFont(io.BytesIO(raw))
|
||||
primary = [check["name"].getDebugName(i) for i in (1, 4, 6, 16)]
|
||||
primary.append(check["CFF "].cff.fontNames[0])
|
||||
ctop = check["CFF "].cff.topDictIndex[0]
|
||||
primary += [getattr(ctop, k, None) for k in ("FullName", "FamilyName")]
|
||||
if any(p == RESERVED_FONT_NAME for p in primary):
|
||||
sys.exit(f"reserved font name leaked into a primary name record: {primary}")
|
||||
|
||||
return (SUBSET_FAMILY, base64.b64encode(raw).decode("ascii"),
|
||||
len(raw), blake3.blake3(raw).hexdigest())
|
||||
|
||||
def round_d(d, nd=4):
|
||||
def r(m):
|
||||
|
|
@ -73,7 +151,7 @@ def main():
|
|||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
font, glyphnames = load()
|
||||
font, glyphnames, font_bytes = load()
|
||||
upm = font["head"].unitsPerEm
|
||||
sp = upm / 4.0 # font units per staff space (SMuFL em = 4 staff spaces)
|
||||
scale = 1.0 / sp
|
||||
|
|
@ -151,5 +229,50 @@ def main():
|
|||
sys.stdout.write("\n".join(o) + "\n")
|
||||
print(f"// extracted {len(rows)}/{len(NAMES)} glyphs", file=sys.stderr)
|
||||
|
||||
# Optionally emit the embedded-font subset (renderer GlyphMode::EmbeddedFont).
|
||||
if "--font-out" in sys.argv:
|
||||
import fontTools
|
||||
out_path = sys.argv[sys.argv.index("--font-out") + 1]
|
||||
family, b64, raw_len, digest = subset_font_b64(font_bytes, [cp for _, cp, _, _ in rows])
|
||||
f = []
|
||||
f.append("//! GENERATED by `tools/extract_bravura_outlines.py --font-out` — "
|
||||
"do not edit by hand.")
|
||||
f.append("//!")
|
||||
f.append("//! A subset of the OFL `Bravura.otf` holding exactly the glyphs the v0")
|
||||
f.append("//! layout pipeline can name (the `BRAVURA_METRICS` / `NAMES` set), base64")
|
||||
f.append("//! OTF for the renderer's `GlyphMode::EmbeddedFont` `@font-face` data-URI.")
|
||||
f.append("//!")
|
||||
f.append("//! As a Modified Version under the OFL, the subset's primary font name is")
|
||||
f.append(f"//! `{family}`, NOT the Reserved Font Name; the copyright/trademark/license")
|
||||
f.append("//! name records are retained as attribution (and `tools/OFL.txt` ships the")
|
||||
f.append("//! full license). The cmap is verified at generation to cover every glyph.")
|
||||
f.append("//!")
|
||||
f.append(f"//! Source (pinned + SHA-256 verified): Bravura {FONT_TAG}, "
|
||||
f"steinbergmedia/bravura @ {FONT_REF}.")
|
||||
f.append(f"//! Subsetted with fontTools {fontTools.version}. Unlike the geometry-only")
|
||||
f.append("//! outlines, the binary subset's exact bytes depend on the fontTools")
|
||||
f.append("//! version recorded here, so regeneration is reproducible per version.")
|
||||
f.append("")
|
||||
f.append("/// The subset's font-family name (non-reserved; see the module note).")
|
||||
f.append(f'pub(crate) const BRAVURA_SUBSET_FAMILY: &str = "{family}";')
|
||||
f.append("")
|
||||
f.append("/// MIME type for the embedded-font data-URI.")
|
||||
f.append('pub(crate) const BRAVURA_SUBSET_MIME: &str = "font/otf";')
|
||||
f.append("")
|
||||
f.append("/// Decoded length, in bytes, of the embedded OTF (integrity lock).")
|
||||
f.append("#[allow(dead_code)] // consumed only by the integrity test")
|
||||
f.append(f"pub(crate) const BRAVURA_SUBSET_LEN: usize = {raw_len};")
|
||||
f.append("")
|
||||
f.append("/// BLAKE3-256 (hex) of the decoded OTF bytes (content-integrity lock).")
|
||||
f.append("#[allow(dead_code)] // consumed only by the integrity test")
|
||||
f.append(f'pub(crate) const BRAVURA_SUBSET_BLAKE3: &str = "{digest}";')
|
||||
f.append("")
|
||||
f.append("/// The Bravura subset (OTF/CFF outlines), base64-encoded.")
|
||||
f.append(f'pub(crate) const BRAVURA_SUBSET_OTF_BASE64: &str = "{b64}";')
|
||||
with open(out_path, "w") as fh:
|
||||
fh.write("\n".join(f) + "\n")
|
||||
print(f"// wrote {out_path} ({len(b64)} b64 chars, {raw_len} bytes, "
|
||||
f"blake3 {digest[:16]}…)", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||