epiphany/crates/epiphany-layout-ir/src/glyph.rs

621 lines
26 KiB
Rust

//! The glyph catalog (Chapter 7 §"Glyph Catalog Interface") and its
//! reproducibility identity (§7.3.2), with Bravura metrics bundled in-tree for
//! testing (QUICKSTART, Agent E).
//!
//! The IR references glyphs by name and queries metrics from a font catalog;
//! metrics are never embedded in pipeline objects (Chapter 7 §"Glyph metrics
//! live elsewhere"). For reproducible layout, the exact catalog consumed by a
//! solve MUST be identifiable: [`GlyphCatalogIdentity`] carries the font id, its
//! version, the SMuFL version, and a content hash over the canonical
//! serialization of every consulted glyph's metrics (bounding box, advance
//! width, **and named anchors**), computed with the Appendix D domain tag
//! `MUSCFNTM` ([`DomainTag::FONT_METRICS`]).
//!
//! v0 bundles a small but representative slice of the real
//! [Bravura](https://github.com/steinbergmedia/bravura) SMuFL font's metrics, in
//! `1/1024`-staff-space units (the catalog's exact, hashable unit), so the
//! catalog identity is exercised end to end without shipping a font file. A full
//! catalog (and the render-data side of the interface) is an out-of-core concern.
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use epiphany_determinism::{DomainTag, Preimage};
use crate::spatial::{BoundingBox, Point};
/// The SMuFL version a catalog targets (Chapter 7: `SmuflVersion`).
///
/// This is [`epiphany_core::SmuflVersion`] (Chapter 4 §"SMuFL Versioning"):
/// the glyph catalog's version and the tuning context's declared SMuFL
/// version are **the same type**, unified here in Push 4b tranche 3b-ii
/// (P13-S12). Layout-ir previously defined its own `{ major: u16, minor: u16
/// }` storing the minor *literally*, whose derived `Ord` sorted 1.3/1.4
/// before 1.12 — backwards versus SMuFL's real release order. See
/// [`epiphany_core::SmuflVersion`]'s doc comment for the fraction-normalized
/// `minor_centi` shape that fixes it.
pub use epiphany_core::SmuflVersion;
/// Identifier of a specific SMuFL font (Chapter 7: `FontId`).
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct FontId(pub Cow<'static, str>);
impl FontId {
/// The reference font v0 bundles metrics for.
pub const BRAVURA: FontId = FontId(Cow::Borrowed("Bravura"));
/// Constructs an identifier for a catalog loaded at runtime.
pub fn owned(name: impl Into<String>) -> Self {
FontId(Cow::Owned(name.into()))
}
}
/// A font-catalog glyph identifier that may be bundled or loaded at runtime.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct GlyphReference(pub Cow<'static, str>);
impl GlyphReference {
pub const fn borrowed(name: &'static str) -> Self {
GlyphReference(Cow::Borrowed(name))
}
pub fn owned(name: impl Into<String>) -> Self {
GlyphReference(Cow::Owned(name.into()))
}
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
/// A semantic font version (Chapter 7 §7.3.2: the optional `font_version`).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl SemVer {
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
SemVer {
major,
minor,
patch,
}
}
}
/// A reproducibility-quality identifier for the glyph catalog used to produce a
/// layout (Chapter 7 §7.3.2). Required for any layout-conformance claim that
/// depends on byte-equal output across runs.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct GlyphCatalogIdentity {
/// SMuFL version targeted.
pub smufl_version: SmuflVersion,
/// The specific font in use.
pub font_id: FontId,
/// The font's release version, if the publisher uses versioned releases
/// (Chapter 7 §7.3.2: the spec's optional `font_version`). Bravura is
/// versioned, so v0 records the release the bundled metrics track.
pub font_version: Option<SemVer>,
/// Content hash (BLAKE3 / `MUSCFNTM`) over the canonical serialization of
/// every consulted glyph's metrics (Chapter 7 §7.3.2).
pub metrics_hash: [u8; 32],
}
/// The Bravura release the in-tree metrics table is extracted from — the *same*
/// SHA-pinned font (`bravura-1.392`) the renderer's outlines come from, so the
/// reserved advances/bboxes and the drawn ink agree.
///
/// **Version mapping (canonical):** the font declares a single decimal version —
/// name-table ID 5 is literally `"Version 1.392"` and `head.fontRevision ≈ 1.392`
/// — so it is recorded verbatim as `major.minor = 1.392`, i.e. `SemVer { major: 1,
/// minor: 392, patch: 0 }`. The minor field carries the font's `392` unchanged
/// rather than inventing a `1.39.2` patch split the font never declares, so this
/// identifier round-trips to the font's own version string.
pub const BRAVURA_VERSION: SemVer = SemVer::new(1, 392, 0);
impl Default for GlyphCatalogIdentity {
/// The bundled Bravura identity, with `metrics_hash` over the *whole*
/// in-tree table. A [`crate::ConstrainedLayoutIR`] overrides the hash with
/// one over only the glyphs it references (the solve's true inputs).
fn default() -> Self {
GlyphCatalogIdentity {
smufl_version: SmuflVersion::from_decimal(1, "4")
.expect("1.4 is a valid SMuFL version"),
font_id: FontId::BRAVURA,
font_version: Some(BRAVURA_VERSION),
metrics_hash: metrics_hash_for(BRAVURA_METRICS.iter().map(|m| m.name.as_ref())),
}
}
}
/// A named anchor on a glyph (Chapter 7 §"Glyph Catalog Interface":
/// `GlyphMetrics.anchors`), e.g. a notehead's stem-attachment point, in
/// `1/1024`-staff-space units relative to the glyph origin.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct GlyphAnchor {
pub name: Cow<'static, str>,
pub x: i32,
pub y: i32,
}
/// One glyph's metrics: advance width, bounding box, and named anchors, in
/// `1/1024`-staff-space units (Chapter 7 §"Glyph Catalog Interface":
/// `GlyphMetrics`). Units are exact integers (hashable, deterministic);
/// [`GlyphMetric::bounding_box`] converts to staff-space `f32` on demand.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct GlyphMetric {
pub name: Cow<'static, str>,
pub advance: i32,
pub bbox: [i32; 4],
pub anchors: Cow<'static, [GlyphAnchor]>,
}
impl GlyphMetric {
/// Constructs a metrics entry with no named anchors.
pub const fn new(name: &'static str, advance: i32, bbox: [i32; 4]) -> Self {
GlyphMetric {
name: Cow::Borrowed(name),
advance,
bbox,
anchors: Cow::Borrowed(&[]),
}
}
/// Constructs a metrics entry with named anchors.
pub const fn anchored(
name: &'static str,
advance: i32,
bbox: [i32; 4],
anchors: &'static [GlyphAnchor],
) -> Self {
GlyphMetric {
name: Cow::Borrowed(name),
advance,
bbox,
anchors: Cow::Borrowed(anchors),
}
}
/// The bounding box in staff spaces (Chapter 7: `GlyphMetrics.bounding_box`).
pub fn bounding_box(&self) -> BoundingBox {
let g = |u: i32| u as f32 / 1024.0;
let [l, b, r, t] = self.bbox;
BoundingBox::new(g(l), g(b), g(r), g(t))
}
}
/// A representative in-tree slice of Bravura's SMuFL metrics
/// (`(name, advance, [left, bottom, right, top])`, `1/1024`-staff-space units),
/// extracted from the SHA-pinned `bravura-1.392` font by `epiphany-render-svg`'s
/// `tools/extract_bravura_outlines.py` — the **same** font the renderer's outlines
/// come from. Each `bbox` is the corresponding outline's bounds rounded *outward*
/// to the `1/1024` grid (floor the mins, ceil the maxes), so the metric box always
/// **contains** the drawn ink: the engraver evaluates collisions from this box, and
/// a containing box keeps a no-collision result honest on paper (a `render-svg` test
/// proves the containment). Every glyph the v0 pipeline names is in this table; the
/// stub solver checks that, so a missing entry surfaces as
/// [`crate::SolveStatus::InternalError`].
///
/// **No glyph carries anchors.** They live in the font's SMuFL metadata
/// (`bravura_metadata.json`), not its glyf bounds, so the outline extraction never
/// covered them and the two the table once held were written by hand — and were
/// wrong under any reading (P13-I3). `extract_bravura_outlines.py --anchors` now
/// emits them from the pinned metadata; the table regains them at the next
/// regeneration, generated rather than remembered. Nothing consumes anchors today:
/// a stem takes its attachment from the notehead's bounding box, whose edges are
/// the anchor x's the font declares.
pub const BRAVURA_METRICS: &[GlyphMetric] = &[
GlyphMetric::new("noteheadBlack", 1208, [0, -512, 1209, 512]),
GlyphMetric::new("noteheadHalf", 1208, [0, -512, 1209, 512]),
GlyphMetric::new("noteheadWhole", 1729, [0, -512, 1729, 512]),
GlyphMetric::new("noteheadDoubleWhole", 2454, [0, -635, 2454, 635]),
GlyphMetric::new("gClef", 2748, [0, -2696, 2749, 4498]),
GlyphMetric::new("fClef", 2802, [-21, -2601, 2802, 1074]),
GlyphMetric::new("cClef", 2863, [0, -2073, 2864, 2073]),
GlyphMetric::new("accidentalSharp", 1020, [0, -1426, 1020, 1434]),
GlyphMetric::new("accidentalFlat", 926, [0, -717, 926, 1799]),
GlyphMetric::new("accidentalNatural", 688, [0, -1373, 689, 1397]),
GlyphMetric::new("accidentalDoubleSharp", 1024, [0, -512, 1012, 521]),
GlyphMetric::new("restWhole", 1159, [0, -553, 1156, 37]),
GlyphMetric::new("restHalf", 1159, [0, -9, 1156, 582]),
GlyphMetric::new("restQuarter", 1106, [4, -1536, 1106, 1528]),
GlyphMetric::new("rest8th", 1024, [0, -1029, 1012, 713]),
GlyphMetric::new("flag8thUp", 1081, [0, -3319, 1082, 37]),
GlyphMetric::new("flag8thDown", 1253, [0, -58, 1254, 3310]),
GlyphMetric::new("augmentationDot", 410, [0, -205, 410, 205]),
// Time-signature digits and the common-time C — centred on the baseline
// (y ≈ ±1), so a numerator/denominator pair straddles the staff midline.
GlyphMetric::new("timeSig0", 1925, [81, -1024, 1844, 1029]),
GlyphMetric::new("timeSig1", 1368, [81, -1024, 1287, 1029]),
GlyphMetric::new("timeSig2", 1827, [81, -1053, 1745, 1041]),
GlyphMetric::new("timeSig3", 1724, [81, -1029, 1643, 1020]),
GlyphMetric::new("timeSig4", 1925, [81, -1024, 1844, 1029]),
GlyphMetric::new("timeSig5", 1651, [81, -1029, 1569, 1008]),
GlyphMetric::new("timeSig6", 1778, [81, -1020, 1696, 1029]),
GlyphMetric::new("timeSig7", 1806, [81, -1024, 1725, 1020]),
GlyphMetric::new("timeSig8", 1786, [81, -1061, 1704, 1061]),
GlyphMetric::new("timeSig9", 1778, [81, -1020, 1696, 1029]),
GlyphMetric::new("timeSigCommon", 1737, [20, -1020, 1737, 1029]),
// Barlines: the origin is the lower end; the line runs 0..4096 (four staff
// spaces) *up* from it, spanning the staff when anchored at the bottom line.
GlyphMetric::new("barlineSingle", 147, [0, 0, 148, 4096]),
GlyphMetric::new("barlineFinal", 938, [0, 0, 934, 4096]),
// Repeat signs: composite barline glyphs (heavy/thin lines plus dots),
// staff-spanning like the barlines above; `repeatDots` is the bare dot
// pair drawn beside a barline (its box sits in the middle two staff
// spaces when anchored at the bottom line).
GlyphMetric::new("repeatLeft", 1507, [0, 0, 1500, 4096]),
GlyphMetric::new("repeatRight", 1503, [4, 0, 1504, 4096]),
GlyphMetric::new("repeatRightLeft", 2490, [4, 0, 2491, 4096]),
GlyphMetric::new("repeatDots", 410, [0, 1302, 410, 2745]),
GlyphMetric::new("dynamicForte", 1491, [-578, -623, 1491, 1819]),
GlyphMetric::new("dynamicPiano", 1495, [-365, -582, 1500, 1123]),
];
/// Looks up one glyph's metrics by SMuFL name, if bundled.
pub fn metrics(name: &str) -> Option<&'static GlyphMetric> {
BRAVURA_METRICS.iter().find(|m| m.name.as_ref() == name)
}
/// Whether every name in `names` has bundled metrics.
pub fn all_available<'a>(names: impl IntoIterator<Item = &'a str>) -> bool {
names.into_iter().all(|n| metrics(n).is_some())
}
/// Whether a glyph name draws a **measure-boundary barline**: the plain and
/// final barlines plus the composite repeat signs (which replace a plain
/// barline at a repeat boundary). The casting-off solver classifies
/// measure-boundary columns with this instead of hard-coding the engraver's
/// name choices. `repeatDots` is decoration drawn *beside* a barline, not a
/// barline itself, so it is excluded.
pub fn is_barline_glyph(name: &str) -> bool {
name.starts_with("barline") || matches!(name, "repeatLeft" | "repeatRight" | "repeatRightLeft")
}
/// A glyph's rendering data (Chapter 7 §"Glyph Catalog Interface":
/// `GlyphRenderData`). The full outline/bitmap vocabulary (`PathCommand`,
/// `GlyphBitmap`) belongs to the out-of-core renderer; v0 carries an opaque
/// marker so the interface is complete without bundling outlines.
#[derive(Clone, PartialEq, Debug)]
pub enum PathCommand {
MoveTo(Point),
LineTo(Point),
CurveTo {
control1: Point,
control2: Point,
to: Point,
},
Close,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct GlyphBitmap {
pub width: u32,
pub height: u32,
pub rgba8: Vec<u8>,
}
#[derive(Clone, PartialEq, Debug, Default)]
pub struct GlyphRenderData {
pub outline: Vec<PathCommand>,
pub bitmap: Option<GlyphBitmap>,
}
/// The font-catalog query interface (Chapter 7 §"Glyph Catalog Interface":
/// `GlyphCatalog`). `Send + Sync` per the spec, so a catalog can be shared
/// across threads during parallel re-engraving.
pub trait GlyphCatalog: Send + Sync {
/// Resolve a glyph name to its metrics.
fn metrics(&self, name: &str) -> Option<&GlyphMetric>;
/// Resolve a glyph name to its rendering data, if any.
fn render_data(&self, name: &str) -> Option<GlyphRenderData>;
/// The SMuFL version this catalog supports.
fn smufl_version(&self) -> SmuflVersion;
/// This catalog's reproducibility identity over the given consulted names.
fn identity(&self, consulted: &[&str]) -> GlyphCatalogIdentity;
}
/// The bundled in-tree Bravura catalog. **Metric-only**: it bundles no render
/// data (outlines/bitmaps are a renderer concern), so
/// [`BravuraCatalog::render_data`] honestly returns `None` for every glyph.
pub struct BravuraCatalog;
impl GlyphCatalog for BravuraCatalog {
fn metrics(&self, name: &str) -> Option<&GlyphMetric> {
metrics(name)
}
fn render_data(&self, _name: &str) -> Option<GlyphRenderData> {
// No outlines or bitmaps are bundled; reporting `Some` would claim render
// data that does not exist.
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()
}
}
}
/// The catalog metrics identity (Chapter 7 §7.3.2): a **domain-tagged**
/// (`MUSCFNTM`) BLAKE3 hash over the canonical serialization of the consulted
/// glyph metrics (advance, bounding box, and named anchors), rather than a raw
/// hash of a descriptive string.
///
/// Names are de-duplicated and put in canonical (sorted) order first, so two
/// solves consulting the same metric set hash identically regardless of glyph
/// ordering (Appendix D §"Ordered Iteration"). Panics if a name has no bundled
/// metrics — every glyph delivered to a solve MUST name available metrics.
pub fn metrics_hash_for<'a>(names: impl IntoIterator<Item = &'a str>) -> [u8; 32] {
let names: BTreeSet<&str> = names.into_iter().collect();
let entries: Vec<(&str, &GlyphMetric)> = names
.into_iter()
.map(|name| {
(
name,
metrics(name).expect("every consulted glyph must name bundled metrics"),
)
})
.collect();
metrics_hash_of(&entries)
}
/// Hashes `(name, metrics)` pairs already in canonical name order — the body of
/// [`metrics_hash_for`], factored out so a test can hash metrics that are not in
/// the bundled table (the only way to prove a *field* participates in the hash is
/// to vary it while holding every other field fixed).
fn metrics_hash_of(entries: &[(&str, &GlyphMetric)]) -> [u8; 32] {
let mut p = Preimage::new(DomainTag::FONT_METRICS);
p.push_u64_le(entries.len() as u64);
for (name, m) in entries {
p.push_u64_le(name.len() as u64);
p.push_bytes(name.as_bytes());
p.push_u64_le(m.advance as u64);
for coord in m.bbox {
p.push_u64_le(coord as u64);
}
// Anchors are a *map* keyed by name (Chapter 7 §"Glyph Catalog
// Interface": `anchors: HashMap<AnchorName, Point2D>`), so hash them in
// canonical name order (Appendix D §"Ordered Iteration over Sets and
// Maps"). A duplicate anchor name is invalid map data and is **rejected**
// (a panic), not silently order-collapsed — so the hash never depends on
// anchor slice order.
let mut anchors: BTreeMap<&str, (i32, i32)> = BTreeMap::new();
for a in m.anchors.iter() {
assert!(
anchors.insert(a.name.as_ref(), (a.x, a.y)).is_none(),
"glyph {} has a duplicate anchor name {}",
name,
a.name
);
}
p.push_u64_le(anchors.len() as u64);
for (anchor_name, (x, y)) in anchors {
p.push_u64_le(anchor_name.len() as u64);
p.push_bytes(anchor_name.as_bytes());
p.push_u64_le(x as u64);
p.push_u64_le(y as u64);
}
}
*p.finish().as_bytes()
}
/// The bundled Bravura catalog identity (the [`GlyphCatalogIdentity::default`]).
pub fn bravura_catalog_identity() -> GlyphCatalogIdentity {
GlyphCatalogIdentity::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metrics_hash_is_domain_tagged_and_nonzero() {
let h = bravura_catalog_identity().metrics_hash;
assert_ne!(h, [0u8; 32]);
assert_eq!(GlyphCatalogIdentity::default().metrics_hash, h);
assert_eq!(
GlyphCatalogIdentity::default().font_version,
Some(BRAVURA_VERSION)
);
}
/// P13-S12 / Push 4b tranche 3b-ii: the live bug this tranche fixes.
/// Layout-ir used to define its own `SmuflVersion { major: u16, minor:
/// u16 }` storing the minor digit *literally*, so derived `Ord` sorted
/// 1.3 and 1.4 **before** 1.12 — backwards versus SMuFL's real release
/// order (1.12 → 1.18 → 1.20 → 1.3 → 1.4). Exercised through
/// [`GlyphCatalogIdentity::smufl_version`], not the bare type, because
/// that field is the layout-conformance identity the bug actually lived
/// on.
///
/// Non-vacuous: reverting `crate::SmuflVersion` to the deleted literal-minor
/// shape (`{ major: u16, minor: u16 }` with derived `Ord`) and rebuilding
/// each identity as `{ major: 1, minor: <the literal digits> }` makes this
/// test fail — `{1,3}` and `{1,4}` sort *before* `{1,12}`, breaking the
/// third assertion in the loop below. Verified by hand against the
/// pre-unification type before it was deleted.
#[test]
fn catalog_identity_smufl_version_orders_the_real_release_sequence() {
let identity_with = |minor_digits: &str| GlyphCatalogIdentity {
smufl_version: SmuflVersion::from_decimal(1, minor_digits)
.expect("valid SMuFL version"),
..GlyphCatalogIdentity::default()
};
let sequence = ["12", "18", "20", "3", "4"].map(identity_with);
for pair in sequence.windows(2) {
assert!(
pair[0].smufl_version < pair[1].smufl_version,
"{:?} did not sort before {:?} in SMuFL's real release order",
pair[0].smufl_version,
pair[1].smufl_version
);
}
}
#[test]
fn metrics_hash_is_order_and_duplicate_independent() {
let a = metrics_hash_for(["gClef", "noteheadBlack", "accidentalSharp"]);
let b = metrics_hash_for(["accidentalSharp", "gClef", "noteheadBlack", "gClef"]);
assert_eq!(a, b);
}
/// A glyph's anchors are part of its catalog identity: two catalogs whose
/// glyphs differ only in an anchor are different inputs, and a byte-equal
/// conformance claim across them is not expected.
///
/// This must be shown by varying the anchors while holding every other field
/// fixed. The previous version of this test compared `noteheadBlack` (which
/// carried anchors) with `noteheadWhole` (which did not) and asserted they
/// hash differently — but their `advance` and `bbox` differ too, so it would
/// have passed with the anchors ignored entirely. It proved nothing.
#[test]
fn anchors_participate_in_the_hash() {
let bare = GlyphMetric::new("g", 1024, [0, 0, 1024, 1024]);
let anchored = GlyphMetric {
anchors: Cow::Owned(vec![GlyphAnchor {
name: Cow::Owned("stemUpSE".to_owned()),
x: 1208,
y: 172,
}]),
..bare.clone()
};
let moved = GlyphMetric {
anchors: Cow::Owned(vec![GlyphAnchor {
name: Cow::Owned("stemUpSE".to_owned()),
x: 1209,
y: 172,
}]),
..bare.clone()
};
let renamed = GlyphMetric {
anchors: Cow::Owned(vec![GlyphAnchor {
name: Cow::Owned("stemDownNW".to_owned()),
x: 1208,
y: 172,
}]),
..bare.clone()
};
let hash = |m: &GlyphMetric| metrics_hash_of(&[("g", m)]);
assert_ne!(hash(&bare), hash(&anchored), "presence changes the hash");
assert_ne!(hash(&anchored), hash(&moved), "a coordinate does too");
assert_ne!(hash(&anchored), hash(&renamed), "and so does a name");
}
/// The bundled table carries no anchors. They were hand-written where every
/// other number in it is machine-extracted from the SHA-pinned font, and they
/// were wrong under any reading: they named `stemUpNW`/`stemDownSE` — corners
/// a normal notehead's stems do not attach to, and a pair Bravura's
/// `noteheadBlack` does not define — with an x of 1180, which reads as 1.18
/// staff spaces written in thousandths rather than the table's 1/1024 units
/// (1.18 sp = 1208). Nothing consumed them. `extract_bravura_outlines.py`
/// now emits anchors from `bravura_metadata.json`; the table regains them at
/// the next regeneration, generated rather than remembered (P13-I3).
#[test]
fn the_bundled_table_carries_no_hand_written_anchors() {
for metric in BRAVURA_METRICS {
assert!(
metric.anchors.is_empty(),
"{} carries hand-written anchors",
metric.name
);
}
}
#[test]
fn catalog_trait_resolves_and_identifies() {
let cat = BravuraCatalog;
assert_eq!(cat.metrics("gClef"), metrics("gClef"));
assert!(cat.metrics("noSuchGlyph").is_none());
assert_eq!(
cat.identity(&["gClef"]).metrics_hash,
metrics_hash_for(["gClef"])
);
}
#[test]
fn runtime_catalog_can_use_owned_names_and_report_identity_through_dyn_trait() {
struct RuntimeCatalog {
metric: GlyphMetric,
}
impl GlyphCatalog for RuntimeCatalog {
fn metrics(&self, name: &str) -> Option<&GlyphMetric> {
(self.metric.name.as_ref() == name).then_some(&self.metric)
}
fn render_data(&self, _name: &str) -> Option<GlyphRenderData> {
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 {
smufl_version: self.smufl_version(),
font_id: FontId::owned("Runtime Font"),
font_version: None,
metrics_hash: [7; 32],
}
}
}
let catalog: Box<dyn GlyphCatalog> = Box::new(RuntimeCatalog {
metric: GlyphMetric {
name: Cow::Owned("runtimeGlyph".to_owned()),
advance: 1024,
bbox: [0, 0, 1024, 1024],
anchors: Cow::Owned(vec![GlyphAnchor {
name: Cow::Owned("runtimeAnchor".to_owned()),
x: 0,
y: 0,
}]),
},
});
assert!(catalog.metrics("runtimeGlyph").is_some());
assert_eq!(catalog.identity(&["runtimeGlyph"]).metrics_hash, [7; 32]);
}
#[test]
fn barline_classification_covers_the_repeat_signs_but_not_the_dots() {
for name in [
"barlineSingle",
"barlineFinal",
"repeatLeft",
"repeatRight",
"repeatRightLeft",
] {
assert!(
is_barline_glyph(name),
"{name} is a measure-boundary barline"
);
}
for name in ["repeatDots", "noteheadBlack", "timeSig2", "gClef"] {
assert!(!is_barline_glyph(name), "{name} is not a barline");
}
}
#[test]
fn every_bundled_name_is_unique_and_resolvable() {
let mut seen = BTreeSet::new();
for m in BRAVURA_METRICS {
assert!(
seen.insert(m.name.as_ref()),
"duplicate bundled glyph {}",
m.name
);
assert_eq!(metrics(m.name.as_ref()), Some(m));
}
assert!(all_available(
BRAVURA_METRICS.iter().map(|m| m.name.as_ref())
));
assert!(!all_available(["noSuchGlyph"]));
}
}