Editor T4-pre W2: the glyph outlines become a shared typed seam
A canvas tessellator needs typed vector paths, and the outlines were private to render-svg as SVG `d` strings. They move to a new epiphany-glyphs crate, which implements layout-ir's GlyphCatalog with a real render_data. The seam was already designed and merely unpopulated: PathCommand, GlyphRenderData, and GlyphCatalog::render_data all existed, and BravuraCatalog returned None by deliberate documented honesty -- reporting Some would claim render data that does not exist. Like W1, this packet fills a structure rather than building one. Typed paths are parsed from the `d` strings rather than regenerated, because Bravura.otf is not vendored -- tools/ carries only the extractor and its OFL notice, and the generated header pins source hashes verified at extraction time. Equivalence is therefore proven rather than assumed: parse every bundled glyph, re-emit, compare byte-for-byte. All thirty-seven round-trip exactly, so the sanctioned coordinate-comparison fallback was not needed. The real grammar is wider than the contract's summary claimed -- absolute V/H lineto shorthand appears in twenty-three of thirty-seven glyphs, and numbers carry nought to three fractional digits with trailing zeros stripped, never a fixed four. The parser lowers V/H to LineTo and the emitter reconstructs the shorthand from geometry. Nothing rendered moves. The outline data is byte-identical to its previous home (the only edit is BravuraOutline's visibility), render-svg keeps emitting the stored strings and re-exports bundled_glyph_count and smufl_codepoint unchanged, and BRAVURA_METRICS is untouched -- it feeds metrics_hash and hence GlyphCatalogIdentity, which is inside the resolved layout's canonical bytes. Verified: all eight reference fixtures produce byte-identical canonical layouts, and all five GUI goldens are unchanged. The cross-table test locks what glyph.rs asserted only in prose: every glyph's declared metrics bbox contains its drawn ink, re-derived through true cubic-bezier extrema rather than control points. Worst-case deviation across the whole table is exactly zero, at noteheadBlack's left edge. Stated honestly, this is a rounding-chain lock rather than an independent cross-check -- the same extractor emits both tables and deliberately rounds the metric box outward so it always contains the ink -- but it catches a unit-conversion error, a regeneration mismatch, or a future inward rounding, none of which had a test before. Five of six mutations killed, all coordinator-re-verified independently. The sixth is an honest survivor and a defect in the contract rather than the work: it asked for proof that re-emitting from the typed form breaks the goldens, which the round-trip pin makes a no-op. The goldens were re-proven live a different way -- perturbing one glyph's stored path by one digit flips one pixel of 279,992 and fails all five. Gate: 35 suites / 1351 tests / 0 failed, conformance 9/9 with golden-gate and 8/8 without, requirement labels 6/6, clippy 0, cargo tree showing exactly one dependency and no third-party crate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4e30fe8475
commit
24f8c8099a
|
|
@ -1244,6 +1244,13 @@ dependencies = [
|
|||
"epiphany-testkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epiphany-glyphs"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"epiphany-layout-ir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epiphany-layout-ir"
|
||||
version = "0.0.0"
|
||||
|
|
@ -1269,6 +1276,7 @@ dependencies = [
|
|||
"epiphany-core",
|
||||
"epiphany-determinism",
|
||||
"epiphany-engrave",
|
||||
"epiphany-glyphs",
|
||||
"epiphany-layout-ir",
|
||||
"epiphany-testkit",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ members = [
|
|||
"crates/epiphany-ops",
|
||||
"crates/epiphany-textproj",
|
||||
"crates/epiphany-layout-ir",
|
||||
"crates/epiphany-glyphs",
|
||||
"crates/epiphany-engrave",
|
||||
"crates/epiphany-render-svg",
|
||||
"crates/epiphany-editor-core",
|
||||
|
|
@ -58,6 +59,14 @@ epiphany-textproj = { path = "crates/epiphany-textproj" }
|
|||
# (v0 acceptance criterion 6), so its path is declared here alongside the other
|
||||
# intra-workspace crates.
|
||||
epiphany-layout-ir = { path = "crates/epiphany-layout-ir" }
|
||||
# Editor T4-pre W2: the shared typed glyph-asset seam. Bundled genuine Bravura
|
||||
# SMuFL outline data (extracted from the official OFL Bravura.otf) plus a real
|
||||
# GlyphCatalog whose render_data returns genuine parsed outlines — depends on
|
||||
# epiphany-layout-ir only (no third-party dependency, no build.rs, MSRV-clean).
|
||||
# epiphany-render-svg depends on it (moved out of that crate, which used to
|
||||
# hold the table privately); a future canvas-tessellating editor-app renderer
|
||||
# is why it is its own crate rather than a layout-ir module.
|
||||
epiphany-glyphs = { path = "crates/epiphany-glyphs" }
|
||||
# Agent I's Phase-2 visible-slice crates: epiphany-engrave is the real
|
||||
# constraint solver (Chapter 9 Minimal tier — in progress) consuming the
|
||||
# ConstrainedLayoutIR; epiphany-render-svg is the SVG renderer behind the
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "epiphany-glyphs"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Editor T4-pre W2: the shared typed glyph-asset seam. Bundled genuine Bravura SMuFL outline data (SVG path `d` strings extracted from the official OFL Bravura.otf) plus a real GlyphCatalog whose render_data returns parsed typed outlines, for any renderer or app that depends on epiphany-layout-ir. Dependency-free and MSRV-clean by design."
|
||||
|
||||
[dependencies]
|
||||
# The typed PathCommand / GlyphCatalog / GlyphRenderData vocabulary this crate
|
||||
# populates, and BRAVURA_METRICS, which stays in epiphany-layout-ir (W2 pin 3)
|
||||
# — this crate never edits or re-derives it.
|
||||
epiphany-layout-ir.workspace = true
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# epiphany-glyphs — decisions
|
||||
|
||||
Editor T4-pre W2 (`spec/CONTRACT_EDITOR_T4PRE_W2_GLYPHS.md`): the shared
|
||||
typed glyph-asset seam a canvas tessellator needs, populated on top of the
|
||||
already-designed `layout-ir` interface (`PathCommand`, `GlyphRenderData`,
|
||||
`GlyphCatalog::render_data`) rather than designing a new one.
|
||||
|
||||
## Scope and status
|
||||
|
||||
New crate. Owns the bundled Bravura outline data (`src/outlines_generated.rs`,
|
||||
moved from `epiphany-render-svg`, which held it `pub(crate)` and private to
|
||||
itself), the extractor tool and its OFL license (`tools/`), an in-crate
|
||||
grammar-specific parser from the bundled SVG path `d` strings into
|
||||
`epiphany_layout_ir::PathCommand` (`src/path.rs`, private — reachable only
|
||||
through the catalog), and `BravuraGlyphCatalog` (`src/catalog.rs`): a real
|
||||
`GlyphCatalog` whose `render_data` returns genuine outlines, unlike
|
||||
`epiphany_layout_ir::BravuraCatalog`, which is metrics-only and honestly
|
||||
returns `None`.
|
||||
|
||||
`BRAVURA_METRICS` does not move here and is not edited (contract pin 3): it
|
||||
stays in `epiphany-layout-ir`, and this crate depends on it read-only. No
|
||||
cycle: `epiphany-glyphs` → `epiphany-layout-ir`, never the reverse.
|
||||
|
||||
## The grammar the contract described vs. the grammar the generator emits
|
||||
|
||||
The contract's "verified starting point" states the bundled `d` strings use
|
||||
"absolute `M`/`L`/`C`/`Z`, decimal coordinates, 4 decimal places". Verified
|
||||
against all 37 bundled glyphs before writing the parser (not assumed), the
|
||||
real grammar is wider on both counts, traced to
|
||||
`tools/extract_bravura_outlines.py`'s use of
|
||||
`fontTools.pens.svgPathPen.SVGPathPen`, whose default `optimizeCommands`
|
||||
behavior the extractor never disables:
|
||||
|
||||
* **`V` (vertical-only) and `H` (horizontal-only) lineto shorthand** are also
|
||||
emitted, absolute, whenever a lineto's target shares an axis with the
|
||||
current point — 23 of the 37 bundled glyphs use at least one. `PathCommand`
|
||||
(the already-designed shared type) has no shorthand variant, so `parse_d`
|
||||
lowers `V`/`H` to `PathCommand::LineTo`; `emit_d` reconstructs the
|
||||
shorthand byte-for-byte purely from geometry — comparing the lineto's
|
||||
target to the current point (`V` when only *x* is unchanged, `H` when only
|
||||
*y* is, `L` otherwise) — which is exactly what the round-trip test proves
|
||||
for every bundled glyph. No information is lost: the typed form and the
|
||||
stored string are geometrically and (after this reconstruction)
|
||||
byte-for-byte equivalent.
|
||||
* **Coordinates are rounded to *at most* 4 decimals**, with trailing zeros
|
||||
and a bare `-0` stripped by the generator's own `round_d`
|
||||
(`tools/extract_bravura_outlines.py:180-185`: `f"{v:.4f}".rstrip('0').rstrip('.')`,
|
||||
normalising `""`/`"-0"` to `"0"`). Printed precision therefore varies per
|
||||
number (0-3 fractional digits observed in the bundled data; 4 is the
|
||||
ceiling, not the width). `emit_d`'s number formatter reproduces this exact
|
||||
rule.
|
||||
* Every command in the observed grammar carries exactly one point/coordinate
|
||||
group (`M`, `L`, `V`, `H` one; `C` three points; `Z` none) — the generator
|
||||
never merges consecutive same-type commands into a multi-coordinate group
|
||||
(SVG allows this generally; the fontTools pen never emits it), so the
|
||||
parser does not implement that generality either.
|
||||
|
||||
This is a correction to the contract's stated facts, not a deviation from its
|
||||
design pins: pin 4 ("an in-crate parser … covers exactly the grammar the
|
||||
generator emits") is satisfied by parsing the *real* grammar, which is what
|
||||
"exactly the grammar the generator emits" requires once the grammar is
|
||||
actually read from the data rather than assumed from the contract's summary.
|
||||
|
||||
## Round-trip proof, not the sanctioned fallback (pin 5)
|
||||
|
||||
Pin 5 sanctions a weaker fallback — comparing parsed coordinate sequences as
|
||||
exact `f32` — if the generator's number formatting cannot be reproduced
|
||||
exactly. It can: `emit_d`'s formatter is a direct transcription of
|
||||
`round_d`'s Python (round to 4, strip trailing zeros/dot, normalise `-0`),
|
||||
verified by an actual byte-for-byte `assert_eq!` against every bundled
|
||||
glyph's stored `d` string (`path.rs`'s
|
||||
`every_bundled_glyph_round_trips_byte_for_byte`, the load-bearing test). The
|
||||
fallback was not needed and was not taken.
|
||||
|
||||
## `render_data` caching (pin 6)
|
||||
|
||||
`BravuraGlyphCatalog::render_data` looks up a `std::sync::OnceLock`-cached
|
||||
`BTreeMap<&'static str, Vec<PathCommand>>`, built once by parsing every
|
||||
bundled outline's `d` string on first use. `parse_d` is pure, so the cached
|
||||
value a lookup clones is exactly what a fresh parse of the same bundled
|
||||
string would produce on every call — caching changes only *when* the parse
|
||||
work happens, never *what* it returns. No interior mutability leaks into the
|
||||
result; two calls for the same name compare equal (`GlyphRenderData:
|
||||
PartialEq`), asserted directly.
|
||||
|
||||
## `epiphany-render-svg`'s byte-neutrality (pin 2, pin 5)
|
||||
|
||||
`render-svg`'s `outline()`/`bundled_glyph_count()`/`smufl_codepoint()` now
|
||||
delegate to this crate (`src/outline.rs` in each crate — `render-svg`'s copy
|
||||
is a thin re-export/delegation, this crate's is the real lookup). Critically,
|
||||
`render-svg`'s SVG emission (`svg.rs`'s `GlyphMode::PathOutline` arm) still
|
||||
reads `outline(name).path` — the *stored* string — directly into the `<path
|
||||
d="…">` attribute. It never routes through `parse_d`/`emit_d`; those exist
|
||||
only for the round-trip proof and are not reachable from `render-svg` at all
|
||||
(`path` is a private module here, and its functions are `pub(crate)`, not
|
||||
exported). This is why the byte-neutrality probe (every SVG golden, every
|
||||
`ResolvedLayoutIR::canonical_bytes()` reference-suite fixture) is unaffected
|
||||
by this packet's existence — verified before and after against the base
|
||||
commit, `diff -r` empty.
|
||||
|
||||
## Why a new crate and not a `layout-ir` module
|
||||
|
||||
The contract left this open (§CONTRACT_EDITOR_T4PRE_IR.md W2 framing: "the
|
||||
open decision W2's contract resolves is the home … judged on dependency
|
||||
weight and the MSRV/CI job structure"), and `CONTRACT_EDITOR_T4PRE_W2_GLYPHS.md`
|
||||
pin 1 settles it: a new crate. Reasons that held once the data was in hand:
|
||||
|
||||
* `layout-ir` is deliberately data-light — it defines the `GlyphCatalog`
|
||||
*interface* and a small in-tree metrics slice, not asset payloads; the
|
||||
bundled outline table is ~37 KB of generated Rust literal that has nothing
|
||||
to do with the constraint-solver interface `layout-ir` exists to define.
|
||||
* A separate crate keeps the "moved verbatim, byte-for-byte identical output"
|
||||
claim easy to audit: the outline table's home changed, its content and the
|
||||
bytes it produces did not.
|
||||
* It matches the existing pattern (`epiphany-render-svg` already held this
|
||||
exact data privately) — moving it sideways into a shared crate is a
|
||||
smaller, more reviewable diff than folding it into `layout-ir` and then
|
||||
re-exporting it back out for `render-svg`'s and a future canvas renderer's
|
||||
use.
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
//! The real glyph-render catalog (Editor T4-pre W2 pin 1). Metrics delegate
|
||||
//! to `epiphany_layout_ir::BRAVURA_METRICS`, which stays in `epiphany-layout-ir`
|
||||
//! unmoved and unchanged (pin 3) — this crate never edits or re-derives it.
|
||||
//! `render_data` returns genuine outlines, parsed once from this crate's
|
||||
//! bundled `d` strings: unlike `epiphany_layout_ir::BravuraCatalog` (which
|
||||
//! bundles no render data and honestly returns `None` for every glyph), this
|
||||
//! catalog can answer for real because the outline data lives here.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use epiphany_layout_ir::{
|
||||
metrics, metrics_hash_for, GlyphCatalog, GlyphCatalogIdentity, GlyphMetric, GlyphRenderData,
|
||||
PathCommand, SmuflVersion,
|
||||
};
|
||||
|
||||
use crate::outlines_generated::BRAVURA_OUTLINES;
|
||||
use crate::path::parse_d;
|
||||
|
||||
/// The bundled Bravura catalog with genuine render data (Editor T4-pre W2).
|
||||
pub struct BravuraGlyphCatalog;
|
||||
|
||||
/// The lazily-built, cached name -> parsed-outline table (pin 6: caching is
|
||||
/// permitted as long as it does not change results and two calls agree).
|
||||
/// Parsing happens once per process; [`parse_d`] is pure, so the cached
|
||||
/// `Vec<PathCommand>` a lookup clones is exactly what a fresh parse of the
|
||||
/// same bundled string would produce — caching changes only *when* the work
|
||||
/// happens, never *what* it returns.
|
||||
fn render_data_table() -> &'static BTreeMap<&'static str, Vec<PathCommand>> {
|
||||
static TABLE: OnceLock<BTreeMap<&'static str, Vec<PathCommand>>> = OnceLock::new();
|
||||
TABLE.get_or_init(|| {
|
||||
BRAVURA_OUTLINES
|
||||
.iter()
|
||||
.map(|o| (o.name, parse_d(o.path)))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
impl GlyphCatalog for BravuraGlyphCatalog {
|
||||
fn metrics(&self, name: &str) -> Option<&GlyphMetric> {
|
||||
metrics(name)
|
||||
}
|
||||
|
||||
fn render_data(&self, name: &str) -> Option<GlyphRenderData> {
|
||||
render_data_table()
|
||||
.get(name)
|
||||
.map(|outline| GlyphRenderData {
|
||||
outline: outline.clone(),
|
||||
bitmap: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn smufl_version(&self) -> SmuflVersion {
|
||||
SmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version")
|
||||
}
|
||||
|
||||
fn identity(&self, consulted: &[&str]) -> GlyphCatalogIdentity {
|
||||
GlyphCatalogIdentity {
|
||||
metrics_hash: metrics_hash_for(consulted.iter().copied()),
|
||||
..GlyphCatalogIdentity::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::extent::outline_extent;
|
||||
use epiphany_layout_ir::BRAVURA_METRICS;
|
||||
|
||||
/// (g3) Every pipeline glyph (the `BRAVURA_METRICS` name set) resolves
|
||||
/// through the new catalog's `render_data` — mirroring
|
||||
/// `epiphany-render-svg`'s existing `every_pipeline_glyph_has_a_bundled_outline`,
|
||||
/// but against the real `GlyphCatalog` seam this packet populates rather
|
||||
/// than the raw outline table directly.
|
||||
#[test]
|
||||
fn every_pipeline_glyph_has_render_data() {
|
||||
let catalog = BravuraGlyphCatalog;
|
||||
for m in BRAVURA_METRICS {
|
||||
let data = catalog.render_data(m.name.as_ref());
|
||||
assert!(
|
||||
data.is_some(),
|
||||
"no render data for pipeline glyph {}",
|
||||
m.name
|
||||
);
|
||||
assert!(
|
||||
!data.unwrap().outline.is_empty(),
|
||||
"{}: render data has an empty outline",
|
||||
m.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// (g6, pin 6) Two calls return equal data — the cache must not change
|
||||
/// results, and `render_data` is deterministic and side-effect-free.
|
||||
#[test]
|
||||
fn render_data_is_deterministic_across_calls() {
|
||||
let catalog = BravuraGlyphCatalog;
|
||||
for m in BRAVURA_METRICS {
|
||||
let a = catalog.render_data(m.name.as_ref());
|
||||
let b = catalog.render_data(m.name.as_ref());
|
||||
assert_eq!(a, b, "{}: two render_data calls disagreed", m.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// (g4) Outline ink fits the declared metrics bbox: `layout-ir/src/glyph.rs:107-109`
|
||||
/// claims the metrics and outlines come from the same font release "so
|
||||
/// reserved advances/bboxes and the drawn ink agree" — this is the test
|
||||
/// that actually checks it, computed from the *typed* outline geometry
|
||||
/// (real cubic-bezier extrema, not just control points or the
|
||||
/// separately-stored `bbox` field), against `BRAVURA_METRICS`'s bounding
|
||||
/// box (converted from 1/1024-staff-space integers to `f32` staff
|
||||
/// spaces).
|
||||
///
|
||||
/// Tolerance: the `d` string's coordinates are individually rounded to
|
||||
/// at most 4 decimals before this test re-derives extrema from them, and
|
||||
/// `BRAVURA_METRICS`'s integer bbox is independently floor/ceil-rounded
|
||||
/// to the *coarser* 1/1024 (~0.000977) staff-space grid from the
|
||||
/// generator's own (also 4-decimal-rounded) outline bbox — two
|
||||
/// independent roundings on each side of the comparison. `TOLERANCE`
|
||||
/// generously covers both: it is asserted, not tuned after the fact, and
|
||||
/// the test additionally reports the actual worst-case deviation so a
|
||||
/// real violation cannot hide inside a widened tolerance.
|
||||
#[test]
|
||||
fn outline_ink_fits_the_declared_metrics_bbox() {
|
||||
const TOLERANCE: f32 = 0.005;
|
||||
let catalog = BravuraGlyphCatalog;
|
||||
let mut worst: f32 = f32::NEG_INFINITY;
|
||||
let mut worst_glyph = "";
|
||||
let mut worst_side = "";
|
||||
for m in BRAVURA_METRICS {
|
||||
let data = catalog
|
||||
.render_data(m.name.as_ref())
|
||||
.unwrap_or_else(|| panic!("{}: no render data", m.name));
|
||||
let [ox_min, oy_min, ox_max, oy_max] = outline_extent(&data.outline)
|
||||
.unwrap_or_else(|| panic!("{}: empty outline", m.name));
|
||||
let mb = m.bounding_box();
|
||||
// Positive = the outline pokes out past that side of the metric
|
||||
// box by this many staff spaces; negative = margin to spare.
|
||||
let overflow = [
|
||||
("left", mb.left.0 - ox_min),
|
||||
("bottom", mb.bottom.0 - oy_min),
|
||||
("right", ox_max - mb.right.0),
|
||||
("top", oy_max - mb.top.0),
|
||||
];
|
||||
for (side, amount) in overflow {
|
||||
if amount > worst {
|
||||
worst = amount;
|
||||
worst_glyph = m.name.as_ref();
|
||||
worst_side = side;
|
||||
}
|
||||
assert!(
|
||||
amount <= TOLERANCE,
|
||||
"{}: outline overflows the metrics bbox on the {} side by {} \
|
||||
staff spaces (tolerance {}) — outline extent [{ox_min}, {oy_min}, \
|
||||
{ox_max}, {oy_max}], metrics bbox [{}, {}, {}, {}]",
|
||||
m.name,
|
||||
side,
|
||||
amount,
|
||||
TOLERANCE,
|
||||
mb.left.0,
|
||||
mb.bottom.0,
|
||||
mb.right.0,
|
||||
mb.top.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Reported per the contract (g4): the real worst-case deviation
|
||||
// across every bundled glyph, not just "within tolerance". Printed
|
||||
// to 8 decimals (well past the data's own 4-decimal precision) so a
|
||||
// near-zero value is not mistaken for exactly zero.
|
||||
eprintln!(
|
||||
"g4 worst-case deviation: {worst:.8} staff spaces ({worst_glyph}, {worst_side} side; \
|
||||
negative = margin to spare, positive = overflow)"
|
||||
);
|
||||
assert!(
|
||||
worst.is_finite(),
|
||||
"worst-case deviation must be a real computed number"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
//! The tight axis-aligned bounding box of a typed outline (Editor T4-pre W2
|
||||
//! test g4): used only to prove the bundled outlines' drawn ink is contained
|
||||
//! by their declared `BRAVURA_METRICS` bounding box — the cross-table
|
||||
//! consistency `layout-ir/src/glyph.rs:107-109` asserts in prose ("the
|
||||
//! reserved advances/bboxes and the drawn ink agree") but no test in the
|
||||
//! tree checked directly from parsed outline geometry before this packet.
|
||||
|
||||
use epiphany_layout_ir::PathCommand;
|
||||
|
||||
/// The exact bounding box `[left, bottom, right, top]` of the ink a sequence
|
||||
/// of typed path commands draws, computed from real cubic-bezier extrema —
|
||||
/// not just control points, which routinely lie outside a curve's own tight
|
||||
/// bounds — matching what `tools/extract_bravura_outlines.py`'s
|
||||
/// `fontTools.pens.boundsPen.BoundsPen` computes at generation time (it
|
||||
/// overrides `curveToOne` with the same real-extrema calculation, unlike its
|
||||
/// `ControlBoundsPen` base class). Returns `None` for an empty command list.
|
||||
pub(crate) fn outline_extent(commands: &[PathCommand]) -> Option<[f32; 4]> {
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
let mut max_y = f32::NEG_INFINITY;
|
||||
let mut cur = (0.0f32, 0.0f32);
|
||||
let mut any = false;
|
||||
for cmd in commands {
|
||||
match cmd {
|
||||
PathCommand::MoveTo(p) | PathCommand::LineTo(p) => {
|
||||
let (x, y) = (p.x.0, p.y.0);
|
||||
min_x = min_x.min(x);
|
||||
min_y = min_y.min(y);
|
||||
max_x = max_x.max(x);
|
||||
max_y = max_y.max(y);
|
||||
cur = (x, y);
|
||||
any = true;
|
||||
}
|
||||
PathCommand::CurveTo {
|
||||
control1,
|
||||
control2,
|
||||
to,
|
||||
} => {
|
||||
let (x0, y0) = cur;
|
||||
let (lo_x, hi_x) = cubic_extrema_1d(x0, control1.x.0, control2.x.0, to.x.0);
|
||||
let (lo_y, hi_y) = cubic_extrema_1d(y0, control1.y.0, control2.y.0, to.y.0);
|
||||
min_x = min_x.min(lo_x);
|
||||
max_x = max_x.max(hi_x);
|
||||
min_y = min_y.min(lo_y);
|
||||
max_y = max_y.max(hi_y);
|
||||
any = true;
|
||||
cur = (to.x.0, to.y.0);
|
||||
}
|
||||
PathCommand::Close => {}
|
||||
}
|
||||
}
|
||||
if any {
|
||||
Some([min_x, min_y, max_x, max_y])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The `[min, max]` extent of a 1-D cubic bezier `p0..p3` over `t in [0,1]`:
|
||||
/// the two endpoints plus any interior critical point of the derivative (a
|
||||
/// root of the quadratic `B'(t)/3 = a*t^2 + b*t + c`).
|
||||
fn cubic_extrema_1d(p0: f32, p1: f32, p2: f32, p3: f32) -> (f32, f32) {
|
||||
let mut lo = p0.min(p3);
|
||||
let mut hi = p0.max(p3);
|
||||
let d0 = p1 - p0;
|
||||
let d1 = p2 - p1;
|
||||
let d2 = p3 - p2;
|
||||
let a = d0 - 2.0 * d1 + d2;
|
||||
let b = 2.0 * (d1 - d0);
|
||||
let c = d0;
|
||||
let mut consider = |t: f32| {
|
||||
if (0.0..=1.0).contains(&t) {
|
||||
let mt = 1.0 - t;
|
||||
let v =
|
||||
mt * mt * mt * p0 + 3.0 * mt * mt * t * p1 + 3.0 * mt * t * t * p2 + t * t * t * p3;
|
||||
lo = lo.min(v);
|
||||
hi = hi.max(v);
|
||||
}
|
||||
};
|
||||
if a.abs() < 1e-12 {
|
||||
if b.abs() > 1e-12 {
|
||||
consider(-c / b);
|
||||
}
|
||||
} else {
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc >= 0.0 {
|
||||
let sqrt_disc = disc.sqrt();
|
||||
consider((-b + sqrt_disc) / (2.0 * a));
|
||||
consider((-b - sqrt_disc) / (2.0 * a));
|
||||
}
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_layout_ir::Point;
|
||||
|
||||
#[test]
|
||||
fn straight_line_extent_is_its_endpoints() {
|
||||
let cmds = vec![
|
||||
PathCommand::MoveTo(Point::new(0.0, 0.0)),
|
||||
PathCommand::LineTo(Point::new(2.0, 3.0)),
|
||||
];
|
||||
assert_eq!(outline_extent(&cmds), Some([0.0, 0.0, 2.0, 3.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_commands_have_no_extent() {
|
||||
assert_eq!(outline_extent(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curve_extrema_are_found_beyond_the_endpoints() {
|
||||
// The classic "hump" cubic (P0=(0,0), P1=(0,1), P2=(1,1), P3=(1,0)):
|
||||
// its peak y at t=0.5 is 0.75, well past either endpoint's y=0 — a
|
||||
// control-point-only (or endpoint-only) bound would miss it.
|
||||
let cmds = vec![
|
||||
PathCommand::MoveTo(Point::new(0.0, 0.0)),
|
||||
PathCommand::CurveTo {
|
||||
control1: Point::new(0.0, 1.0),
|
||||
control2: Point::new(1.0, 1.0),
|
||||
to: Point::new(1.0, 0.0),
|
||||
},
|
||||
];
|
||||
let ext = outline_extent(&cmds).unwrap();
|
||||
assert!(
|
||||
(ext[3] - 0.75).abs() < 1e-5,
|
||||
"expected top ~0.75, got {}",
|
||||
ext[3]
|
||||
);
|
||||
assert_eq!(ext[0], 0.0, "left stays at the endpoints' x");
|
||||
assert_eq!(ext[2], 1.0, "right stays at the endpoints' x");
|
||||
assert_eq!(
|
||||
ext[1], 0.0,
|
||||
"bottom is the (only) minimum, at both endpoints"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
#![forbid(unsafe_code)]
|
||||
//! # epiphany-glyphs
|
||||
//!
|
||||
//! The shared typed glyph-asset seam (Editor T4-pre W2,
|
||||
//! `spec/PLAN_EDITOR_APP.md` §3.7 / Ruling A: "a canvas tessellator needs
|
||||
//! typed vector paths from a crate both the renderer and the app can depend
|
||||
//! on"). This crate owns:
|
||||
//!
|
||||
//! * the bundled genuine Bravura SMuFL outline data ([`outline`],
|
||||
//! [`bundled_glyph_count`], [`smufl_codepoint`]) extracted by
|
||||
//! `tools/extract_bravura_outlines.py` into `src/outlines_generated.rs` —
|
||||
//! SVG path `d` strings, staff-space units, y-up, moved here from
|
||||
//! `epiphany-render-svg` (which previously kept the table private,
|
||||
//! `outline()` was `pub(crate)`);
|
||||
//! * a parser from that `d`-string grammar into
|
||||
//! [`epiphany_layout_ir::PathCommand`] (`path` module, private — consumed
|
||||
//! only through [`BravuraGlyphCatalog`]), with a round-trip re-emitter
|
||||
//! that proves the typed form describes the same geometry (pin 5);
|
||||
//! * [`BravuraGlyphCatalog`], a real [`epiphany_layout_ir::GlyphCatalog`]
|
||||
//! whose `render_data` returns genuine parsed outlines — unlike
|
||||
//! `epiphany_layout_ir::BravuraCatalog`, which bundles no render data and
|
||||
//! honestly returns `None` for every glyph;
|
||||
//! * `tools/OFL.txt`, the SIL Open Font License 1.1 these redistributed
|
||||
//! outlines travel under (a redistribution condition, not decoration).
|
||||
//!
|
||||
//! ## What does NOT live here
|
||||
//!
|
||||
//! `BRAVURA_METRICS` stays in `epiphany_layout_ir` (pin 3) — this crate
|
||||
//! depends on it and never edits or re-derives it; any edit to that table
|
||||
//! churns every conformance byte in the repo, and it is out of this
|
||||
//! packet's scope entirely. `epiphany-render-svg`'s embedded-font subset
|
||||
//! (`font_subset_generated.rs`) also stays put: an embeddable font subset is
|
||||
//! a renderer concern, not a shared asset (pin 2).
|
||||
//!
|
||||
//! ## No cycle, no third-party dependency
|
||||
//!
|
||||
//! This crate depends on `epiphany-layout-ir` only (for `BRAVURA_METRICS`,
|
||||
//! `PathCommand`, and the `GlyphCatalog` vocabulary) — never the reverse —
|
||||
//! and pulls in no third-party crate (`cargo tree -p epiphany-glyphs` proves
|
||||
//! it) and uses no `build.rs`: it is in the MSRV closure, and the MSRV job
|
||||
//! runs `--workspace --exclude epiphany-editor-gui`, so it must stay clean.
|
||||
//!
|
||||
//! ## Byte-neutrality for `render-svg`
|
||||
//!
|
||||
//! `epiphany-render-svg` depends on this crate for the outline lookup but
|
||||
//! keeps emitting each glyph's *stored* `d` string, byte-for-byte, in its
|
||||
//! SVG output — never a re-serialization of the typed outline. Every SVG
|
||||
//! golden and every layout-conformance byte is unaffected by this crate's
|
||||
//! existence.
|
||||
|
||||
mod catalog;
|
||||
// `extent` computes an outline's tight bounding box purely to prove test g4
|
||||
// (the drawn ink fits the declared metrics bbox); nothing in production code
|
||||
// consumes it, so it is compiled only for `cargo test`.
|
||||
#[cfg(test)]
|
||||
mod extent;
|
||||
mod outline;
|
||||
mod outlines_generated;
|
||||
mod path;
|
||||
|
||||
pub use catalog::BravuraGlyphCatalog;
|
||||
pub use outline::{bundled_glyph_count, outline, smufl_codepoint, BravuraOutline};
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
//! Lookup over the bundled Bravura outlines ([`crate::outlines_generated`]) —
|
||||
//! the shared seam `epiphany-render-svg` depends on for its exact stored `d`
|
||||
//! strings and bounding boxes (Editor T4-pre W2 pin 2). `render-svg` keeps
|
||||
//! using [`outline`]'s returned `d` verbatim in its SVG output; it never
|
||||
//! re-serializes the typed form (pin 5 — see `path::emit_d`'s doc comment
|
||||
//! for why that distinction matters).
|
||||
|
||||
use crate::outlines_generated::BRAVURA_OUTLINES;
|
||||
|
||||
pub use crate::outlines_generated::BravuraOutline;
|
||||
|
||||
/// The genuine Bravura outline for a SMuFL glyph name, if bundled. The table is
|
||||
/// sorted by name, so this is a binary search.
|
||||
pub fn outline(name: &str) -> Option<&'static BravuraOutline> {
|
||||
BRAVURA_OUTLINES
|
||||
.binary_search_by(|o| o.name.cmp(name))
|
||||
.ok()
|
||||
.map(|i| &BRAVURA_OUTLINES[i])
|
||||
}
|
||||
|
||||
/// How many glyph outlines are bundled.
|
||||
pub fn bundled_glyph_count() -> usize {
|
||||
BRAVURA_OUTLINES.len()
|
||||
}
|
||||
|
||||
/// The SMuFL codepoint of a bundled glyph name, if bundled. Used by
|
||||
/// `render-svg`'s 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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_table_is_sorted_and_searchable() {
|
||||
// Binary search depends on the generator emitting names in order.
|
||||
assert!(BRAVURA_OUTLINES.windows(2).all(|w| w[0].name < w[1].name));
|
||||
assert!(outline("noteheadBlack").is_some());
|
||||
assert!(outline("gClef").is_some());
|
||||
assert!(outline("noSuchGlyph").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_pipeline_glyph_has_a_bundled_outline() {
|
||||
// Non-vacuity: every glyph the v0 layout pipeline can name (the
|
||||
// layout-ir BRAVURA_METRICS set) is drawable. If the metrics table grows
|
||||
// a glyph, the generator must be re-run — this test fails until it is.
|
||||
for m in epiphany_layout_ir::BRAVURA_METRICS {
|
||||
assert!(
|
||||
outline(m.name.as_ref()).is_some(),
|
||||
"no bundled outline for pipeline glyph {}",
|
||||
m.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metric_bboxes_contain_the_drawn_outlines() {
|
||||
// The engraver evaluates collisions from a glyph's metric bounding box,
|
||||
// while the renderer draws (and bounds) its outline. If a metric box were a
|
||||
// hair smaller than the ink — e.g. from rounding the bbox to the *nearest*
|
||||
// 1/1024 — a hard no-collision verdict could be microscopically false on
|
||||
// paper. The metrics are extracted as the outline bounds rounded *outward*
|
||||
// to the grid, so every metric box must contain its outline box.
|
||||
for m in epiphany_layout_ir::BRAVURA_METRICS {
|
||||
let Some(o) = outline(m.name.as_ref()) else {
|
||||
continue;
|
||||
};
|
||||
let mb = m.bounding_box();
|
||||
let [ol, ob, oright, otop] = o.bbox;
|
||||
assert!(
|
||||
mb.left.0 <= ol && mb.bottom.0 <= ob && mb.right.0 >= oright && mb.top.0 >= otop,
|
||||
"metric bbox {:?} for {} must contain its outline bbox {:?}",
|
||||
[mb.left.0, mb.bottom.0, mb.right.0, mb.top.0],
|
||||
m.name,
|
||||
o.bbox,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_have_finite_bounds_and_nonempty_paths() {
|
||||
for o in BRAVURA_OUTLINES {
|
||||
assert!(o.bbox.iter().all(|v| v.is_finite()));
|
||||
assert!(o.bbox[0] <= o.bbox[2] && o.bbox[1] <= o.bbox[3]);
|
||||
assert!(!o.path.is_empty());
|
||||
assert!(o.path.starts_with('M'), "{} path must start with M", o.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,10 @@
|
|||
|
||||
/// One bundled glyph outline: SMuFL name, codepoint, the SVG path `d` in
|
||||
/// staff-space / y-up coordinates, and the outline's tight bounding box
|
||||
/// `[left, bottom, right, top]` in staff spaces.
|
||||
pub(crate) struct BravuraOutline {
|
||||
/// `[left, bottom, right, top]` in staff spaces. `pub`: this crate's whole
|
||||
/// point is to be a shared seam other crates (`epiphany-render-svg`, and a
|
||||
/// future canvas renderer) depend on for exactly these fields.
|
||||
pub struct BravuraOutline {
|
||||
pub name: &'static str,
|
||||
pub codepoint: u32,
|
||||
pub path: &'static str,
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
//! Parses the SVG path `d` grammar the bundled outlines are generated in into
|
||||
//! the shared typed [`PathCommand`] form (Editor T4-pre W2 pin 4), and
|
||||
//! re-emits it in the generator's exact byte formatting so a round-trip
|
||||
//! proves the typed form describes the same geometry (pin 5).
|
||||
//!
|
||||
//! **Correction to the contract's stated grammar.**
|
||||
//! `spec/CONTRACT_EDITOR_T4PRE_W2_GLYPHS.md`'s "verified starting point"
|
||||
//! describes the generated grammar as "absolute `M`/`L`/`C`/`Z`, decimal
|
||||
//! coordinates, 4 decimal places". Verified against all 37 bundled glyphs
|
||||
//! before writing this parser, the real grammar is wider on both counts:
|
||||
//!
|
||||
//! * The generator (`tools/extract_bravura_outlines.py`, via
|
||||
//! `fontTools.pens.svgPathPen.SVGPathPen`'s default `optimizeCommands`)
|
||||
//! also emits absolute `V` (vertical-only lineto) and `H` (horizontal-only
|
||||
//! lineto) wherever a lineto's target shares an axis with the current
|
||||
//! point — 23 of the 37 bundled glyphs use at least one. [`PathCommand`]
|
||||
//! has no shorthand variant, so [`parse_d`] lowers `V`/`H` to
|
||||
//! [`PathCommand::LineTo`]; [`emit_d`] reconstructs the shorthand
|
||||
//! byte-for-byte from geometry alone (comparing the target to the current
|
||||
//! point — see its doc comment), which is exactly what the round-trip
|
||||
//! test (pin 5, contract test g1) proves for every bundled glyph.
|
||||
//! * Coordinates are rounded to *at most* 4 decimals with trailing zeros (and
|
||||
//! a bare `-0`) stripped by the generator's own `round_d`
|
||||
//! (`tools/extract_bravura_outlines.py:180-185`), so the printed precision
|
||||
//! varies per number — 0 to 3 fractional digits are observed in the
|
||||
//! bundled data (never 4, though the grammar allows it); it is not a fixed
|
||||
//! width.
|
||||
//!
|
||||
//! Every command in the observed grammar carries exactly one point (`M`,
|
||||
//! `L`), one coordinate (`V`, `H`), three points (`C`), or none (`Z`) — the
|
||||
//! generator never merges consecutive same-type commands into a
|
||||
//! multi-coordinate group, so the parser does not need to handle that SVG
|
||||
//! generality either.
|
||||
|
||||
use epiphany_layout_ir::{PathCommand, Point};
|
||||
|
||||
/// Parses an absolute SVG path `d` string in the generator's grammar
|
||||
/// (`M`/`L`/`C`/`V`/`H`/`Z`, one point/coordinate per command, absolute
|
||||
/// coordinates) into typed path commands.
|
||||
///
|
||||
/// Panics on malformed input. The input is always this crate's own bundled,
|
||||
/// generator-produced constant data — never external or untrusted text — so
|
||||
/// a parse failure is a bug in this parser or the bundled table, not a
|
||||
/// runtime condition a caller should recover from.
|
||||
pub(crate) fn parse_d(d: &str) -> Vec<PathCommand> {
|
||||
let bytes = d.as_bytes();
|
||||
let mut i = 0usize;
|
||||
let mut out = Vec::new();
|
||||
let mut cur = (0.0f32, 0.0f32);
|
||||
let mut subpath_start = (0.0f32, 0.0f32);
|
||||
|
||||
let take_number = |bytes: &[u8], i: &mut usize| -> f32 {
|
||||
while *i < bytes.len() && bytes[*i] == b' ' {
|
||||
*i += 1;
|
||||
}
|
||||
let start = *i;
|
||||
if *i < bytes.len() && bytes[*i] == b'-' {
|
||||
*i += 1;
|
||||
}
|
||||
while *i < bytes.len() && bytes[*i].is_ascii_digit() {
|
||||
*i += 1;
|
||||
}
|
||||
if *i < bytes.len() && bytes[*i] == b'.' {
|
||||
*i += 1;
|
||||
while *i < bytes.len() && bytes[*i].is_ascii_digit() {
|
||||
*i += 1;
|
||||
}
|
||||
}
|
||||
let tok = std::str::from_utf8(&bytes[start..*i])
|
||||
.unwrap_or_else(|e| panic!("non-UTF-8 number token in {d:?}: {e}"));
|
||||
tok.parse::<f32>()
|
||||
.unwrap_or_else(|e| panic!("bad number token {tok:?} in {d:?}: {e}"))
|
||||
};
|
||||
|
||||
while i < bytes.len() {
|
||||
let cmd = bytes[i];
|
||||
i += 1;
|
||||
match cmd {
|
||||
b'M' => {
|
||||
let x = take_number(bytes, &mut i);
|
||||
let y = take_number(bytes, &mut i);
|
||||
out.push(PathCommand::MoveTo(Point::new(x, y)));
|
||||
cur = (x, y);
|
||||
subpath_start = cur;
|
||||
}
|
||||
b'L' => {
|
||||
let x = take_number(bytes, &mut i);
|
||||
let y = take_number(bytes, &mut i);
|
||||
out.push(PathCommand::LineTo(Point::new(x, y)));
|
||||
cur = (x, y);
|
||||
}
|
||||
b'V' => {
|
||||
let y = take_number(bytes, &mut i);
|
||||
cur = (cur.0, y);
|
||||
out.push(PathCommand::LineTo(Point::new(cur.0, cur.1)));
|
||||
}
|
||||
b'H' => {
|
||||
let x = take_number(bytes, &mut i);
|
||||
cur = (x, cur.1);
|
||||
out.push(PathCommand::LineTo(Point::new(cur.0, cur.1)));
|
||||
}
|
||||
b'C' => {
|
||||
let c1x = take_number(bytes, &mut i);
|
||||
let c1y = take_number(bytes, &mut i);
|
||||
let c2x = take_number(bytes, &mut i);
|
||||
let c2y = take_number(bytes, &mut i);
|
||||
let tx = take_number(bytes, &mut i);
|
||||
let ty = take_number(bytes, &mut i);
|
||||
out.push(PathCommand::CurveTo {
|
||||
control1: Point::new(c1x, c1y),
|
||||
control2: Point::new(c2x, c2y),
|
||||
to: Point::new(tx, ty),
|
||||
});
|
||||
cur = (tx, ty);
|
||||
}
|
||||
b'Z' => {
|
||||
out.push(PathCommand::Close);
|
||||
cur = subpath_start;
|
||||
}
|
||||
other => panic!(
|
||||
"unsupported path command byte {:#04x} ({}) in {d:?}: the bundled grammar is \
|
||||
absolute M/L/C/V/H/Z only",
|
||||
other, other as char
|
||||
),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Re-emits typed path commands in the generator's exact `d`-string
|
||||
/// formatting (pin 5's round-trip proof).
|
||||
///
|
||||
/// [`PathCommand::LineTo`] carries no record of whether it was originally an
|
||||
/// `L`, `V`, or `H` command — the shared type has no shorthand variant (see
|
||||
/// the module doc). This reconstructs the shorthand from geometry alone,
|
||||
/// matching `fontTools.pens.svgPathPen.SVGPathPen`'s own rule: emit `V` when
|
||||
/// only the *x* coordinate is unchanged from the current point, `H` when
|
||||
/// only *y* is unchanged, and `L` otherwise (including the degenerate
|
||||
/// zero-length case, x and y both unchanged, which does not occur in any
|
||||
/// bundled glyph — verified below). Numbers are formatted exactly as the
|
||||
/// generator's `round_d`: at most 4 decimals, trailing zeros and a trailing
|
||||
/// `.` stripped, `-0`/empty normalised to `0`.
|
||||
///
|
||||
/// Exists solely for the round-trip proof (test g1) — nothing in production
|
||||
/// code re-serializes the typed form (pin 5: `render-svg` always emits the
|
||||
/// *stored* `d` string), so this is compiled only for `cargo test`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn emit_d(commands: &[PathCommand]) -> String {
|
||||
let mut out = String::new();
|
||||
let mut cur = (0.0f32, 0.0f32);
|
||||
let mut subpath_start = (0.0f32, 0.0f32);
|
||||
for cmd in commands {
|
||||
match cmd {
|
||||
PathCommand::MoveTo(p) => {
|
||||
let (x, y) = (p.x.0, p.y.0);
|
||||
out.push('M');
|
||||
push_num(&mut out, x);
|
||||
out.push(' ');
|
||||
push_num(&mut out, y);
|
||||
cur = (x, y);
|
||||
subpath_start = cur;
|
||||
}
|
||||
PathCommand::LineTo(p) => {
|
||||
let (x, y) = (p.x.0, p.y.0);
|
||||
let x_same = x == cur.0;
|
||||
let y_same = y == cur.1;
|
||||
if x_same && !y_same {
|
||||
out.push('V');
|
||||
push_num(&mut out, y);
|
||||
} else if y_same && !x_same {
|
||||
out.push('H');
|
||||
push_num(&mut out, x);
|
||||
} else {
|
||||
out.push('L');
|
||||
push_num(&mut out, x);
|
||||
out.push(' ');
|
||||
push_num(&mut out, y);
|
||||
}
|
||||
cur = (x, y);
|
||||
}
|
||||
PathCommand::CurveTo {
|
||||
control1,
|
||||
control2,
|
||||
to,
|
||||
} => {
|
||||
out.push('C');
|
||||
push_num(&mut out, control1.x.0);
|
||||
out.push(' ');
|
||||
push_num(&mut out, control1.y.0);
|
||||
out.push(' ');
|
||||
push_num(&mut out, control2.x.0);
|
||||
out.push(' ');
|
||||
push_num(&mut out, control2.y.0);
|
||||
out.push(' ');
|
||||
push_num(&mut out, to.x.0);
|
||||
out.push(' ');
|
||||
push_num(&mut out, to.y.0);
|
||||
cur = (to.x.0, to.y.0);
|
||||
}
|
||||
PathCommand::Close => {
|
||||
out.push('Z');
|
||||
cur = subpath_start;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Formats one coordinate exactly as the generator's `round_d`: at most 4
|
||||
/// decimals, trailing zeros and a trailing `.` stripped, `-0`/empty
|
||||
/// normalised to `0`. `emit_d`'s only caller; test-only for the same reason.
|
||||
#[cfg(test)]
|
||||
fn push_num(out: &mut String, v: f32) {
|
||||
use std::fmt::Write as _;
|
||||
let mut s = String::new();
|
||||
let _ = write!(s, "{v:.4}");
|
||||
if s.contains('.') {
|
||||
while s.ends_with('0') {
|
||||
s.pop();
|
||||
}
|
||||
if s.ends_with('.') {
|
||||
s.pop();
|
||||
}
|
||||
}
|
||||
if s.is_empty() || s == "-0" {
|
||||
s = "0".to_owned();
|
||||
}
|
||||
out.push_str(&s);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::outlines_generated::BRAVURA_OUTLINES;
|
||||
|
||||
/// (g1) The packet's load-bearing test: every bundled glyph's `d` string
|
||||
/// parses and re-emits byte-for-byte identical. If this holds for all 37
|
||||
/// bundled glyphs, the typed form provably describes the same geometry —
|
||||
/// no geometric spot-checking is needed (pin 5, primary path, not the
|
||||
/// sanctioned coordinate-sequence fallback).
|
||||
#[test]
|
||||
fn every_bundled_glyph_round_trips_byte_for_byte() {
|
||||
assert_eq!(
|
||||
BRAVURA_OUTLINES.len(),
|
||||
37,
|
||||
"sanity: the bundled glyph count moved"
|
||||
);
|
||||
for o in BRAVURA_OUTLINES {
|
||||
let parsed = parse_d(o.path);
|
||||
let reemitted = emit_d(&parsed);
|
||||
assert_eq!(
|
||||
reemitted, o.path,
|
||||
"{}: parse -> emit did not round-trip byte-for-byte",
|
||||
o.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// (g2) `Close` survives parsing: every bundled glyph ends with `Z`, and
|
||||
/// the parsed command list must carry a trailing `PathCommand::Close`,
|
||||
/// not merely reproduce the byte (which g1 already proves) — this
|
||||
/// exercises the *typed* value directly, independent of re-emission.
|
||||
#[test]
|
||||
fn close_survives_parsing_as_a_typed_command() {
|
||||
for o in BRAVURA_OUTLINES {
|
||||
let parsed = parse_d(o.path);
|
||||
assert!(
|
||||
matches!(parsed.last(), Some(PathCommand::Close)),
|
||||
"{}: parsed commands must end with Close",
|
||||
o.name
|
||||
);
|
||||
// Every bundled path has at least one closed subpath, so Close
|
||||
// must appear at least once, not just coincidentally last.
|
||||
assert!(
|
||||
parsed.iter().any(|c| matches!(c, PathCommand::Close)),
|
||||
"{}: parsed commands must contain a Close",
|
||||
o.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// (g6) absolute, not relative — a hand-verified case, not a
|
||||
/// re-derivation through the parser under test. `augmentationDot`'s `d`
|
||||
/// (transcribed below, independently of [`BRAVURA_OUTLINES`]) is four
|
||||
/// cubic curves tracing a circle back to its own start point `(0.4, 0)`.
|
||||
/// Read by eye, every command's numbers *are* the absolute point they
|
||||
/// land on. Were the parser instead accumulating each command's numbers
|
||||
/// onto the current point (SVG's lowercase/relative convention), the
|
||||
/// first curve's endpoint would still land right (an all-positive
|
||||
/// glyph's first hop can't distinguish the two rules), but the second
|
||||
/// curve's endpoint would land at `(0.2, 0.2) + (0, 0) = (0.2, 0.2)`
|
||||
/// instead of the correct `(0, 0)`, and every command after that would
|
||||
/// drift further from a straightforward accumulation of offsets. This
|
||||
/// pins the correct (absolute) reading explicitly, command by command.
|
||||
#[test]
|
||||
fn parsed_coordinates_are_absolute_not_relative() {
|
||||
let d = "M0.4 0C0.4 0.112 0.312 0.2 0.2 0.2C0.088 0.2 0 0.112 0 0\
|
||||
C0 -0.112 0.088 -0.2 0.2 -0.2C0.312 -0.2 0.4 -0.112 0.4 0Z";
|
||||
// Sanity: this is really `augmentationDot`'s bundled `d`, not a
|
||||
// stand-in string that happens to look similar.
|
||||
let augmentation_dot = BRAVURA_OUTLINES
|
||||
.iter()
|
||||
.find(|o| o.name == "augmentationDot")
|
||||
.expect("augmentationDot is bundled");
|
||||
assert_eq!(augmentation_dot.path, d);
|
||||
|
||||
let expected = vec![
|
||||
PathCommand::MoveTo(Point::new(0.4, 0.0)),
|
||||
PathCommand::CurveTo {
|
||||
control1: Point::new(0.4, 0.112),
|
||||
control2: Point::new(0.312, 0.2),
|
||||
to: Point::new(0.2, 0.2),
|
||||
},
|
||||
PathCommand::CurveTo {
|
||||
control1: Point::new(0.088, 0.2),
|
||||
control2: Point::new(0.0, 0.112),
|
||||
to: Point::new(0.0, 0.0),
|
||||
},
|
||||
PathCommand::CurveTo {
|
||||
control1: Point::new(0.0, -0.112),
|
||||
control2: Point::new(0.088, -0.2),
|
||||
to: Point::new(0.2, -0.2),
|
||||
},
|
||||
PathCommand::CurveTo {
|
||||
control1: Point::new(0.312, -0.2),
|
||||
control2: Point::new(0.4, -0.112),
|
||||
to: Point::new(0.4, 0.0),
|
||||
},
|
||||
PathCommand::Close,
|
||||
];
|
||||
assert_eq!(parse_d(d), expected);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Extract genuine Bravura SMuFL glyph outlines into a Rust table.
|
||||
|
||||
Reproducible generator for `epiphany-render-svg`'s bundled outline data. It
|
||||
fetches the official OFL `Bravura.otf` and the SMuFL `glyphnames.json`, then
|
||||
emits `src/outlines_generated.rs` with each glyph's outline as an SVG path in
|
||||
**staff-space**, **y-up** coordinates (the renderer's coordinate system).
|
||||
Reproducible generator for `epiphany-glyphs`'s bundled outline data (moved
|
||||
here from `epiphany-render-svg` at Editor T4-pre W2, the shared typed
|
||||
glyph-asset seam — `render-svg` now depends on this crate instead of owning
|
||||
the table itself). It fetches the official OFL `Bravura.otf` and the SMuFL
|
||||
`glyphnames.json`, then emits `src/outlines_generated.rs` with each glyph's
|
||||
outline as an SVG path in **staff-space**, **y-up** coordinates.
|
||||
|
||||
Usage:
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools
|
||||
python3 extract_bravura_outlines.py > ../crates/epiphany-render-svg/src/outlines_generated.rs
|
||||
python3 extract_bravura_outlines.py > ../src/outlines_generated.rs
|
||||
|
||||
# Also regenerate epiphany-render-svg's embedded-font subset (that
|
||||
# generated file stays in render-svg — W2 pin 2 — so the output path
|
||||
# crosses back into the sibling crate):
|
||||
python3 extract_bravura_outlines.py --font-out \
|
||||
../../epiphany-render-svg/src/font_subset_generated.rs \
|
||||
> ../src/outlines_generated.rs
|
||||
|
||||
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; the
|
||||
|
|
@ -250,8 +259,10 @@ def main():
|
|||
o.append("")
|
||||
o.append("/// One bundled glyph outline: SMuFL name, codepoint, the SVG path `d` in")
|
||||
o.append("/// staff-space / y-up coordinates, and the outline's tight bounding box")
|
||||
o.append("/// `[left, bottom, right, top]` in staff spaces.")
|
||||
o.append("pub(crate) struct BravuraOutline {")
|
||||
o.append("/// `[left, bottom, right, top]` in staff spaces. `pub`: this crate's whole")
|
||||
o.append("/// point is to be a shared seam other crates (`epiphany-render-svg`, and a")
|
||||
o.append("/// future canvas renderer) depend on for exactly these fields.")
|
||||
o.append("pub struct BravuraOutline {")
|
||||
o.append(" pub name: &'static str,")
|
||||
o.append(" pub codepoint: u32,")
|
||||
o.append(" pub path: &'static str,")
|
||||
|
|
@ -273,6 +284,10 @@ def main():
|
|||
print(f"// extracted {len(rows)}/{len(NAMES)} glyphs", file=sys.stderr)
|
||||
|
||||
# Optionally emit the embedded-font subset (renderer GlyphMode::EmbeddedFont).
|
||||
# This generated file stays in `epiphany-render-svg` (W2 pin 2: an embeddable
|
||||
# font subset is a renderer concern, not a shared asset), so `--font-out`
|
||||
# crosses back into the sibling crate even though this script now lives in
|
||||
# `epiphany-glyphs`.
|
||||
if "--font-out" in sys.argv:
|
||||
import fontTools
|
||||
out_path = sys.argv[sys.argv.index("--font-out") + 1]
|
||||
|
|
@ -11,6 +11,12 @@ description = "Agent I's SVG renderer behind the Epiphany RenderIR interface (sp
|
|||
# The renderer consumes the Chapter 7 ResolvedLayoutIR / RenderIR and the glyph
|
||||
# catalog, all defined in epiphany-layout-ir.
|
||||
epiphany-layout-ir.workspace = true
|
||||
# The shared typed glyph-asset seam (Editor T4-pre W2): the bundled Bravura
|
||||
# outline table (and its extractor tool) moved here from this crate, so the
|
||||
# renderer now depends on it for the outline lookup instead of owning the
|
||||
# table privately. This crate still emits each glyph's stored `d` string
|
||||
# byte-for-byte in its SVG output, never the typed form.
|
||||
epiphany-glyphs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# The demo binary's `--solver=real` path drives Agent I's engrave solver; only
|
||||
|
|
|
|||
|
|
@ -126,3 +126,34 @@ loop. `content_bounds` grows by each curve's control-point hull ± half-thicknes
|
|||
the acceptance snapshot prints it and the `provenance_count == glyph + stroke`
|
||||
invariant became `+ curve`. `GlyphClass` is untouched — curves are not glyphs,
|
||||
so they carry no `data-class`.
|
||||
|
||||
## Bundled outline table moved to `epiphany-glyphs` (Editor T4-pre W2, 2026-07-24)
|
||||
|
||||
`src/outlines_generated.rs` and `tools/{extract_bravura_outlines.py,OFL.txt}`
|
||||
moved out of this crate into the new `epiphany-glyphs` crate — the shared
|
||||
typed glyph-asset seam a canvas tessellator needs (`spec/PLAN_EDITOR_APP.md`
|
||||
§3.7 / Ruling A), populated on top of `layout-ir`'s already-designed
|
||||
`PathCommand`/`GlyphRenderData`/`GlyphCatalog::render_data` interface. This
|
||||
crate now depends on `epiphany-glyphs` instead of owning the table; `outline()`,
|
||||
`bundled_glyph_count()`, and `smufl_codepoint()` in `src/outline.rs` became
|
||||
thin delegations (the latter two stay `pub` here per the W2 contract's pin 2,
|
||||
even though nothing outside this crate calls them — an API that costs one
|
||||
`pub use` is not worth breaking). The table's own tests (sortedness, pipeline
|
||||
coverage, metric/outline bbox containment, finite-bounds sanity) moved with
|
||||
it to `epiphany-glyphs`; this crate's `outline.rs` test module now holds only
|
||||
the font-subset-specific tests (`font_subset_generated.rs` deliberately
|
||||
**stayed** — an embeddable font subset is a renderer concern, not a shared
|
||||
asset) plus a thin delegation smoke test.
|
||||
|
||||
**Byte-neutrality is unconditional.** `svg.rs`'s `GlyphMode::PathOutline` arm
|
||||
still reads `outline(name).path` — the stored `d` string — directly into the
|
||||
emitted `<path d="…">`. `epiphany-glyphs` additionally parses that same
|
||||
string into typed `PathCommand`s (for `BravuraGlyphCatalog::render_data`,
|
||||
consumed by a future canvas renderer, not by this crate), but that parser and
|
||||
its round-trip re-emitter are private to `epiphany-glyphs` and unreachable
|
||||
from here — this renderer has no code path that could route through the
|
||||
typed form even by mistake. Verified before/after against the base commit: a
|
||||
throwaway probe captured `ResolvedLayoutIR::canonical_bytes()` for every
|
||||
reference-suite fixture plus the two named W1 fixtures, and all five GUI
|
||||
goldens were re-run; every byte was identical (reported in the W2 packet
|
||||
report, not committed here).
|
||||
|
|
|
|||
|
|
@ -55,23 +55,30 @@ only (display scale, margin, provenance attributes, and `glyph_mode` — inline
|
|||
## Bundled Bravura data
|
||||
|
||||
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:
|
||||
`epiphany-glyphs`'s `tools/extract_bravura_outlines.py` — the font is **not
|
||||
vendored**, only the generated Rust is committed. **The outlines moved to
|
||||
`epiphany-glyphs` in T4-pre W2** (a canvas renderer needs them too, so they are
|
||||
a shared asset rather than a renderer-private one); the font subset stayed here,
|
||||
because an embeddable OTF is a renderer concern:
|
||||
|
||||
- `src/outlines_generated.rs` — the inline glyph outlines (geometry-only, so
|
||||
byte-stable across fontTools versions);
|
||||
- `epiphany-glyphs/src/outlines_generated.rs` — the inline glyph outlines
|
||||
(geometry-only, so byte-stable across fontTools versions). This crate reaches
|
||||
them through `epiphany_glyphs::outline`, and re-exports
|
||||
`bundled_glyph_count`/`smufl_codepoint` unchanged;
|
||||
- `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
|
||||
(`../epiphany-glyphs/tools/OFL.txt`, which travels with the redistributed
|
||||
outlines); 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
|
||||
cd crates/epiphany-glyphs/tools
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools blake3
|
||||
python3 extract_bravura_outlines.py --font-out ../src/font_subset_generated.rs \
|
||||
python3 extract_bravura_outlines.py \
|
||||
--font-out ../../epiphany-render-svg/src/font_subset_generated.rs \
|
||||
> ../src/outlines_generated.rs
|
||||
```
|
||||
|
|
|
|||
|
|
@ -21,12 +21,17 @@
|
|||
//!
|
||||
//! ## What it draws, and the non-overreach rule
|
||||
//!
|
||||
//! The bundled outlines are extracted from the official OFL `Bravura.otf` (see
|
||||
//! `tools/extract_bravura_outlines.py` and `tools/OFL.txt`) in staff-space,
|
||||
//! y-up coordinates. The renderer makes SVG-encoding choices only and never
|
||||
//! engraving-semantic ones; see the private `svg` module for the coordinate
|
||||
//! system, the provenance-tracing contract, and the diagnostic-not-paper-over
|
||||
//! rule.
|
||||
//! The bundled outlines are extracted from the official OFL `Bravura.otf` in
|
||||
//! staff-space, y-up coordinates, by `epiphany-glyphs`'s
|
||||
//! `tools/extract_bravura_outlines.py` and redistributed under
|
||||
//! `epiphany-glyphs/tools/OFL.txt` (Editor T4-pre W2 moved the bundled table
|
||||
//! and its extractor out of this crate into that shared seam; this crate
|
||||
//! depends on it but still emits each glyph's *stored* `d` string
|
||||
//! byte-for-byte — never a re-serialization of the typed outline — so this
|
||||
//! move changed no SVG byte). The renderer makes SVG-encoding choices only
|
||||
//! and never engraving-semantic ones; see the private `svg` module for the
|
||||
//! coordinate system, the provenance-tracing contract, and the
|
||||
//! diagnostic-not-paper-over rule.
|
||||
//!
|
||||
//! ## Font availability
|
||||
//!
|
||||
|
|
@ -38,14 +43,15 @@
|
|||
//! * [`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
|
||||
//! regenerated by `epiphany-glyphs/tools/extract_bravura_outlines.py --font-out`,
|
||||
//! which stays this crate's own generated file — an embeddable font subset
|
||||
//! is a renderer concern, not a shared asset). 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;
|
||||
pub mod xml;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,79 +1,39 @@
|
|||
//! Lookup over the bundled Bravura outlines ([`crate::outlines_generated`]).
|
||||
//! Delegates the bundled Bravura outline lookup to `epiphany-glyphs`, the
|
||||
//! shared typed glyph-asset seam (Editor T4-pre W2): the outline table used
|
||||
//! to live here privately (`outline()` was `pub(crate)`, with no callers
|
||||
//! outside this crate); it now lives in `epiphany-glyphs` so a future canvas
|
||||
//! renderer can depend on the same typed geometry without depending on this
|
||||
//! SVG renderer. This crate's own outline-data tests (sortedness, pipeline
|
||||
//! coverage, metric/outline bbox containment) moved with the table; what
|
||||
//! remains here is font-subset-specific (that generated file stays in this
|
||||
//! crate — W2 pin 2: an embeddable font subset is a renderer concern, not a
|
||||
//! shared asset).
|
||||
//!
|
||||
//! Byte-neutrality (W2 pin 5): `svg.rs` still reads `outline(name).path` —
|
||||
//! the *stored* `d` string — directly into the emitted SVG. It never routes
|
||||
//! through `epiphany-glyphs`'s typed-path parser/re-emitter, so every SVG
|
||||
//! golden and layout-conformance byte is unaffected by this delegation.
|
||||
|
||||
use crate::outlines_generated::{BravuraOutline, BRAVURA_OUTLINES};
|
||||
pub(crate) use epiphany_glyphs::outline;
|
||||
|
||||
/// The genuine Bravura outline for a SMuFL glyph name, if bundled. The table is
|
||||
/// sorted by name, so this is a binary search.
|
||||
pub(crate) fn outline(name: &str) -> Option<&'static BravuraOutline> {
|
||||
BRAVURA_OUTLINES
|
||||
.binary_search_by(|o| o.name.cmp(name))
|
||||
.ok()
|
||||
.map(|i| &BRAVURA_OUTLINES[i])
|
||||
}
|
||||
|
||||
/// How many glyph outlines are bundled.
|
||||
/// How many glyph outlines are bundled. Re-exported from `epiphany-glyphs`
|
||||
/// even though nothing outside `render-svg` calls it today — an API that
|
||||
/// costs one `pub use` is not worth breaking (W2 pin 2).
|
||||
pub fn bundled_glyph_count() -> usize {
|
||||
BRAVURA_OUTLINES.len()
|
||||
epiphany_glyphs::bundled_glyph_count()
|
||||
}
|
||||
|
||||
/// 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)
|
||||
epiphany_glyphs::smufl_codepoint(name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_table_is_sorted_and_searchable() {
|
||||
// Binary search depends on the generator emitting names in order.
|
||||
assert!(BRAVURA_OUTLINES.windows(2).all(|w| w[0].name < w[1].name));
|
||||
assert!(outline("noteheadBlack").is_some());
|
||||
assert!(outline("gClef").is_some());
|
||||
assert!(outline("noSuchGlyph").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_pipeline_glyph_has_a_bundled_outline() {
|
||||
// Non-vacuity: every glyph the v0 layout pipeline can name (the
|
||||
// layout-ir BRAVURA_METRICS set) is drawable. If the metrics table grows
|
||||
// a glyph, the generator must be re-run — this test fails until it is.
|
||||
for m in epiphany_layout_ir::BRAVURA_METRICS {
|
||||
assert!(
|
||||
outline(m.name.as_ref()).is_some(),
|
||||
"no bundled outline for pipeline glyph {}",
|
||||
m.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metric_bboxes_contain_the_drawn_outlines() {
|
||||
// The engraver evaluates collisions from a glyph's metric bounding box,
|
||||
// while the renderer draws (and bounds) its outline. If a metric box were a
|
||||
// hair smaller than the ink — e.g. from rounding the bbox to the *nearest*
|
||||
// 1/1024 — a hard no-collision verdict could be microscopically false on
|
||||
// paper. The metrics are extracted as the outline bounds rounded *outward*
|
||||
// to the grid, so every metric box must contain its outline box.
|
||||
for m in epiphany_layout_ir::BRAVURA_METRICS {
|
||||
let Some(o) = outline(m.name.as_ref()) else {
|
||||
continue;
|
||||
};
|
||||
let mb = m.bounding_box();
|
||||
let [ol, ob, oright, otop] = o.bbox;
|
||||
assert!(
|
||||
mb.left.0 <= ol && mb.bottom.0 <= ob && mb.right.0 >= oright && mb.top.0 >= otop,
|
||||
"metric bbox {:?} for {} must contain its outline bbox {:?}",
|
||||
[mb.left.0, mb.bottom.0, mb.right.0, mb.top.0],
|
||||
m.name,
|
||||
o.bbox,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
|
|
@ -220,12 +180,18 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_have_finite_bounds_and_nonempty_paths() {
|
||||
for o in BRAVURA_OUTLINES {
|
||||
assert!(o.bbox.iter().all(|v| v.is_finite()));
|
||||
assert!(o.bbox[0] <= o.bbox[2] && o.bbox[1] <= o.bbox[3]);
|
||||
assert!(!o.path.is_empty());
|
||||
assert!(o.path.starts_with('M'), "{} path must start with M", o.name);
|
||||
}
|
||||
fn outline_delegates_to_epiphany_glyphs() {
|
||||
// A thin smoke test that the delegation is wired, not a re-test of
|
||||
// epiphany-glyphs's own (more thorough) outline-data tests.
|
||||
assert!(outline("noteheadBlack").is_some());
|
||||
assert!(outline("noSuchGlyph").is_none());
|
||||
assert_eq!(
|
||||
bundled_glyph_count(),
|
||||
epiphany_glyphs::bundled_glyph_count()
|
||||
);
|
||||
assert_eq!(
|
||||
smufl_codepoint("gClef"),
|
||||
epiphany_glyphs::smufl_codepoint("gClef")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue