diff --git a/Cargo.lock b/Cargo.lock index e6055e4..501eb8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ name = "epiphany-render-svg" version = "0.0.0" dependencies = [ "epiphany-core", + "epiphany-determinism", "epiphany-engrave", "epiphany-layout-ir", "epiphany-testkit", diff --git a/crates/epiphany-render-svg/Cargo.toml b/crates/epiphany-render-svg/Cargo.toml index 863b79c..69bad85 100644 --- a/crates/epiphany-render-svg/Cargo.toml +++ b/crates/epiphany-render-svg/Cargo.toml @@ -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 . 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 diff --git a/crates/epiphany-render-svg/DECISIONS.md b/crates/epiphany-render-svg/DECISIONS.md index a4b0a41..0617c03 100644 --- a/crates/epiphany-render-svg/DECISIONS.md +++ b/crates/epiphany-render-svg/DECISIONS.md @@ -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 ``. 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 ``s +(`GlyphMode::PathOutline`) or as `` 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 ``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 `` 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 diff --git a/crates/epiphany-render-svg/README.md b/crates/epiphany-render-svg/README.md index b266f45..d086474 100644 --- a/crates/epiphany-render-svg/README.md +++ b/crates/epiphany-render-svg/README.md @@ -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** ``. It is the visible end of -the v0 `Score → layout IR` pipeline. +glyph from **genuine Bravura SMuFL** data — inline outline ``s by default +(`GlyphMode::PathOutline`), or `` 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 ( + @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 ``` diff --git a/crates/epiphany-render-svg/examples/render_fixture.rs b/crates/epiphany-render-svg/examples/render_fixture.rs index 44b80a7..f328b9b 100644 --- a/crates/epiphany-render-svg/examples/render_fixture.rs +++ b/crates/epiphany-render-svg/examples/render_fixture.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 ``s (`path`, default) or the embedded-font +//! `` 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 [--solver=stub|real] [--seed=N] [--no-provenance]\n\ + "usage: render_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)" ); } diff --git a/crates/epiphany-render-svg/src/font_subset_generated.rs b/crates/epiphany-render-svg/src/font_subset_generated.rs new file mode 100644 index 0000000..4d14d5b --- /dev/null +++ b/crates/epiphany-render-svg/src/font_subset_generated.rs @@ -0,0 +1,33 @@ +//! GENERATED by `tools/extract_bravura_outlines.py --font-out` — do not edit by hand. +//! +//! A subset of the OFL `Bravura.otf` holding exactly the glyphs the v0 +//! layout pipeline can name (the `BRAVURA_METRICS` / `NAMES` set), base64 +//! OTF for the renderer's `GlyphMode::EmbeddedFont` `@font-face` data-URI. +//! +//! As a Modified Version under the OFL, the subset's primary font name is +//! `EpiphanyBravuraSubset`, NOT the Reserved Font Name; the copyright/trademark/license +//! name records are retained as attribution (and `tools/OFL.txt` ships the +//! full license). The cmap is verified at generation to cover every glyph. +//! +//! Source (pinned + SHA-256 verified): Bravura 1.392, steinbergmedia/bravura @ 301087ca0b0d30b65d81bc3e718ff64b613e2a9a. +//! Subsetted with fontTools 4.63.0. Unlike the geometry-only +//! outlines, the binary subset's exact bytes depend on the fontTools +//! version recorded here, so regeneration is reproducible per version. + +/// The subset's font-family name (non-reserved; see the module note). +pub(crate) const BRAVURA_SUBSET_FAMILY: &str = "EpiphanyBravuraSubset"; + +/// MIME type for the embedded-font data-URI. +pub(crate) const BRAVURA_SUBSET_MIME: &str = "font/otf"; + +/// Decoded length, in bytes, of the embedded OTF (integrity lock). +#[allow(dead_code)] // consumed only by the integrity test +pub(crate) const BRAVURA_SUBSET_LEN: usize = 21688; + +/// BLAKE3-256 (hex) of the decoded OTF bytes (content-integrity lock). +#[allow(dead_code)] // consumed only by the integrity test +pub(crate) const BRAVURA_SUBSET_BLAKE3: &str = + "791225702d98dedf75ca439a50d5452a57590dd2a4e2cf50af731397ce9577c4"; + +/// The Bravura subset (OTF/CFF outlines), base64-encoded. +pub(crate) const BRAVURA_SUBSET_OTF_BASE64: &str = "T1RUTwAMAIAAAwBAQ0ZGIAWqQAsAACkMAAAqzEdERUYAEQAiAABT2AAAABZHUE9TRHZMdQAAU/AAAAAgR1NVQkR2THUAAFQQAAAAIE9TLzJOv1DLAAABMAAAAGBjbWFwMa0pJgAAKFAAAACcaGVhZBm4g/sAAADMAAAANmhoZWEKDPsBAAABBAAAACRobXR4MCD/4wAAVDAAAACIbWF4cAAiUAAAAAEoAAAABm5hbWUPn8HRAAABkAAAJr5wb3N0/7gAMgAAKOwAAAAgAAEAAAABZFosBRnIXw889QADA+gAAAAA3DiMSgAAAADcOlFY/3P81QK7BEoAAAADAAIAAAAAAAAAAQAAB9z4JAAAArv/c///ArsAAQAAAAAAAAAAAAAAAAAAACIAAFAAACIAAAAEAbgBkAAFAAACigJYAAAASwKKAlgAAAFeADIApQAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAABTTVRHAEDgMOUiB9z4JAAAB9wH3AAAAAEAAAAAARMB2wAAACAABgAAAA8AugADAAEECQAAIoAAAAADAAEECQABACoigAADAAEECQACAA4iqgADAAEECQADAGYiuAADAAEECQAEACoigAADAAEECQAFABojHgADAAEECQAGACoigAADAAEECQAHAOYjOAADAAEECQAIAEIkHgADAAEECQAJADAkYAADAAEECQAKAQ4kkAADAAEECQALADIlngADAAEECQAMADIlngADAAEECQANIoAAAAADAAEECQAOADQl0ABDAG8AcAB5AHIAaQBnAGgAdAAgAKkAIAAyADAAMgAxACwAIABTAHQAZQBpAG4AYgBlAHIAZwAgAE0AZQBkAGkAYQAgAFQAZQBjAGgAbgBvAGwAbwBnAGkAZQBzACAARwBtAGIASAAgACgAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAHMAdABlAGkAbgBiAGUAcgBnAC4AbgBlAHQALwApACwAIAB3AGkAdABoACAAUgBlAHMAZQByAHYAZQBkACAARgBvAG4AdAAgAE4AYQBtAGUAIAAiAEIAcgBhAHYAdQByAGEAIgAuAAoACgBUAGgAaQBzACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAGkAcwAgAGwAaQBjAGUAbgBzAGUAZAAgAHUAbgBkAGUAcgAgAHQAaABlACAAUwBJAEwAIABPAHAAZQBuACAARgBvAG4AdAAgAEwAaQBjAGUAbgBzAGUALAAgAFYAZQByAHMAaQBvAG4AIAAxAC4AMQAuACAAVABoAGkAcwAgAGwAaQBjAGUAbgBzAGUAIABpAHMAIABjAG8AcABpAGUAZAAgAGIAZQBsAG8AdwAsACAAYQBuAGQAIABpAHMAIABhAGwAcwBvACAAYQB2AGEAaQBsAGEAYgBsAGUAIAB3AGkAdABoACAAYQAgAEYAQQBRACAAYQB0ADoAIABoAHQAdABwADoALwAvAHMAYwByAGkAcAB0AHMALgBzAGkAbAAuAG8AcgBnAC8ATwBGAEwACgAKAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAKAFMASQBMACAATwBQAEUATgAgAEYATwBOAFQAIABMAEkAQwBFAE4AUwBFACAAVgBlAHIAcwBpAG8AbgAgADEALgAxACAALQAgADIANgAgAEYAZQBiAHIAdQBhAHIAeQAgADIAMAAwADcACgAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ACgAKAFAAUgBFAEEATQBCAEwARQAKAFQAaABlACAAZwBvAGEAbABzACAAbwBmACAAdABoAGUAIABPAHAAZQBuACAARgBvAG4AdAAgAEwAaQBjAGUAbgBzAGUAIAAoAE8ARgBMACkAIABhAHIAZQAgAHQAbwAgAHMAdABpAG0AdQBsAGEAdABlACAAdwBvAHIAbABkAHcAaQBkAGUAIABkAGUAdgBlAGwAbwBwAG0AZQBuAHQAIABvAGYAIABjAG8AbABsAGEAYgBvAHIAYQB0AGkAdgBlACAAZgBvAG4AdAAgAHAAcgBvAGoAZQBjAHQAcwAsACAAdABvACAAcwB1AHAAcABvAHIAdAAgAHQAaABlACAAZgBvAG4AdAAgAGMAcgBlAGEAdABpAG8AbgAgAGUAZgBmAG8AcgB0AHMAIABvAGYAIABhAGMAYQBkAGUAbQBpAGMAIABhAG4AZAAgAGwAaQBuAGcAdQBpAHMAdABpAGMAIABjAG8AbQBtAHUAbgBpAHQAaQBlAHMALAAgAGEAbgBkACAAdABvACAAcAByAG8AdgBpAGQAZQAgAGEAIABmAHIAZQBlACAAYQBuAGQAIABvAHAAZQBuACAAZgByAGEAbQBlAHcAbwByAGsAIABpAG4AIAB3AGgAaQBjAGgAIABmAG8AbgB0AHMAIABtAGEAeQAgAGIAZQAgAHMAaABhAHIAZQBkACAAYQBuAGQAIABpAG0AcAByAG8AdgBlAGQAIABpAG4AIABwAGEAcgB0AG4AZQByAHMAaABpAHAAIAB3AGkAdABoACAAbwB0AGgAZQByAHMALgAKAAoAVABoAGUAIABPAEYATAAgAGEAbABsAG8AdwBzACAAdABoAGUAIABsAGkAYwBlAG4AcwBlAGQAIABmAG8AbgB0AHMAIAB0AG8AIABiAGUAIAB1AHMAZQBkACwAIABzAHQAdQBkAGkAZQBkACwAIABtAG8AZABpAGYAaQBlAGQAIABhAG4AZAAgAHIAZQBkAGkAcwB0AHIAaQBiAHUAdABlAGQAIABmAHIAZQBlAGwAeQAgAGEAcwAgAGwAbwBuAGcAIABhAHMAIAB0AGgAZQB5ACAAYQByAGUAIABuAG8AdAAgAHMAbwBsAGQAIABiAHkAIAB0AGgAZQBtAHMAZQBsAHYAZQBzAC4AIABUAGgAZQAgAGYAbwBuAHQAcwAsACAAaQBuAGMAbAB1AGQAaQBuAGcAIABhAG4AeQAgAGQAZQByAGkAdgBhAHQAaQB2AGUAIAB3AG8AcgBrAHMALAAgAGMAYQBuACAAYgBlACAAYgB1AG4AZABsAGUAZAAsACAAZQBtAGIAZQBkAGQAZQBkACwAIAByAGUAZABpAHMAdAByAGkAYgB1AHQAZQBkACAAYQBuAGQALwBvAHIAIABzAG8AbABkACAAdwBpAHQAaAAgAGEAbgB5ACAAcwBvAGYAdAB3AGEAcgBlACAAcAByAG8AdgBpAGQAZQBkACAAdABoAGEAdAAgAGEAbgB5ACAAcgBlAHMAZQByAHYAZQBkACAAbgBhAG0AZQBzACAAYQByAGUAIABuAG8AdAAgAHUAcwBlAGQAIABiAHkAIABkAGUAcgBpAHYAYQB0AGkAdgBlACAAdwBvAHIAawBzAC4AIABUAGgAZQAgAGYAbwBuAHQAcwAgAGEAbgBkACAAZABlAHIAaQB2AGEAdABpAHYAZQBzACwAIABoAG8AdwBlAHYAZQByACwAIABjAGEAbgBuAG8AdAAgAGIAZQAgAHIAZQBsAGUAYQBzAGUAZAAgAHUAbgBkAGUAcgAgAGEAbgB5ACAAbwB0AGgAZQByACAAdAB5AHAAZQAgAG8AZgAgAGwAaQBjAGUAbgBzAGUALgAgAFQAaABlACAAcgBlAHEAdQBpAHIAZQBtAGUAbgB0ACAAZgBvAHIAIABmAG8AbgB0AHMAIAB0AG8AIAByAGUAbQBhAGkAbgAgAHUAbgBkAGUAcgAgAHQAaABpAHMAIABsAGkAYwBlAG4AcwBlACAAZABvAGUAcwAgAG4AbwB0ACAAYQBwAHAAbAB5ACAAdABvACAAYQBuAHkAIABkAG8AYwB1AG0AZQBuAHQAIABjAHIAZQBhAHQAZQBkACAAdQBzAGkAbgBnACAAdABoAGUAIABmAG8AbgB0AHMAIABvAHIAIAB0AGgAZQBpAHIAIABkAGUAcgBpAHYAYQB0AGkAdgBlAHMALgAKAAoARABFAEYASQBOAEkAVABJAE8ATgBTAAoAIgBGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACIAIAByAGUAZgBlAHIAcwAgAHQAbwAgAHQAaABlACAAcwBlAHQAIABvAGYAIABmAGkAbABlAHMAIAByAGUAbABlAGEAcwBlAGQAIABiAHkAIAB0AGgAZQAgAEMAbwBwAHkAcgBpAGcAaAB0ACAASABvAGwAZABlAHIAKABzACkAIAB1AG4AZABlAHIAIAB0AGgAaQBzACAAbABpAGMAZQBuAHMAZQAgAGEAbgBkACAAYwBsAGUAYQByAGwAeQAgAG0AYQByAGsAZQBkACAAYQBzACAAcwB1AGMAaAAuACAAVABoAGkAcwAgAG0AYQB5AAoAaQBuAGMAbAB1AGQAZQAgAHMAbwB1AHIAYwBlACAAZgBpAGwAZQBzACwAIABiAHUAaQBsAGQAIABzAGMAcgBpAHAAdABzACAAYQBuAGQAIABkAG8AYwB1AG0AZQBuAHQAYQB0AGkAbwBuAC4ACgAKACIAUgBlAHMAZQByAHYAZQBkACAARgBvAG4AdAAgAE4AYQBtAGUAIgAgAHIAZQBmAGUAcgBzACAAdABvACAAYQBuAHkAIABuAGEAbQBlAHMAIABzAHAAZQBjAGkAZgBpAGUAZAAgAGEAcwAgAHMAdQBjAGgAIABhAGYAdABlAHIAIAB0AGgAZQAgAGMAbwBwAHkAcgBpAGcAaAB0ACAAcwB0AGEAdABlAG0AZQBuAHQAKABzACkALgAKAAoAIgBPAHIAaQBnAGkAbgBhAGwAIABWAGUAcgBzAGkAbwBuACIAIAByAGUAZgBlAHIAcwAgAHQAbwAgAHQAaABlACAAYwBvAGwAbABlAGMAdABpAG8AbgAgAG8AZgAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUAIABjAG8AbQBwAG8AbgBlAG4AdABzACAAYQBzACAAZABpAHMAdAByAGkAYgB1AHQAZQBkACAAYgB5ACAAdABoAGUAIABDAG8AcAB5AHIAaQBnAGgAdAAgAEgAbwBsAGQAZQByACgAcwApAC4ACgAKACIATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgAiACAAcgBlAGYAZQByAHMAIAB0AG8AIABhAG4AeQAgAGQAZQByAGkAdgBhAHQAaQB2AGUAIABtAGEAZABlACAAYgB5ACAAYQBkAGQAaQBuAGcAIAB0AG8ALAAgAGQAZQBsAGUAdABpAG4AZwAsACAAbwByACAAcwB1AGIAcwB0AGkAdAB1AHQAaQBuAGcAIAAtAC0AIABpAG4AIABwAGEAcgB0ACAAbwByACAAaQBuACAAdwBoAG8AbABlACAALQAtACAAYQBuAHkAIABvAGYAIAB0AGgAZQAgAGMAbwBtAHAAbwBuAGUAbgB0AHMAIABvAGYAIAB0AGgAZQAgAE8AcgBpAGcAaQBuAGEAbAAgAFYAZQByAHMAaQBvAG4ALAAgAGIAeQAgAGMAaABhAG4AZwBpAG4AZwAgAGYAbwByAG0AYQB0AHMAIABvAHIAIABiAHkAIABwAG8AcgB0AGkAbgBnACAAdABoAGUAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACAAdABvACAAYQAgAG4AZQB3ACAAZQBuAHYAaQByAG8AbgBtAGUAbgB0AC4ACgAKACIAQQB1AHQAaABvAHIAIgAgAHIAZQBmAGUAcgBzACAAdABvACAAYQBuAHkAIABkAGUAcwBpAGcAbgBlAHIALAAgAGUAbgBnAGkAbgBlAGUAcgAsACAAcAByAG8AZwByAGEAbQBtAGUAcgAsACAAdABlAGMAaABuAGkAYwBhAGwAIAB3AHIAaQB0AGUAcgAgAG8AcgAgAG8AdABoAGUAcgAgAHAAZQByAHMAbwBuACAAdwBoAG8AIABjAG8AbgB0AHIAaQBiAHUAdABlAGQAIAB0AG8AIAB0AGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALgAKAAoAUABFAFIATQBJAFMAUwBJAE8ATgAgACYAIABDAE8ATgBEAEkAVABJAE8ATgBTAAoAUABlAHIAbQBpAHMAcwBpAG8AbgAgAGkAcwAgAGgAZQByAGUAYgB5ACAAZwByAGEAbgB0AGUAZAAsACAAZgByAGUAZQAgAG8AZgAgAGMAaABhAHIAZwBlACwAIAB0AG8AIABhAG4AeQAgAHAAZQByAHMAbwBuACAAbwBiAHQAYQBpAG4AaQBuAGcAIABhACAAYwBvAHAAeQAgAG8AZgAgAHQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAsACAAdABvACAAdQBzAGUALAAgAHMAdAB1AGQAeQAsACAAYwBvAHAAeQAsACAAbQBlAHIAZwBlACwAIABlAG0AYgBlAGQALAAgAG0AbwBkAGkAZgB5ACwAIAByAGUAZABpAHMAdAByAGkAYgB1AHQAZQAsACAAYQBuAGQAIABzAGUAbABsACAAbQBvAGQAaQBmAGkAZQBkACAAYQBuAGQAIAB1AG4AbQBvAGQAaQBmAGkAZQBkACAAYwBvAHAAaQBlAHMAIABvAGYAIAB0AGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALAAgAHMAdQBiAGoAZQBjAHQAIAB0AG8AIAB0AGgAZQAgAGYAbwBsAGwAbwB3AGkAbgBnACAAYwBvAG4AZABpAHQAaQBvAG4AcwA6AAoACgAxACkAIABOAGUAaQB0AGgAZQByACAAdABoAGUAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACAAbgBvAHIAIABhAG4AeQAgAG8AZgAgAGkAdABzACAAaQBuAGQAaQB2AGkAZAB1AGEAbAAgAGMAbwBtAHAAbwBuAGUAbgB0AHMALAAgAGkAbgAgAE8AcgBpAGcAaQBuAGEAbAAgAG8AcgAgAE0AbwBkAGkAZgBpAGUAZAAgAFYAZQByAHMAaQBvAG4AcwAsACAAbQBhAHkAIABiAGUAIABzAG8AbABkACAAYgB5ACAAaQB0AHMAZQBsAGYALgAKAAoAMgApACAATwByAGkAZwBpAG4AYQBsACAAbwByACAATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgBzACAAbwBmACAAdABoAGUAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACAAbQBhAHkAIABiAGUAIABiAHUAbgBkAGwAZQBkACwAIAByAGUAZABpAHMAdAByAGkAYgB1AHQAZQBkACAAYQBuAGQALwBvAHIAIABzAG8AbABkACAAdwBpAHQAaAAgAGEAbgB5ACAAcwBvAGYAdAB3AGEAcgBlACwAIABwAHIAbwB2AGkAZABlAGQAIAB0AGgAYQB0ACAAZQBhAGMAaAAgAGMAbwBwAHkAIABjAG8AbgB0AGEAaQBuAHMAIAB0AGgAZQAgAGEAYgBvAHYAZQAgAGMAbwBwAHkAcgBpAGcAaAB0ACAAbgBvAHQAaQBjAGUAIABhAG4AZAAgAHQAaABpAHMAIABsAGkAYwBlAG4AcwBlAC4AIABUAGgAZQBzAGUAIABjAGEAbgAgAGIAZQAgAGkAbgBjAGwAdQBkAGUAZAAgAGUAaQB0AGgAZQByACAAYQBzACAAcwB0AGEAbgBkAC0AYQBsAG8AbgBlACAAdABlAHgAdAAgAGYAaQBsAGUAcwAsACAAaAB1AG0AYQBuAC0AcgBlAGEAZABhAGIAbABlACAAaABlAGEAZABlAHIAcwAgAG8AcgAgAGkAbgAgAHQAaABlACAAYQBwAHAAcgBvAHAAcgBpAGEAdABlACAAbQBhAGMAaABpAG4AZQAtAHIAZQBhAGQAYQBiAGwAZQAgAG0AZQB0AGEAZABhAHQAYQAgAGYAaQBlAGwAZABzACAAdwBpAHQAaABpAG4AIAB0AGUAeAB0ACAAbwByACAAYgBpAG4AYQByAHkAIABmAGkAbABlAHMAIABhAHMAIABsAG8AbgBnACAAYQBzACAAdABoAG8AcwBlACAAZgBpAGUAbABkAHMAIABjAGEAbgAgAGIAZQAgAGUAYQBzAGkAbAB5ACAAdgBpAGUAdwBlAGQAIABiAHkAIAB0AGgAZQAgAHUAcwBlAHIALgAKAAoAMwApACAATgBvACAATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgAgAG8AZgAgAHQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAG0AYQB5ACAAdQBzAGUAIAB0AGgAZQAgAFIAZQBzAGUAcgB2AGUAZAAgAEYAbwBuAHQAIABOAGEAbQBlACgAcwApACAAdQBuAGwAZQBzAHMAIABlAHgAcABsAGkAYwBpAHQAIAB3AHIAaQB0AHQAZQBuACAAcABlAHIAbQBpAHMAcwBpAG8AbgAgAGkAcwAgAGcAcgBhAG4AdABlAGQAIABiAHkAIAB0AGgAZQAgAGMAbwByAHIAZQBzAHAAbwBuAGQAaQBuAGcAIABDAG8AcAB5AHIAaQBnAGgAdAAgAEgAbwBsAGQAZQByAC4AIABUAGgAaQBzACAAcgBlAHMAdAByAGkAYwB0AGkAbwBuACAAbwBuAGwAeQAgAGEAcABwAGwAaQBlAHMAIAB0AG8AIAB0AGgAZQAgAHAAcgBpAG0AYQByAHkAIABmAG8AbgB0ACAAbgBhAG0AZQAgAGEAcwAgAHAAcgBlAHMAZQBuAHQAZQBkACAAdABvACAAdABoAGUAIAB1AHMAZQByAHMALgAKAAoANAApACAAVABoAGUAIABuAGEAbQBlACgAcwApACAAbwBmACAAdABoAGUAIABDAG8AcAB5AHIAaQBnAGgAdAAgAEgAbwBsAGQAZQByACgAcwApACAAbwByACAAdABoAGUAIABBAHUAdABoAG8AcgAoAHMAKQAgAG8AZgAgAHQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAHMAaABhAGwAbAAgAG4AbwB0ACAAYgBlACAAdQBzAGUAZAAgAHQAbwAgAHAAcgBvAG0AbwB0AGUALAAgAGUAbgBkAG8AcgBzAGUAIABvAHIAIABhAGQAdgBlAHIAdABpAHMAZQAgAGEAbgB5ACAATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgAsACAAZQB4AGMAZQBwAHQAIAB0AG8AIABhAGMAawBuAG8AdwBsAGUAZABnAGUAIAB0AGgAZQAgAGMAbwBuAHQAcgBpAGIAdQB0AGkAbwBuACgAcwApACAAbwBmACAAdABoAGUAIABDAG8AcAB5AHIAaQBnAGgAdAAgAEgAbwBsAGQAZQByACgAcwApACAAYQBuAGQAIAB0AGgAZQAgAEEAdQB0AGgAbwByACgAcwApACAAbwByACAAdwBpAHQAaAAgAHQAaABlAGkAcgAgAGUAeABwAGwAaQBjAGkAdAAgAHcAcgBpAHQAdABlAG4AIABwAGUAcgBtAGkAcwBzAGkAbwBuAC4ACgAKADUAKQAgAFQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAsACAAbQBvAGQAaQBmAGkAZQBkACAAbwByACAAdQBuAG0AbwBkAGkAZgBpAGUAZAAsACAAaQBuACAAcABhAHIAdAAgAG8AcgAgAGkAbgAgAHcAaABvAGwAZQAsACAAbQB1AHMAdAAgAGIAZQAgAGQAaQBzAHQAcgBpAGIAdQB0AGUAZAAgAGUAbgB0AGkAcgBlAGwAeQAgAHUAbgBkAGUAcgAgAHQAaABpAHMAIABsAGkAYwBlAG4AcwBlACwAIABhAG4AZAAgAG0AdQBzAHQAIABuAG8AdAAgAGIAZQAgAGQAaQBzAHQAcgBpAGIAdQB0AGUAZAAgAHUAbgBkAGUAcgAgAGEAbgB5ACAAbwB0AGgAZQByACAAbABpAGMAZQBuAHMAZQAuACAAVABoAGUAIAByAGUAcQB1AGkAcgBlAG0AZQBuAHQAIABmAG8AcgAgAGYAbwBuAHQAcwAgAHQAbwAgAHIAZQBtAGEAaQBuACAAdQBuAGQAZQByACAAdABoAGkAcwAgAGwAaQBjAGUAbgBzAGUAIABkAG8AZQBzACAAbgBvAHQAIABhAHAAcABsAHkAIAB0AG8AIABhAG4AeQAgAGQAbwBjAHUAbQBlAG4AdAAgAGMAcgBlAGEAdABlAGQAIAB1AHMAaQBuAGcAIAB0AGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALgAKAAoAVABFAFIATQBJAE4AQQBUAEkATwBOAAoAVABoAGkAcwAgAGwAaQBjAGUAbgBzAGUAIABiAGUAYwBvAG0AZQBzACAAbgB1AGwAbAAgAGEAbgBkACAAdgBvAGkAZAAgAGkAZgAgAGEAbgB5ACAAbwBmACAAdABoAGUAIABhAGIAbwB2AGUAIABjAG8AbgBkAGkAdABpAG8AbgBzACAAYQByAGUAIABuAG8AdAAgAG0AZQB0AC4ACgAKAEQASQBTAEMATABBAEkATQBFAFIACgBUAEgARQAgAEYATwBOAFQAIABTAE8ARgBUAFcAQQBSAEUAIABJAFMAIABQAFIATwBWAEkARABFAEQAIAAiAEEAUwAgAEkAUwAiACwAIABXAEkAVABIAE8AVQBUACAAVwBBAFIAUgBBAE4AVABZACAATwBGACAAQQBOAFkAIABLAEkATgBEACwAIABFAFgAUABSAEUAUwBTACAATwBSACAASQBNAFAATABJAEUARAAsACAASQBOAEMATABVAEQASQBOAEcAIABCAFUAVAAgAE4ATwBUACAATABJAE0ASQBUAEUARAAgAFQATwAgAEEATgBZACAAVwBBAFIAUgBBAE4AVABJAEUAUwAgAE8ARgAgAE0ARQBSAEMASABBAE4AVABBAEIASQBMAEkAVABZACwAIABGAEkAVABOAEUAUwBTACAARgBPAFIAIABBACAAUABBAFIAVABJAEMAVQBMAEEAUgAgAFAAVQBSAFAATwBTAEUAIABBAE4ARAAgAE4ATwBOAEkATgBGAFIASQBOAEcARQBNAEUATgBUACAATwBGACAAQwBPAFAAWQBSAEkARwBIAFQALAAgAFAAQQBUAEUATgBUACwAIABUAFIAQQBEAEUATQBBAFIASwAsACAATwBSACAATwBUAEgARQBSACAAUgBJAEcASABUAC4AIABJAE4AIABOAE8AIABFAFYARQBOAFQAIABTAEgAQQBMAEwAIABUAEgARQAgAEMATwBQAFkAUgBJAEcASABUACAASABPAEwARABFAFIAIABCAEUAIABMAEkAQQBCAEwARQAgAEYATwBSACAAQQBOAFkAIABDAEwAQQBJAE0ALAAgAEQAQQBNAEEARwBFAFMAIABPAFIAIABPAFQASABFAFIAIABMAEkAQQBCAEkATABJAFQAWQAsACAASQBOAEMATABVAEQASQBOAEcAIABBAE4AWQAgAEcARQBOAEUAUgBBAEwALAAgAFMAUABFAEMASQBBAEwALAAgAEkATgBEAEkAUgBFAEMAVAAsACAASQBOAEMASQBEAEUATgBUAEEATAAsACAATwBSACAAQwBPAE4AUwBFAFEAVQBFAE4AVABJAEEATAAgAEQAQQBNAEEARwBFAFMALAAgAFcASABFAFQASABFAFIAIABJAE4AIABBAE4AIABBAEMAVABJAE8ATgAgAE8ARgAgAEMATwBOAFQAUgBBAEMAVAAsACAAVABPAFIAVAAgAE8AUgAgAE8AVABIAEUAUgBXAEkAUwBFACwAIABBAFIASQBTAEkATgBHACAARgBSAE8ATQAsACAATwBVAFQAIABPAEYAIABUAEgARQAgAFUAUwBFACAATwBSACAASQBOAEEAQgBJAEwASQBUAFkAIABUAE8AIABVAFMARQAgAFQASABFACAARgBPAE4AVAAgAFMATwBGAFQAVwBBAFIARQAgAE8AUgAgAEYAUgBPAE0AIABPAFQASABFAFIAIABEAEUAQQBMAEkATgBHAFMAIABJAE4AIABUAEgARQAgAEYATwBOAFQAIABTAE8ARgBUAFcAQQBSAEUALgBFAHAAaQBwAGgAYQBuAHkAQgByAGEAdgB1AHIAYQBTAHUAYgBzAGUAdABSAGUAZwB1AGwAYQByAFYAZQByAHMAaQBvAG4AIAAxAC4AMwA5ADIAOwBTAE0AVABHADsARQBwAGkAcABoAGEAbgB5AEIAcgBhAHYAdQByAGEAUwB1AGIAcwBlAHQAOwAyADAAMgAxADsARgBMADcAMgAwAFYAZQByAHMAaQBvAG4AIAAxAC4AMwA5ADIAQgByAGEAdgB1AHIAYQAgAGkAcwAgAGEAIAByAGUAZwBpAHMAdABlAHIAZQBkACAAdAByAGEAZABlAG0AYQByAGsAIABvAGYAIABTAHQAZQBpAG4AYgBlAHIAZwAgAE0AZQBkAGkAYQAgAFQAZQBjAGgAbgBvAGwAbwBnAGkAZQBzACAARwBtAGIASAAgAGkAbgAgAHQAaABlACAARQB1AHIAbwBwAGUAYQBuACAAVQBuAGkAbwBuACAAYQBuAGQAIABvAHQAaABlAHIAIAB0AGUAcgByAGkAdABvAHIAaQBlAHMALgBTAHQAZQBpAG4AYgBlAHIAZwAgAE0AZQBkAGkAYQAgAFQAZQBjAGgAbgBvAGwAbwBnAGkAZQBzACAARwBtAGIASABEAGEAbgBpAGUAbAAgAFMAcAByAGUAYQBkAGIAdQByAHkAIABlAHQAIABhAGwALgBDAG8AcAB5AHIAaQBnAGgAdAAgAKkAIAAyADAAMgAxACAAUwB0AGUAaQBuAGIAZQByAGcAIABNAGUAZABpAGEAIABUAGUAYwBoAG4AbwBsAG8AZwBpAGUAcwAgAEcAbQBiAEgALgAgAFQAaABpAHMAIABmAG8AbgB0ACAAaQBzACAAbABpAGMAZQBuAHMAZQBkACAAdQBuAGQAZQByACAAdABoAGUAIABTAEkATAAgAE8AcABlAG4AIABGAG8AbgB0ACAATABpAGMAZQBuAHMAZQAgACgAaAB0AHQAcAA6AC8ALwBzAGMAcgBpAHAAdABzAC4AcwBpAGwALgBvAHIAZwAvAE8ARgBMACkALgBoAHQAdABwADoALwAvAHcAdwB3AC4AcwB0AGUAaQBuAGIAZQByAGcALgBuAGUAdAAvAGgAdAB0AHAAOgAvAC8AcwBjAHIAaQBwAHQAcwAuAHMAaQBsAC4AbwByAGcALwBPAEYATAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABACIAAAAHgAQAAMADuAw4DLgUOBc4GLgiuCg4KTh5+JB4mPk5uUg5SL//wAA4DDgMuBQ4FzgYuCA4KDgouHn4kDiYOTj5SDlIv//H9Ef0B+zH6gfox+GH3EfcB4uHdYduBs5GwAa/wABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/tQAyAAAAAAAAAAAAAAAAAAAAAAAAAAABAAQCAAEBARZFcGlwaGFueUJyYXZ1cmFTdWJzZXQAAQEBKvg8APg9Afg+DAD4PwL4PwP4QAT7If2/+U/63gUcIP8PpBwmUxIcIQMRACYCAAEACAAPABYAHQAkACsAMgA5AEAARwBOAFUAXABjAGoAcQB4AH8AhgCNAJQAmwCiAKkAsAC3AL4AxQDMANMA2gDhAOgA9QFoErgSzRLTdW5pRTAzMHVuaUUwMzJ1bmlFMDUwdW5pRTA1Q3VuaUUwNjJ1bmlFMDgwdW5pRTA4MXVuaUUwODJ1bmlFMDgzdW5pRTA4NHVuaUUwODV1bmlFMDg2dW5pRTA4N3VuaUUwODh1bmlFMDg5dW5pRTA4QXVuaUUwQTB1bmlFMEEydW5pRTBBM3VuaUUwQTR1bmlFMUU3dW5pRTI0MHVuaUUyNDF1bmlFMjYwdW5pRTI2MXVuaUUyNjJ1bmlFMjYzdW5pRTRFM3VuaUU0RTR1bmlFNEU1dW5pRTRFNnVuaUU1MjB1bmlFNTIyVmVyc2lvbiAxLjM5MkJyYXZ1cmEgaXMgYSByZWdpc3RlcmVkIHRyYWRlbWFyayBvZiBTdGVpbmJlcmcgTWVkaWEgVGVjaG5vbG9naWVzIEdtYkggaW4gdGhlIEV1cm9wZWFuIFVuaW9uIGFuZCBvdGhlciB0ZXJyaXRvcmllcy5Db3B5cmlnaHQgXChjXCkgMjAyMSwgU3RlaW5iZXJnIE1lZGlhIFRlY2hub2xvZ2llcyBHbWJIIFwoaHR0cDovL3d3dy5zdGVpbmJlcmcubmV0L1wpLCB3aXRoIFJlc2VydmVkIEZvbnQgTmFtZSAiQnJhdnVyYSIuIFRoaXMgRm9udCBTb2Z0d2FyZSBpcyBsaWNlbnNlZCB1bmRlciB0aGUgU0lMIE9wZW4gRm9udCBMaWNlbnNlLCBWZXJzaW9uIDEuMS4gVGhpcyBsaWNlbnNlIGlzIGNvcGllZCBiZWxvdywgYW5kIGlzIGFsc28gYXZhaWxhYmxlIHdpdGggYSBGQVEgYXQ6IGh0dHA6Ly9zY3JpcHRzLnNpbC5vcmcvT0ZMIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIFNJTCBPUEVOIEZPTlQgTElDRU5TRSBWZXJzaW9uIDEuMSAtIDI2IEZlYnJ1YXJ5IDIwMDcgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gUFJFQU1CTEUgVGhlIGdvYWxzIG9mIHRoZSBPcGVuIEZvbnQgTGljZW5zZSBcKE9GTFwpIGFyZSB0byBzdGltdWxhdGUgd29ybGR3aWRlIGRldmVsb3BtZW50IG9mIGNvbGxhYm9yYXRpdmUgZm9udCBwcm9qZWN0cywgdG8gc3VwcG9ydCB0aGUgZm9udCBjcmVhdGlvbiBlZmZvcnRzIG9mIGFjYWRlbWljIGFuZCBsaW5ndWlzdGljIGNvbW11bml0aWVzLCBhbmQgdG8gcHJvdmlkZSBhIGZyZWUgYW5kIG9wZW4gZnJhbWV3b3JrIGluIHdoaWNoIGZvbnRzIG1heSBiZSBzaGFyZWQgYW5kIGltcHJvdmVkIGluIHBhcnRuZXJzaGlwIHdpdGggb3RoZXJzLiBUaGUgT0ZMIGFsbG93cyB0aGUgbGljZW5zZWQgZm9udHMgdG8gYmUgdXNlZCwgc3R1ZGllZCwgbW9kaWZpZWQgYW5kIHJlZGlzdHJpYnV0ZWQgZnJlZWx5IGFzIGxvbmcgYXMgdGhleSBhcmUgbm90IHNvbGQgYnkgdGhlbXNlbHZlcy4gVGhlIGZvbnRzLCBpbmNsdWRpbmcgYW55IGRlcml2YXRpdmUgd29ya3MsIGNhbiBiZSBidW5kbGVkLCBlbWJlZGRlZCwgcmVkaXN0cmlidXRlZCBhbmQvb3Igc29sZCB3aXRoIGFueSBzb2Z0d2FyZSBwcm92aWRlZCB0aGF0IGFueSByZXNlcnZlZCBuYW1lcyBhcmUgbm90IHVzZWQgYnkgZGVyaXZhdGl2ZSB3b3Jrcy4gVGhlIGZvbnRzIGFuZCBkZXJpdmF0aXZlcywgaG93ZXZlciwgY2Fubm90IGJlIHJlbGVhc2VkIHVuZGVyIGFueSBvdGhlciB0eXBlIG9mIGxpY2Vuc2UuIFRoZSByZXF1aXJlbWVudCBmb3IgZm9udHMgdG8gcmVtYWluIHVuZGVyIHRoaXMgbGljZW5zZSBkb2VzIG5vdCBhcHBseSB0byBhbnkgZG9jdW1lbnQgY3JlYXRlZCB1c2luZyB0aGUgZm9udHMgb3IgdGhlaXIgZGVyaXZhdGl2ZXMuIERFRklOSVRJT05TICJGb250IFNvZnR3YXJlIiByZWZlcnMgdG8gdGhlIHNldCBvZiBmaWxlcyByZWxlYXNlZCBieSB0aGUgQ29weXJpZ2h0IEhvbGRlclwoc1wpIHVuZGVyIHRoaXMgbGljZW5zZSBhbmQgY2xlYXJseSBtYXJrZWQgYXMgc3VjaC4gVGhpcyBtYXkgaW5jbHVkZSBzb3VyY2UgZmlsZXMsIGJ1aWxkIHNjcmlwdHMgYW5kIGRvY3VtZW50YXRpb24uICJSZXNlcnZlZCBGb250IE5hbWUiIHJlZmVycyB0byBhbnkgbmFtZXMgc3BlY2lmaWVkIGFzIHN1Y2ggYWZ0ZXIgdGhlIGNvcHlyaWdodCBzdGF0ZW1lbnRcKHNcKS4gIk9yaWdpbmFsIFZlcnNpb24iIHJlZmVycyB0byB0aGUgY29sbGVjdGlvbiBvZiBGb250IFNvZnR3YXJlIGNvbXBvbmVudHMgYXMgZGlzdHJpYnV0ZWQgYnkgdGhlIENvcHlyaWdodCBIb2xkZXJcKHNcKS4gIk1vZGlmaWVkIFZlcnNpb24iIHJlZmVycyB0byBhbnkgZGVyaXZhdGl2ZSBtYWRlIGJ5IGFkZGluZyB0bywgZGVsZXRpbmcsIG9yIHN1YnN0aXR1dGluZyAtLSBpbiBwYXJ0IG9yIGluIHdob2xlIC0tIGFueSBvZiB0aGUgY29tcG9uZW50cyBvZiB0aGUgT3JpZ2luYWwgVmVyc2lvbiwgYnkgY2hhbmdpbmcgZm9ybWF0cyBvciBieSBwb3J0aW5nIHRoZSBGb250IFNvZnR3YXJlIHRvIGEgbmV3IGVudmlyb25tZW50LiAiQXV0aG9yIiByZWZlcnMgdG8gYW55IGRlc2lnbmVyLCBlbmdpbmVlciwgcHJvZ3JhbW1lciwgdGVjaG5pY2FsIHdyaXRlciBvciBvdGhlciBwZXJzb24gd2hvIGNvbnRyaWJ1dGVkIHRvIHRoZSBGb250IFNvZnR3YXJlLiBQRVJNSVNTSU9OICYgQ09ORElUSU9OUyBQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYSBjb3B5IG9mIHRoZSBGb250IFNvZnR3YXJlLCB0byB1c2UsIHN0dWR5LCBjb3B5LCBtZXJnZSwgZW1iZWQsIG1vZGlmeSwgcmVkaXN0cmlidXRlLCBhbmQgc2VsbCBtb2RpZmllZCBhbmQgdW5tb2RpZmllZCBjb3BpZXMgb2YgdGhlIEZvbnQgU29mdHdhcmUsIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOiAxXCkgTmVpdGhlciB0aGUgRm9udCBTb2Z0d2FyZSBub3IgYW55IG9mIGl0cyBpbmRpdmlkdWFsIGNvbXBvbmVudHMsIGluIE9yaWdpbmFsIG9yIE1vZGlmaWVkIFZlcnNpb25zLCBtYXkgYmUgc29sZCBieSBpdHNlbGYuIDJcKSBPcmlnaW5hbCBvciBNb2RpZmllZCBWZXJzaW9ucyBvZiB0aGUgRm9udCBTb2Z0d2FyZSBtYXkgYmUgYnVuZGxlZCwgcmVkaXN0cmlidXRlZCBhbmQvb3Igc29sZCB3aXRoIGFueSBzb2Z0d2FyZSwgcHJvdmlkZWQgdGhhdCBlYWNoIGNvcHkgY29udGFpbnMgdGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgbGljZW5zZS4gVGhlc2UgY2FuIGJlIGluY2x1ZGVkIGVpdGhlciBhcyBzdGFuZC1hbG9uZSB0ZXh0IGZpbGVzLCBodW1hbi1yZWFkYWJsZSBoZWFkZXJzIG9yIGluIHRoZSBhcHByb3ByaWF0ZSBtYWNoaW5lLXJlYWRhYmxlIG1ldGFkYXRhIGZpZWxkcyB3aXRoaW4gdGV4dCBvciBiaW5hcnkgZmlsZXMgYXMgbG9uZyBhcyB0aG9zZSBmaWVsZHMgY2FuIGJlIGVhc2lseSB2aWV3ZWQgYnkgdGhlIHVzZXIuIDNcKSBObyBNb2RpZmllZCBWZXJzaW9uIG9mIHRoZSBGb250IFNvZnR3YXJlIG1heSB1c2UgdGhlIFJlc2VydmVkIEZvbnQgTmFtZVwoc1wpIHVubGVzcyBleHBsaWNpdCB3cml0dGVuIHBlcm1pc3Npb24gaXMgZ3JhbnRlZCBieSB0aGUgY29ycmVzcG9uZGluZyBDb3B5cmlnaHQgSG9sZGVyLiBUaGlzIHJlc3RyaWN0aW9uIG9ubHkgYXBwbGllcyB0byB0aGUgcHJpbWFyeSBmb250IG5hbWUgYXMgcHJlc2VudGVkIHRvIHRoZSB1c2Vycy4gNFwpIFRoZSBuYW1lXChzXCkgb2YgdGhlIENvcHlyaWdodCBIb2xkZXJcKHNcKSBvciB0aGUgQXV0aG9yXChzXCkgb2YgdGhlIEZvbnQgU29mdHdhcmUgc2hhbGwgbm90IGJlIHVzZWQgdG8gcHJvbW90ZSwgZW5kb3JzZSBvciBhZHZlcnRpc2UgYW55IE1vZGlmaWVkIFZlcnNpb24sIGV4Y2VwdCB0byBhY2tub3dsZWRnZSB0aGUgY29udHJpYnV0aW9uXChzXCkgb2YgdGhlIENvcHlyaWdodCBIb2xkZXJcKHNcKSBhbmQgdGhlIEF1dGhvclwoc1wpIG9yIHdpdGggdGhlaXIgZXhwbGljaXQgd3JpdHRlbiBwZXJtaXNzaW9uLiA1XCkgVGhlIEZvbnQgU29mdHdhcmUsIG1vZGlmaWVkIG9yIHVubW9kaWZpZWQsIGluIHBhcnQgb3IgaW4gd2hvbGUsIG11c3QgYmUgZGlzdHJpYnV0ZWQgZW50aXJlbHkgdW5kZXIgdGhpcyBsaWNlbnNlLCBhbmQgbXVzdCBub3QgYmUgZGlzdHJpYnV0ZWQgdW5kZXIgYW55IG90aGVyIGxpY2Vuc2UuIFRoZSByZXF1aXJlbWVudCBmb3IgZm9udHMgdG8gcmVtYWluIHVuZGVyIHRoaXMgbGljZW5zZSBkb2VzIG5vdCBhcHBseSB0byBhbnkgZG9jdW1lbnQgY3JlYXRlZCB1c2luZyB0aGUgRm9udCBTb2Z0d2FyZS4gVEVSTUlOQVRJT04gVGhpcyBsaWNlbnNlIGJlY29tZXMgbnVsbCBhbmQgdm9pZCBpZiBhbnkgb2YgdGhlIGFib3ZlIGNvbmRpdGlvbnMgYXJlIG5vdCBtZXQuIERJU0NMQUlNRVIgVEhFIEZPTlQgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUiBJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIEFOWSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVCBPRiBDT1BZUklHSFQsIFBBVEVOVCwgVFJBREVNQVJLLCBPUiBPVEhFUiBSSUdIVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIENPUFlSSUdIVCBIT0xERVIgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIgTElBQklMSVRZLCBJTkNMVURJTkcgQU5ZIEdFTkVSQUwsIFNQRUNJQUwsIElORElSRUNULCBJTkNJREVOVEFMLCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMsIFdIRVRIRVIg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"; diff --git a/crates/epiphany-render-svg/src/lib.rs b/crates/epiphany-render-svg/src/lib.rs index ddfb82d..c9127c5 100644 --- a/crates/epiphany-render-svg/src/lib.rs +++ b/crates/epiphany-render-svg/src/lib.rs @@ -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** -//! ``. 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 ``s by default, or `` 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 +//! ``s — no font dependency, byte-golden-locked, the pixel-verified +//! reference (QUICKSTART, Agent I, recommendation). +//! * [`GlyphMode::EmbeddedFont`] references glyphs by SMuFL codepoint via a +//! `` 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; diff --git a/crates/epiphany-render-svg/src/outline.rs b/crates/epiphany-render-svg/src/outline.rs index cf889f8..055172c 100644 --- a/crates/epiphany-render-svg/src/outline.rs +++ b/crates/epiphany-render-svg/src/outline.rs @@ -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 { 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 { + fn val(c: u8) -> Option { + 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 { + 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 { + 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 { + 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 { diff --git a/crates/epiphany-render-svg/src/svg.rs b/crates/epiphany-render-svg/src/svg.rs index 47a1b4e..8dc6dc4 100644 --- a/crates/epiphany-render-svg/src/svg.rs +++ b/crates/epiphany-render-svg/src/svg.rs @@ -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 ``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 `` 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, - /// `` elements emitted (glyphs drawn from a bundled outline). + /// `` elements emitted (glyphs drawn from a bundled outline, the + /// default [`GlyphMode::PathOutline`]). pub path_count: usize, + /// `` elements emitted (glyphs set in the embedded font, the + /// [`GlyphMode::EmbeddedFont`] mode). Zero in the default path mode. + pub text_count: usize, /// Fallback `` elements emitted (glyphs with no bundled outline). pub fallback_rect_count: usize, /// `` 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, - " ", + " ", + glyph_note(options.glyph_mode), provenance_note(options.emit_provenance), ); + // In embedded-font mode, declare the Bravura subset once via `@font-face`; the + // `` 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, + " ", + 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 `` + // (the self-contained default) or a `` 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, - " ", + format!( + "", 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!( + "&#x{cp:X};", + ) + }), + }; + 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!( "\n\ \n\ - \x20\x20\n\ + \x20\x20\n\ \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 `` (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("`; 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] diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg index 5872acf..2c3ac94 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg index 7c832af..5633961 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg index e525062..aa726ff 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg index 114502a..35e8d5d 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tools/extract_bravura_outlines.py b/crates/epiphany-render-svg/tools/extract_bravura_outlines.py index 1b2c063..7f36635 100644 --- a/crates/epiphany-render-svg/tools/extract_bravura_outlines.py +++ b/crates/epiphany-render-svg/tools/extract_bravura_outlines.py @@ -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()