From 74c6fef0d31eba5ef739a859f555ed206e7cac9e Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 13:54:13 -0800 Subject: [PATCH 1/7] Read a VARC delta set that ends early as HarfBuzz does A MultiItemVariationStore delta set that ended before it filled its tuples (one per region of its subtable) added nothing: the reader checked the whole set first. HarfBuzz's MultiVarData::get_delta adds what it finds, region by region, so on a crafted font a component moved by its first values in HarfBuzz and not at all in sigilbuzz. The reader now walks such a set as HarfBuzz's TupleValues::fetcher_t does: a region's tuple stops where the stream does, and a run whose values do not fit the bytes left ends that region's tuple (or the lazy skip past regions whose scalar is zero) after its control byte, so the next region's tuple starts at the byte after it. It used to end the stream there. A Value condition now adds the summed deltas to its default value, as HarfBuzz's instancer returns them, instead of adding each region's delta to the default in turn; the two can round differently in f32. The sigilbuzz-subset round-trip test that encoded the old reading (each delta set holds one value for two translations) now expects HarfBuzz's: at axis 0 = 1 each set's value moves X and Y keeps its stored value, which also checks that every remapped variation index still reads its own set. --- .../tests/varc_round_trip/mvs.rs | 30 +++++++---- src/tables/multi_var_store.rs | 50 ++++++++----------- src/tables/varc.rs | 9 ++-- src/tables/varc/tests.rs | 41 ++++++++++++++- 4 files changed, 86 insertions(+), 44 deletions(-) diff --git a/crates/sigilbuzz-subset/tests/varc_round_trip/mvs.rs b/crates/sigilbuzz-subset/tests/varc_round_trip/mvs.rs index 80e849d1..414fd13b 100644 --- a/crates/sigilbuzz-subset/tests/varc_round_trip/mvs.rs +++ b/crates/sigilbuzz-subset/tests/varc_round_trip/mvs.rs @@ -108,8 +108,9 @@ fn build_mvs_varc() -> Vec { }; // MVS: 2 subtables, 5 entries each = 10 total. Each delta set is - // a single i8 zero (0x00, 0x00): `0x00` ctrl = run of 1 i8, `0x00` - // payload. Two bytes per entry. + // one i8: `0x00` ctrl = run of 1 i8, then the value, `i` for entry + // `i` of subtable 0 and `10 + i` for subtable 1. Two bytes per + // entry. let mvs_subtables: Vec<(Vec, Vec>)> = vec![ (vec![0], (0..5).map(|i| vec![0x00_u8, i as u8]).collect()), ( @@ -471,17 +472,26 @@ fn varc_subset_prunes_mvs_and_remaps_var_idx_orphan_free() { // gid 1 has 3 components, gid 2 has 3 components. assert_eq!(comp.components.len(), 3, "gid {old} component count"); // Translation values survive (verifies the var-idx splice did - // not corrupt the trailing transform fields). + // not corrupt the trailing transform fields), and each component + // still reads its own delta set. A set holds one value for the + // two translations, so at axis 0 = 1 its value moves X, as + // HarfBuzz reads a set that ends early, and Y keeps its stored + // value: gid 1 names (0,0) (0,1) (0,2), whose values are 0 1 2; + // gid 2 names (0,3) (0,4) (1,0), whose values are 3 4 10. + let (stored, deltas): (f32, [f32; 3]) = if old == 1 { + (1.0, [0.0, 1.0, 2.0]) + } else { + (4.0, [3.0, 4.0, 10.0]) + }; for (i, c) in comp.components.iter().enumerate() { - let tx_expected = if old == 1 { - f32::from((i + 1) as i16) - } else { - f32::from((i + 4) as i16) - }; + let ty_expected = stored + i as f32; + let tx_expected = ty_expected + deltas[i]; assert!( - (c.transform[4] - tx_expected).abs() < 1e-3, - "gid {old} comp {i} tx {} expected {tx_expected}", + (c.transform[4] - tx_expected).abs() < 1e-3 + && (c.transform[5] - ty_expected).abs() < 1e-3, + "gid {old} comp {i} translate ({}, {}) expected ({tx_expected}, {ty_expected})", c.transform[4], + c.transform[5], ); } } diff --git a/src/tables/multi_var_store.rs b/src/tables/multi_var_store.rs index c517ab82..b632a55e 100644 --- a/src/tables/multi_var_store.rs +++ b/src/tables/multi_var_store.rs @@ -398,9 +398,11 @@ impl<'a> MultiVarStore<'a> { /// region (region 0's tuple, then region 1's, ...), as the spec, /// fontTools and HarfBuzz lay it out. Values past the last region's /// tuple are ignored, as in HarfBuzz. A stream that ends before it - /// fills every tuple is malformed and adds nothing; HarfBuzz adds - /// the values it does find, so such a set varies only some of the - /// fields it names there. + /// fills every tuple adds the values it holds, as HarfBuzz's + /// `MultiItemVariationStore::get_delta` does: a region's tuple stops + /// where the stream does, and a run whose values do not fit the + /// bytes left ends that region's tuple after its control byte, so + /// the next region's tuple starts at the byte after it. /// /// Returns `None` when `outer` or `inner` is out of range, or when /// the delta set is too short to hold `value_count` values even for @@ -429,7 +431,14 @@ impl<'a> MultiVarStore<'a> { /// `MultiItemVariationStore::get_delta` adds them to the values it /// varies: each region's scalar times its tuple, added in place. /// Regions whose scalar is zero are skipped. Does nothing when the - /// indices are out of range or the set is too short for its tuples. + /// indices are out of range. + /// + /// A set that ends early adds what it holds, read as HarfBuzz's + /// `TupleValues::fetcher_t` reads it: a region's tuple stops where + /// the stream does, and a run whose values do not fit the bytes left + /// ends that region's tuple (or the skip past regions whose scalar + /// is zero) after its control byte, so the next region's tuple + /// starts at the byte after that control byte. pub(crate) fn add_deltas(&self, outer: u16, inner: u32, coords: &[f32], out: &mut [f32]) { let Some(slot) = self.subtable_slot(outer) else { return; @@ -477,10 +486,6 @@ impl<'a> MultiVarStore<'a> { let Some(&raw) = sub.delta_sets.get(inner as usize) else { return; }; - let needed = out.len().saturating_mul(sub.region_indexes.len()); - if !TupleFetcher::new(raw).holds(needed) { - return; - } let mut values = TupleFetcher::new(raw); let mut skip = 0usize; for &scalar in scalars { @@ -495,8 +500,10 @@ impl<'a> MultiVarStore<'a> { } /// Reads a `TupleValues` stream lazily, as HarfBuzz's -/// `TupleValues::fetcher_t` does: a run that does not fit the -/// remaining bytes ends the stream, and reads past the end add nothing. +/// `TupleValues::fetcher_t` does: reads past the end add nothing, and a +/// run whose values do not fit the remaining bytes stops the read that +/// reached it after its control byte, so the next read takes the byte +/// after that control byte as a control byte. struct TupleFetcher<'a> { data: &'a [u8], /// Values left in the current run. @@ -514,8 +521,9 @@ impl<'a> TupleFetcher<'a> { } } - /// Starts the next run if the current one is used up. False once - /// the stream has ended. + /// Starts the next run if the current one is used up. False when + /// the stream has ended, or when the next run does not fit the bytes + /// after its control byte, which is then used up. fn ensure_run(&mut self) -> bool { if self.run > 0 { return true; @@ -523,6 +531,7 @@ impl<'a> TupleFetcher<'a> { let Some((&control, rest)) = self.data.split_first() else { return false; }; + self.data = rest; let run = usize::from(control & 0x3F) + 1; let width = match control & 0xC0 { 0x80 => 0, @@ -531,10 +540,8 @@ impl<'a> TupleFetcher<'a> { _ => 4, }; if rest.len() < run * width { - self.data = &[]; return false; } - self.data = rest; self.run = run; self.width = width; true @@ -560,21 +567,6 @@ impl<'a> TupleFetcher<'a> { value } - /// Whether the stream holds at least `n` values, every run fitting - /// its bytes. - fn holds(mut self, n: usize) -> bool { - let mut seen = 0usize; - while seen < n { - if !self.ensure_run() { - return false; - } - seen += self.run; - self.data = &self.data[self.run * self.width..]; - self.run = 0; - } - true - } - fn skip(&mut self, mut n: usize) { while n > 0 && self.ensure_run() { let k = n.min(self.run); diff --git a/src/tables/varc.rs b/src/tables/varc.rs index 94800daa..b38e3b3a 100644 --- a/src/tables/varc.rs +++ b/src/tables/varc.rs @@ -570,9 +570,12 @@ impl<'a> Varc<'a> { let (Some(default), Some(index)) = (be_u16(data, 2), be_u32(data, 4)) else { return false; }; - let mut value = [f32::from(default as i16)]; - // Out of delta budget the value is unknown, so it fails. - self.add_deltas(index, &mut value, eval) && value[0] > 0.0 + // HarfBuzz sums the deltas on their own, then adds the + // sum to the default value. Out of delta budget the value + // is unknown, so the condition fails. + let mut delta = [0.0]; + self.add_deltas(index, &mut delta, eval) + && f32::from(default as i16) + delta[0] > 0.0 } Some(format @ (3 | 4)) => { let Some(&count) = data.get(2) else { diff --git a/src/tables/varc/tests.rs b/src/tables/varc/tests.rs index d0bcd91f..ccb2fda5 100644 --- a/src/tables/varc/tests.rs +++ b/src/tables/varc/tests.rs @@ -501,7 +501,9 @@ fn delta_sets_hold_one_tuple_per_region() { assert_eq!(t(&[1.0, 0.0]), (10.0, 20.0)); assert_eq!(t(&[0.0, 1.0]), (-3.0, 5.0)); assert_eq!(t(&[0.5, 1.0]), (2.0, 15.0)); - // A delta set that ends before it fills both tuples adds nothing. + // A delta set that ends before it fills both tuples adds the values + // it holds, as HarfBuzz does: region 0's tuple, then the first value + // of region 1's. let short = build_store( &[&[(0, 0.0, 1.0, 1.0)], &[(1, 0.0, 1.0, 1.0)]], &[&[0x02, 10, 20, 7]], @@ -509,7 +511,14 @@ fn delta_sets_hold_one_tuple_per_region() { let c = first(&record, Some(&short), None, &[1.0, 1.0]); assert_eq!( (c.components[0].transform[4], c.components[0].transform[5]), - (0.0, 0.0) + (17.0, 20.0) + ); + // At axis 1 alone only region 1 counts: the reader skips region + // 0's tuple, and region 1's holds one value. + let c = first(&record, Some(&short), None, &[0.0, 1.0]); + assert_eq!( + (c.components[0].transform[4], c.components[0].transform[5]), + (7.0, 0.0) ); // Extra values past the last tuple are ignored. let long = build_store( @@ -523,6 +532,34 @@ fn delta_sets_hold_one_tuple_per_region() { ); } +#[test] +fn a_run_cut_short_ends_one_tuple_at_its_control_byte() { + // Region 0's tuple starts with a run of two words that the one + // byte after it cannot hold. HarfBuzz's reader gives up on that + // tuple past the control byte, and region 1's tuple starts at the + // next byte, read as a control byte: one i8, 5. + let store = build_store( + &[&[(0, 0.0, 1.0, 1.0)], &[(1, 0.0, 1.0, 1.0)]], + &[&[0x41, 0x00, 0x05]], + ); + let flags = VC_TRANSFORM_HAS_VARIATION | VC_HAVE_TRANSLATE_X | VC_HAVE_TRANSLATE_Y; + let mut record = vec![flags as u8, 0x00, 0x05, 0x00]; + record.extend_from_slice(&0i16.to_be_bytes()); + record.extend_from_slice(&0i16.to_be_bytes()); + let c = first(&record, Some(&store), None, &[1.0, 1.0]); + assert_eq!( + (c.components[0].transform[4], c.components[0].transform[5]), + (5.0, 0.0) + ); + // Skipping region 0 (scalar zero) stops at the same control byte, + // so region 1 reads the same way. + let c = first(&record, Some(&store), None, &[0.0, 1.0]); + assert_eq!( + (c.components[0].transform[4], c.components[0].transform[5]), + (5.0, 0.0) + ); +} + #[test] fn deltas_skip_the_default_instance_and_no_variation() { // A region without axes has scalar 1 everywhere, but HarfBuzz adds From b11f275e49a25ea9eb0c698a5ec9ef3fc7df9156 Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 13:54:13 -0800 Subject: [PATCH 2/7] Draw VARC composites in one walk from the font's coords Face::glyph_outline_at_coords resolved a nested VARC composite by calling itself with the component's coords, so Varc::composite took them as the font's: a RESET_UNSPECIFIED_AXES component one or more composites down started from its parent's coords instead of the font's. A component naming its own glyph recursed until the 64-level cap and failed the glyph, where HarfBuzz draws that glyph's plain outline. A VARC glyph is now drawn by one walk (src/face/varc_draw.rs), as HarfBuzz's VARC::get_path_at walks it: - Every composite is resolved with composite_with_font_coords and the coords of the glyph drawn, so a reset starts from the font's coords however deep it sits. - A component whose glyph is its parent's draws that glyph from glyf or CFF at the component's coords. - Transforms compose top down, in hb_transform_t's order, and each leaf is drawn once through a sink that places its points (x0 + xx*x + xy*y, as HarfBuzz's transform_point sums them) instead of copying every nested outline into its parent's. - The leaves' tables (CFF2, CFF, or glyf with loca, hmtx, vmtx and, away from the default instance, gvar) are read once per walk, in the order the face reads them, instead of once per leaf. The component, op and depth caps are unchanged, except that a leaf's ops now count once rather than once per level they were copied through. GlyphOutlines draws a VARC glyph with the same walk and its cached VARC table, streaming into the sink, instead of resolving the composite once to test it and then asking the face. --- src/face.rs | 1 + src/face/glyph_outlines.rs | 37 +---- src/face/outlines.rs | 159 +++--------------- src/face/varc_draw.rs | 327 +++++++++++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+), 163 deletions(-) create mode 100644 src/face/varc_draw.rs diff --git a/src/face.rs b/src/face.rs index 83218b28..83e104de 100644 --- a/src/face.rs +++ b/src/face.rs @@ -36,6 +36,7 @@ mod color; mod glyph_outlines; mod layout; mod outlines; +mod varc_draw; mod variations; use alloc::vec::Vec; diff --git a/src/face/glyph_outlines.rs b/src/face/glyph_outlines.rs index b6d891a7..76d53ff9 100644 --- a/src/face/glyph_outlines.rs +++ b/src/face/glyph_outlines.rs @@ -6,14 +6,13 @@ use core::fmt; use alloc::vec::Vec; +use super::varc_draw::VarcDraw; use super::Face; use crate::error::Result; use crate::font::f2dot14_coords; use crate::tables::cff2::Cff2Shared; use crate::tables::glyf::PhantomMetrics; -use crate::tables::{ - tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, Outline, OutlineSink, PathOp, Varc, Vmtx, -}; +use crate::tables::{tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, Outline, OutlineSink, Varc, Vmtx}; /// Draws glyphs of one face at one set of variation coords, reading the /// outline tables once for all of them. @@ -117,15 +116,11 @@ impl<'a> GlyphOutlines<'a> { .get_or_init(|| self.face.varc()) .as_ref() .map_err(Clone::clone)?; - // A VARC composite is drawn the way the face draws it: its - // components carry coords of their own. + // A VARC composite is drawn by the walk the face draws it with: + // its components carry coords of their own, so its leaves come + // from tables the walk reads for them. if let Some(varc) = varc { - if varc.covers(glyph_id) && varc.composite(glyph_id, &self.coords).is_some() { - let Some(outline) = self.face.glyph_outline_at_coords(glyph_id, &self.coords)? - else { - return Ok(false); - }; - replay(&outline, sink); + if VarcDraw::new(&self.face, varc, &self.coords).draw(glyph_id, sink)? { return Ok(true); } } @@ -190,26 +185,6 @@ impl<'a> GlyphOutlines<'a> { } } -/// Sends the ops of `outline` to `sink`. -fn replay(outline: &Outline, sink: &mut S) { - for op in outline.ops() { - match *op { - PathOp::MoveTo { x, y } => sink.move_to(x, y), - PathOp::LineTo { x, y } => sink.line_to(x, y), - PathOp::QuadTo { cx, cy, x, y } => sink.quad_to(cx, cy, x, y), - PathOp::CubicTo { - c1x, - c1y, - c2x, - c2y, - x, - y, - } => sink.curve_to(c1x, c1y, c2x, c2y, x, y), - PathOp::Close => sink.close(), - } - } -} - impl fmt::Debug for GlyphOutlines<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("GlyphOutlines") diff --git a/src/face/outlines.rs b/src/face/outlines.rs index 9abf66a5..b284c2a6 100644 --- a/src/face/outlines.rs +++ b/src/face/outlines.rs @@ -3,12 +3,13 @@ use alloc::vec::Vec; +use super::varc_draw::VarcDraw; use super::Face; -use crate::error::{Error, Result}; +use crate::error::Result; use crate::font::f2dot14_coords; use crate::tables::glyf::PhantomMetrics; use crate::tables::parse::hb_roundf; -use crate::tables::{tag, Cff, Cff2, Glyf, GlyphBounds, Gvar, Loca, Outline, PathOp, Vmtx}; +use crate::tables::{tag, Cff, Cff2, Glyf, GlyphBounds, Gvar, Loca, Outline, Vmtx}; impl<'a> Face<'a> { /// Parses the `loca` table. Pulls the offset format from `head` @@ -61,7 +62,8 @@ impl<'a> Face<'a> { /// Returns the design-unit bounding box for `glyph_id`, or /// `Ok(None)` when the glyph has no outline (e.g. a space /// glyph). Requires both `loca` and `glyf`. Fonts that use CFF - /// outlines instead will yield [`Error::MissingTable`] for + /// outlines instead will yield + /// [`Error::MissingTable`](crate::Error::MissingTable) for /// `glyf`. pub fn glyph_bounds(&self, glyph_id: u16) -> Result> { let loca = self.loca()?; @@ -171,81 +173,37 @@ impl<'a> Face<'a> { /// zero draw the default instance. The table-level methods /// ([`Glyf::outline_at_coords`], [`Cff2::outline`]) take coords as /// given. + /// + /// A glyph the font's `VARC` table has a record for is drawn from + /// its components, as HarfBuzz draws it: + /// + /// - Each component is drawn at its own coords, which start from + /// the coords of the glyph it belongs to, or from the font's + /// (`coords`) when it sets `RESET_UNSPECIFIED_AXES`, however deep + /// it sits. + /// - A component that names the glyph it belongs to draws that + /// glyph's `glyf` or CFF outline instead of recursing. + /// + /// A glyph that draws more than 2048 components or 2^20 ops, or nests + /// components more than 64 deep, fails with `Malformed`. pub fn glyph_outline_at_coords( &self, glyph_id: u16, coords: &[f32], ) -> Result> { - let mut budget = VarcBudget { - components_left: MAX_VARC_COMPONENTS, - ops_left: MAX_VARC_OPS, - }; let coords = f2dot14_coords(coords); - self.glyph_outline_at_coords_inner(glyph_id, &coords, 0, &mut budget) - } - - /// Recursive entry point used by VARC composite resolution. - /// `depth` caps recursion through nested VARC composites the - /// same way [`Glyf::flatten`] caps `glyf` composites. `budget` - /// caps the total work: depth alone still lets a glyph whose - /// components share children expand exponentially. - fn glyph_outline_at_coords_inner( - &self, - glyph_id: u16, - coords: &[f32], - depth: u8, - budget: &mut VarcBudget, - ) -> Result> { - const MAX_VARC_DEPTH: u8 = 64; - if depth > MAX_VARC_DEPTH { - return Err(Error::Malformed { - offset: 0, - context: "VARC composite recursion exceeded cap", - }); - } - - // VARC routing: if the font ships a VARC table that covers - // this gid, recurse through the resolved components and apply - // each component's affine to the child outline. Children - // outside VARC's coverage fall through to the regular glyf / - // CFF path with the component's effective coord vector. if let Some(varc) = self.varc()? { - if varc.covers(glyph_id) { - if let Some(composite) = varc.composite(glyph_id, coords) { - let mut out = Outline::new(); - for comp in &composite.components { - budget.components_left = - budget - .components_left - .checked_sub(1) - .ok_or(Error::Malformed { - offset: 0, - context: "VARC composite exceeds component budget", - })?; - let child = self.glyph_outline_at_coords_inner( - comp.gid, - &comp.coords, - depth + 1, - budget, - )?; - if let Some(child) = child { - budget.ops_left = budget - .ops_left - .checked_sub(child.ops().len()) - .ok_or(Error::Malformed { - offset: 0, - context: "VARC composite exceeds outline budget", - })?; - for op in child.ops() { - out.push(transform_path_op(*op, comp.transform)); - } - } - } - return Ok(Some(out)); - } + let mut out = Outline::new(); + if VarcDraw::new(self, &varc, &coords).draw(glyph_id, &mut out)? { + return Ok(Some(out)); } } + self.plain_outline_at_coords(glyph_id, &coords) + } + /// The outline of `glyph_id` at `coords` (rounded to F2DOT14) from + /// `glyf` or CFF, without `VARC`. + fn plain_outline_at_coords(&self, glyph_id: u16, coords: &[f32]) -> Result> { // CFF / CFF2 path: presence of `CFF2` wins over `CFF ` since // variable fonts ship only CFF2. `CFF ` is used only when the // font has no `glyf`. A font carrying both takes the TrueType @@ -369,73 +327,10 @@ impl PointBox { } } -/// Most VARC components one outline request may resolve, summed over -/// every nesting level. Mirrors HarfBuzz's graph edge cap. Real VARC -/// glyphs use a few dozen. -const MAX_VARC_COMPONENTS: usize = 2048; - -/// Most path ops VARC composition may copy into composite outlines -/// for one outline request, summed over every nesting level. -const MAX_VARC_OPS: usize = 1 << 20; - -/// Remaining work for one [`Face::glyph_outline_at_coords`] call. -/// Components that share children can make the resolved outline -/// grow exponentially with depth, so the whole request shares one -/// budget and fails with `Malformed` when it runs out. -struct VarcBudget { - components_left: usize, - ops_left: usize, -} - -/// Applies a row-major `[xx, xy, yx, yy, tx, ty]` affine to a single -/// path op, transforming every point inside it. Control points and -/// endpoints alike receive the same affine, which is correct for -/// affine maps because they preserve the "control point ratio" -/// implied by Bezier evaluation. -fn transform_path_op(op: PathOp, m: [f32; 6]) -> PathOp { - let xform = - |x: f32, y: f32| -> (f32, f32) { (m[0] * x + m[1] * y + m[4], m[2] * x + m[3] * y + m[5]) }; - match op { - PathOp::MoveTo { x, y } => { - let (x, y) = xform(x, y); - PathOp::MoveTo { x, y } - } - PathOp::LineTo { x, y } => { - let (x, y) = xform(x, y); - PathOp::LineTo { x, y } - } - PathOp::QuadTo { cx, cy, x, y } => { - let (cx, cy) = xform(cx, cy); - let (x, y) = xform(x, y); - PathOp::QuadTo { cx, cy, x, y } - } - PathOp::CubicTo { - c1x, - c1y, - c2x, - c2y, - x, - y, - } => { - let (c1x, c1y) = xform(c1x, c1y); - let (c2x, c2y) = xform(c2x, c2y); - let (x, y) = xform(x, y); - PathOp::CubicTo { - c1x, - c1y, - c2x, - c2y, - x, - y, - } - } - PathOp::Close => PathOp::Close, - } -} - #[cfg(test)] mod tests { use super::*; + use crate::tables::PathOp; fn bounds(points: &[(f32, f32)]) -> GlyphBounds { let mut b = PointBox::default(); diff --git a/src/face/varc_draw.rs b/src/face/varc_draw.rs new file mode 100644 index 00000000..ae8cb716 --- /dev/null +++ b/src/face/varc_draw.rs @@ -0,0 +1,327 @@ +//! Drawing a `VARC` composite: the walk over its components that +//! [`Face::glyph_outline_at_coords`] and [`crate::GlyphOutlines`] make, +//! as HarfBuzz's `VARC::get_path_at` makes it. + +use core::cell::OnceCell; + +use super::Face; +use crate::error::{Error, Result}; +use crate::tables::glyf::PhantomMetrics; +use crate::tables::varc::VarcComposite; +use crate::tables::{tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, OutlineSink, Varc, Vmtx}; + +/// Deepest a component may sit below the glyph drawn, as the `glyf` +/// composite walk caps its depth. +const MAX_VARC_DEPTH: u8 = 64; + +/// Most VARC components one glyph may draw, summed over every nesting +/// level. Real VARC glyphs use a few dozen. +const MAX_VARC_COMPONENTS: usize = 2048; + +/// Most path ops one glyph's components may draw, summed over all of +/// them. +const MAX_VARC_OPS: usize = 1 << 20; + +/// The identity affine, in [`crate::tables::VarcComponent::transform`]'s +/// `[xx, xy, yx, yy, tx, ty]` order. +const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; + +/// One glyph drawn through `VARC`. +/// +/// Every composite the walk reaches is resolved in the font set to the +/// coords of the glyph drawn, so a `RESET_UNSPECIFIED_AXES` component +/// starts from them however deep it sits. Components that share +/// children can make the walk grow exponentially with depth, so it +/// draws at most [`MAX_VARC_COMPONENTS`] components and +/// [`MAX_VARC_OPS`] ops, and fails with `Malformed` past either or +/// past [`MAX_VARC_DEPTH`]. +pub(crate) struct VarcDraw<'w, 'a> { + face: &'w Face<'a>, + varc: &'w Varc<'a>, + /// The font's coords, rounded to F2DOT14. + font_coords: &'w [f32], + leaves: Leaves<'a>, + components_left: usize, + ops_left: usize, +} + +impl<'w, 'a> VarcDraw<'w, 'a> { + /// A walk over `varc`, the `VARC` table of `face`, in a font set to + /// `font_coords` (rounded to F2DOT14). + pub(crate) fn new(face: &'w Face<'a>, varc: &'w Varc<'a>, font_coords: &'w [f32]) -> Self { + Self { + face, + varc, + font_coords, + leaves: Leaves::default(), + components_left: MAX_VARC_COMPONENTS, + ops_left: MAX_VARC_OPS, + } + } + + /// Draws `glyph_id` into `sink`, as HarfBuzz draws a glyph `VARC` + /// has a record for. Returns `Ok(false)`, drawing nothing, when it + /// has none; the glyph is then drawn from `glyf` or CFF. + pub(crate) fn draw(&mut self, glyph_id: u16, sink: &mut S) -> Result { + let coords = self.font_coords; + let Some(composite) = self.varc.composite(glyph_id, coords) else { + return Ok(false); + }; + self.draw_composite(glyph_id, &composite, IDENTITY, 0, sink)?; + Ok(true) + } + + /// Draws the components of glyph `gid`, whose composite is + /// `composite`, under `transform`. `depth` is the glyph's depth + /// below the glyph drawn. + fn draw_composite( + &mut self, + gid: u16, + composite: &VarcComposite, + transform: [f32; 6], + depth: u8, + sink: &mut S, + ) -> Result<()> { + for component in &composite.components { + self.components_left = self + .components_left + .checked_sub(1) + .ok_or(Error::Malformed { + offset: 0, + context: "VARC composite exceeds component budget", + })?; + let transform = multiply(transform, component.transform); + self.draw_component( + gid, + component.gid, + &component.coords, + transform, + depth + 1, + sink, + )?; + } + Ok(()) + } + + /// Draws glyph `gid` at `coords` under `transform`, as a component + /// of glyph `parent`. + fn draw_component( + &mut self, + parent: u16, + gid: u16, + coords: &[f32], + transform: [f32; 6], + depth: u8, + sink: &mut S, + ) -> Result<()> { + if depth > MAX_VARC_DEPTH { + return Err(Error::Malformed { + offset: 0, + context: "VARC composite recursion exceeded cap", + }); + } + // A component that names its parent's glyph draws that glyph's + // own outline, as in HarfBuzz, which does not recurse on the + // same glyph. Other components VARC has a record for are + // composites, resolved in the font set to the coords of the + // glyph drawn; the rest are drawn from `glyf` or CFF at their + // coords. + if gid != parent { + let composite = self + .varc + .composite_with_font_coords(gid, coords, self.font_coords); + if let Some(composite) = composite { + return self.draw_composite(gid, &composite, transform, depth, sink); + } + } + let mut placed = Placed { + sink, + transform: (transform != IDENTITY).then_some(transform), + left: self.ops_left, + drawn: 0, + }; + self.leaves.draw(self.face, gid, coords, &mut placed)?; + self.ops_left = self + .ops_left + .checked_sub(placed.drawn) + .ok_or(Error::Malformed { + offset: 0, + context: "VARC composite exceeds outline budget", + })?; + Ok(()) + } +} + +/// `a` times `b`, for affines in `[xx, xy, yx, yy, tx, ty]` order: `b` +/// applied first. The sums are taken in the order of HarfBuzz's +/// `hb_transform_t::multiply`, which composes the transforms of nested +/// components the same way. +fn multiply(a: [f32; 6], b: [f32; 6]) -> [f32; 6] { + [ + a[0] * b[0] + a[1] * b[2], + a[0] * b[1] + a[1] * b[3], + a[2] * b[0] + a[3] * b[2], + a[2] * b[1] + a[3] * b[3], + a[0] * b[4] + a[1] * b[5] + a[4], + a[2] * b[4] + a[3] * b[5] + a[5], + ] +} + +/// A sink that places a leaf's ops: maps each point by the component's +/// transform, as HarfBuzz's `transform_point` does (none for the +/// identity), and passes at most `left` ops on, counting every op drawn. +struct Placed<'s, S> { + sink: &'s mut S, + /// `None` for the identity. + transform: Option<[f32; 6]>, + left: usize, + drawn: usize, +} + +impl Placed<'_, S> { + /// Counts one op, and whether it is passed on. + fn admit(&mut self) -> bool { + self.drawn = self.drawn.saturating_add(1); + if self.left == 0 { + return false; + } + self.left -= 1; + true + } + + fn map(&self, x: f32, y: f32) -> (f32, f32) { + match self.transform { + None => (x, y), + Some(m) => (m[4] + m[0] * x + m[1] * y, m[5] + m[2] * x + m[3] * y), + } + } +} + +impl OutlineSink for Placed<'_, S> { + fn move_to(&mut self, x: f32, y: f32) { + if self.admit() { + let (x, y) = self.map(x, y); + self.sink.move_to(x, y); + } + } + + fn line_to(&mut self, x: f32, y: f32) { + if self.admit() { + let (x, y) = self.map(x, y); + self.sink.line_to(x, y); + } + } + + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + if self.admit() { + let (cx, cy) = self.map(cx, cy); + let (x, y) = self.map(x, y); + self.sink.quad_to(cx, cy, x, y); + } + } + + fn curve_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) { + if self.admit() { + let (c1x, c1y) = self.map(c1x, c1y); + let (c2x, c2y) = self.map(c2x, c2y); + let (x, y) = self.map(x, y); + self.sink.curve_to(c1x, c1y, c2x, c2y, x, y); + } + } + + fn close(&mut self) { + if self.admit() { + self.sink.close(); + } + } +} + +/// The tables the leaves of a walk are drawn from, read the first time +/// a leaf needs them, in the order [`Face::glyph_outline_at_coords`] +/// reads them for a glyph `VARC` does not draw, and kept for the walk. +/// A table that fails to read fails every leaf that needs it. +#[derive(Default)] +struct Leaves<'a> { + tables: OnceCell>>, + /// Read for the first leaf away from the default instance. + gvar: OnceCell>>>, +} + +/// The table a face draws glyphs from without `VARC`. +enum LeafTables<'a> { + Cff2(Cff2<'a>), + Cff(Cff<'a>), + Glyf { + glyf: Glyf<'a>, + loca: Loca<'a>, + hmtx: Hmtx<'a>, + /// The font's `vmtx`, or HarfBuzz's metrics for a font without. + vmtx: Vmtx<'a>, + }, +} + +impl<'a> Leaves<'a> { + /// Draws glyph `gid` of `face` at `coords` from `glyf` or CFF, as + /// [`Face::glyph_outline_at_coords`] draws a glyph `VARC` does not. + fn draw( + &self, + face: &Face<'a>, + gid: u16, + coords: &[f32], + sink: &mut S, + ) -> Result { + let tables = self + .tables + .get_or_init(|| read_leaf_tables(face)) + .as_ref() + .map_err(Clone::clone)?; + match tables { + LeafTables::Cff2(cff2) => cff2.outline(gid, coords, sink), + LeafTables::Cff(cff) => cff.outline(gid, sink), + LeafTables::Glyf { + glyf, + loca, + hmtx, + vmtx, + } => { + let gvar = if coords.is_empty() { + None + } else { + self.gvar + .get_or_init(|| face.gvar()) + .as_ref() + .map_err(Clone::clone)? + .as_ref() + }; + let metrics = PhantomMetrics { + hmtx, + vmtx: Some(vmtx), + }; + glyf.outline_at_coords(loca, gid, gvar, coords, Some(&metrics), sink) + } + } + } +} + +/// Reads the table `face` draws glyphs from without `VARC`: `CFF2` +/// whenever the font has one, else `CFF ` in a font without `glyf`, +/// else `glyf` with `loca` and the metrics its phantom points read. +fn read_leaf_tables<'a>(face: &Face<'a>) -> Result> { + if face.record(tag::CFF2).is_some() { + return Ok(LeafTables::Cff2(face.cff2()?)); + } + if face.record(tag::CFF1).is_some() && face.record(tag::GLYF).is_none() { + return Ok(LeafTables::Cff(face.cff()?)); + } + let loca = face.loca()?; + let glyf = face.glyf()?; + let hmtx = face.hmtx()?; + let vmtx = face.vmtx()?; + let vmtx = face.phantom_vmtx(vmtx)?; + Ok(LeafTables::Glyf { + glyf, + loca, + hmtx, + vmtx, + }) +} From d372f947bdba53060ca232c46701cd4b46a7d857 Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 13:53:34 -0800 Subject: [PATCH 3/7] Share one VARC work budget across a glyph's walk The condition and delta budget from #293 was per composite_with_font_coords call, and one Face outline call makes up to 2,049 of them, so a glyph whose 2,048 components each reached heavy condition or delta work could spend 2,049 budgets: 0.5 s for a 200 KB font, 1.2 s for 64 nested levels of 31 components, 0.25 s for 2,047 components naming a glyph of 20,000 gated-off components (release build, HarfBuzz-style hostile fonts built for this change). A walk now holds one VarcMemo for every composite it resolves: - One work budget of 2^20 units and one condition-visit budget of 65,536. Besides the region scalars and delta values #293 charged, units now pay for each component record read and each axis value in it, each offset of an And or Or condition, each coord a component's coord vector holds, the axes a region scalar evaluates, and each coord vector the memo keeps. Every one of these was free or charged per call before, so the walk's total work is bounded by the budget however its components share children. - What depends only on coords, kept by coord vector for the whole walk: condition results, region scalars, and resolved composites (an Rc per glyph). A glyph reached again at the same coords, from any parent, costs nothing more. Out of work, the delta that ran out adds nothing, a condition that needed it fails, and no more components are read, so the composite being read and every later one ends there. A walk holds at most one budget's worth of coords and composites, so it stays small: the worst of the hostile fonts peaks at 4.1 MB of heap and every one of their glyphs draws in under 14 ms. Varc::composite_with_font_coords is one walk over one composite, with the same budget. Its per-composite cap of 2^20 coord values is gone: the coords are charged to the budget instead, so a record of 4096-axis components now stops at 255 of them rather than 256. Tests count the work: 600 components reaching a glyph at distinct coords whose condition and deltas cost 180,047 units spend the budget after six of them (one budget per composite let them spend 108 million units), and 600 reaching it at the same coords resolve it once, for 181,848 units. --- src/face/outlines.rs | 9 +- src/face/varc_draw.rs | 173 ++++++++++++++-- src/tables/multi_var_store.rs | 52 +++-- src/tables/varc.rs | 372 ++++++++++++++++++++++++++-------- src/tables/varc/tests.rs | 94 ++++++--- 5 files changed, 550 insertions(+), 150 deletions(-) diff --git a/src/face/outlines.rs b/src/face/outlines.rs index b284c2a6..c0cf89af 100644 --- a/src/face/outlines.rs +++ b/src/face/outlines.rs @@ -183,9 +183,12 @@ impl<'a> Face<'a> { /// it sits. /// - A component that names the glyph it belongs to draws that /// glyph's `glyf` or CFF outline instead of recursing. - /// - /// A glyph that draws more than 2048 components or 2^20 ops, or nests - /// components more than 64 deep, fails with `Malformed`. + /// - The condition and delta work of every composite the glyph + /// reaches shares one budget (see + /// [`Varc::composite_with_font_coords`](crate::tables::Varc::composite_with_font_coords)), + /// and a glyph reached again at the same coords is resolved once. + /// A glyph that draws more than 2048 components or 2^20 ops, or + /// nests components more than 64 deep, fails with `Malformed`. pub fn glyph_outline_at_coords( &self, glyph_id: u16, diff --git a/src/face/varc_draw.rs b/src/face/varc_draw.rs index ae8cb716..e8fba55d 100644 --- a/src/face/varc_draw.rs +++ b/src/face/varc_draw.rs @@ -7,7 +7,7 @@ use core::cell::OnceCell; use super::Face; use crate::error::{Error, Result}; use crate::tables::glyf::PhantomMetrics; -use crate::tables::varc::VarcComposite; +use crate::tables::varc::{VarcComposite, VarcMemo}; use crate::tables::{tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, OutlineSink, Varc, Vmtx}; /// Deepest a component may sit below the glyph drawn, as the `glyf` @@ -28,18 +28,21 @@ const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; /// One glyph drawn through `VARC`. /// -/// Every composite the walk reaches is resolved in the font set to the -/// coords of the glyph drawn, so a `RESET_UNSPECIFIED_AXES` component -/// starts from them however deep it sits. Components that share -/// children can make the walk grow exponentially with depth, so it -/// draws at most [`MAX_VARC_COMPONENTS`] components and -/// [`MAX_VARC_OPS`] ops, and fails with `Malformed` past either or -/// past [`MAX_VARC_DEPTH`]. +/// The walk resolves composites with one [`VarcMemo`], so one budget +/// bounds the condition and delta work of every composite the glyph +/// reaches, and a glyph reached again at the same coords is resolved +/// once. Every composite is resolved in the font set to the coords of +/// the glyph drawn, so a `RESET_UNSPECIFIED_AXES` component starts from +/// them however deep it sits. Components that share children can make +/// the walk grow exponentially with depth, so it also draws at most +/// [`MAX_VARC_COMPONENTS`] components and [`MAX_VARC_OPS`] ops, and +/// fails with `Malformed` past either or past [`MAX_VARC_DEPTH`]. pub(crate) struct VarcDraw<'w, 'a> { face: &'w Face<'a>, varc: &'w Varc<'a>, /// The font's coords, rounded to F2DOT14. font_coords: &'w [f32], + memo: VarcMemo, leaves: Leaves<'a>, components_left: usize, ops_left: usize, @@ -53,6 +56,7 @@ impl<'w, 'a> VarcDraw<'w, 'a> { face, varc, font_coords, + memo: VarcMemo::new(), leaves: Leaves::default(), components_left: MAX_VARC_COMPONENTS, ops_left: MAX_VARC_OPS, @@ -64,7 +68,7 @@ impl<'w, 'a> VarcDraw<'w, 'a> { /// has none; the glyph is then drawn from `glyf` or CFF. pub(crate) fn draw(&mut self, glyph_id: u16, sink: &mut S) -> Result { let coords = self.font_coords; - let Some(composite) = self.varc.composite(glyph_id, coords) else { + let Some(composite) = self.varc.resolve(glyph_id, coords, coords, &mut self.memo) else { return Ok(false); }; self.draw_composite(glyph_id, &composite, IDENTITY, 0, sink)?; @@ -127,10 +131,8 @@ impl<'w, 'a> VarcDraw<'w, 'a> { // glyph drawn; the rest are drawn from `glyf` or CFF at their // coords. if gid != parent { - let composite = self - .varc - .composite_with_font_coords(gid, coords, self.font_coords); - if let Some(composite) = composite { + let font_coords = self.font_coords; + if let Some(composite) = self.varc.resolve(gid, coords, font_coords, &mut self.memo) { return self.draw_composite(gid, &composite, transform, depth, sink); } } @@ -325,3 +327,148 @@ fn read_leaf_tables<'a>(face: &Face<'a>) -> Result> { vmtx, }) } + +#[cfg(test)] +mod tests { + use alloc::vec; + use alloc::vec::Vec; + + use super::*; + use crate::tables::varc::tests::{ + build_varc, condition_list, deep_value_condition, mention_store, with_conditions, + }; + use crate::tables::varc::MAX_WALK_WORK; + use crate::tables::{Outline, PathOp}; + use crate::Blob; + + /// `font` with its `VARC` table replaced by `varc`. + fn with_varc(font: &[u8], varc: &[u8]) -> Vec { + let face = Face::parse_bytes(font, 0).unwrap(); + let mut tables: Vec<([u8; 4], &[u8])> = face + .records() + .iter() + .filter(|r| r.tag != tag::VARC) + .map(|r| (r.tag, face.table_bytes(r.tag).unwrap())) + .collect(); + tables.push((tag::VARC, varc)); + tables.sort_by_key(|t| t.0); + let mut out = Vec::new(); + out.extend_from_slice(&0x0001_0000u32.to_be_bytes()); + out.extend_from_slice(&(tables.len() as u16).to_be_bytes()); + out.extend_from_slice(&[0; 6]); + let mut offset = 12 + 16 * tables.len(); + let mut body = Vec::new(); + for (tag, data) in &tables { + out.extend_from_slice(tag); + out.extend_from_slice(&0u32.to_be_bytes()); + out.extend_from_slice(&(offset as u32).to_be_bytes()); + out.extend_from_slice(&(data.len() as u32).to_be_bytes()); + body.extend_from_slice(data); + while body.len() % 4 != 0 { + body.push(0); + } + offset = 12 + 16 * tables.len() + body.len(); + } + out.extend_from_slice(&body); + out + } + + /// `varc_parity.ttf` (glyph 1 a box, glyph 2 a triangle) with a VARC + /// table in which glyph 3 has one component per entry of `axis0`, + /// each naming glyph 4 with axis 0 set to that F2DOT14 value, and + /// glyph 4 two components: the box, gated by 20 And levels whose two + /// offsets name the same child over a Value condition walking 60,000 + /// region mentions, and the triangle, its translation varying + /// through the same delta set. + fn heavy_fan_out(axis0: &[i16]) -> Vec { + let mut fan = Vec::new(); + for &value in axis0 { + // HAVE_AXES, glyph 4, axis indices list 0, one i16 value. + fan.extend_from_slice(&[0x02, 0x00, 0x04, 0x00, 0x40]); + fan.extend_from_slice(&value.to_be_bytes()); + } + // The box, gated by condition 0, then the triangle, translated + // by variation index 0. + let heavy = vec![ + 0x80, 0x80, 0x00, 0x01, 0x00, 0x18, 0x00, 0x02, 0x00, 0x00, 0x00, + ]; + let table = build_varc( + &[3, 4], + &[&fan, &heavy], + Some(&mention_store(60_000)), + Some(&[&[0x00, 0x00]]), + ); + let table = with_conditions(table, &condition_list(&[deep_value_condition(20, -30_000)])); + with_varc( + include_bytes!("../../tests/fixtures/varc_parity.ttf"), + &table, + ) + } + + /// Draws glyph 3 of `font` at the default instance, returning the + /// outline, the work units and the condition visits of the walk. + fn walk(font: &[u8]) -> (Outline, u64, u32) { + let blob = Blob::new(font); + let face = Face::parse(&blob, 0).unwrap(); + let varc = face.varc().unwrap().unwrap(); + let mut draw = VarcDraw::new(&face, &varc, &[]); + let mut out = Outline::new(); + assert!(draw.draw(3, &mut out).unwrap()); + let work = draw.memo.work_done(); + let visits = draw.memo.condition_visits(); + // The face draws the same outline. + assert_eq!(face.glyph_outline(3).unwrap(), Some(out.clone())); + (out, work, visits) + } + + fn move_xs(outline: &Outline) -> Vec { + outline + .ops() + .iter() + .filter_map(|op| match *op { + PathOp::MoveTo { x, .. } => Some(x), + _ => None, + }) + .collect() + } + + #[test] + fn components_at_distinct_coords_share_one_work_budget() { + // 600 components, each reaching glyph 4 at its own coords, whose + // condition and deltas cost 180,047 units there: 2 to keep the + // coords, 2 for the records, 2 for their coords, 40 for the And + // offsets, 120,001 for the Value condition (scalars, one region + // axis, the walk) and 60,000 for the translation. One budget per + // composite let every one of them spend that, 108 million units + // in all. Now glyph 3 (1801 units: its coords, its 600 records + // and their coords) and five copies of glyph 4 fit, the sixth + // runs out at its translation, and the rest draw nothing. + // Axis 0 starts just past 0.5, where the condition starts to hold. + let axis0: Vec = (0..600).map(|i| 8193 + i).collect(); + let (out, work, visits) = walk(&heavy_fan_out(&axis0)); + assert_eq!(work, MAX_WALK_WORK); + assert_eq!(visits, 6 * 41); + let xs = move_xs(&out); + assert_eq!(xs.len(), 12); + // Box, then triangle translated by 60,000 deltas of 1 times axis 0, + // summed in f32 as the walk sums them. + for (i, pair) in xs.chunks(2).enumerate().take(5) { + let scalar = (8193.0 + i as f32) / 16384.0; + let tx = (0..60_000).fold(0.0f32, |sum, _| sum + scalar); + assert_eq!(pair, [0.0, tx]); + } + assert_eq!(xs[10..], [0.0, 0.0]); + } + + #[test] + fn a_glyph_reached_again_at_the_same_coords_is_resolved_once() { + // 600 components reaching glyph 4 at axis 0 = 1: its 180,047 + // units are spent once, and every copy is drawn with them. + let (out, work, visits) = walk(&heavy_fan_out(&[16384; 600])); + assert_eq!(work, 1 + 600 * 3 + 180_047); + assert_eq!(visits, 41); + let xs = move_xs(&out); + assert_eq!(xs.len(), 1200); + assert!(xs.chunks(2).all(|pair| pair == [0.0, 60_000.0])); + } +} diff --git a/src/tables/multi_var_store.rs b/src/tables/multi_var_store.rs index b632a55e..e7c519d0 100644 --- a/src/tables/multi_var_store.rs +++ b/src/tables/multi_var_store.rs @@ -341,12 +341,14 @@ impl<'a> MultiVarStore<'a> { } } - /// Scalars for each entry of `region_indexes`, in order. Each - /// distinct region is evaluated once, so a subtable that names one - /// large region many times does not repeat the work. - fn scalars_for(&self, region_indexes: &[u16], coords: &[f32]) -> Vec { + /// Scalars for each entry of `region_indexes`, in order, and the + /// number of region axes evaluated for them. Each distinct region is + /// evaluated once, so a subtable that names one large region many + /// times does not repeat the work. + fn scalars_for(&self, region_indexes: &[u16], coords: &[f32]) -> (Vec, usize) { let mut cache: BTreeMap = BTreeMap::new(); - region_indexes + let mut axis_steps = 0usize; + let scalars = region_indexes .iter() .map(|&ri| { let Some(&slot) = self.region_slots.get(ri as usize) else { @@ -355,11 +357,14 @@ impl<'a> MultiVarStore<'a> { let Some(region) = self.regions.get(slot) else { return 0.0; }; - *cache - .entry(slot) - .or_insert_with(|| sparse_region_scalar(region, coords)) + *cache.entry(slot).or_insert_with(|| { + let (scalar, steps) = sparse_region_scalar_steps(region, coords); + axis_steps = axis_steps.saturating_add(steps); + scalar + }) }) - .collect() + .collect(); + (scalars, axis_steps) } /// Per-region scalars for all regions referenced by subtable `outer`, @@ -368,7 +373,7 @@ impl<'a> MultiVarStore<'a> { #[must_use] pub fn region_scalars(&self, outer: u16, coords: &[f32]) -> Option> { let sub = self.subtable(outer)?; - Some(self.scalars_for(&sub.region_indexes, coords)) + Some(self.scalars_for(&sub.region_indexes, coords).0) } /// Number of regions referenced by subtable `outer`. @@ -443,7 +448,7 @@ impl<'a> MultiVarStore<'a> { let Some(slot) = self.subtable_slot(outer) else { return; }; - let scalars = self.slot_scalars(slot, coords); + let (scalars, _) = self.slot_scalars(slot, coords); self.add_slot_deltas(slot, inner, &scalars, out); } @@ -462,12 +467,14 @@ impl<'a> MultiVarStore<'a> { } /// The scalar of each region index of the subtable in `slot` at - /// `coords`, for [`Self::add_slot_deltas`]. Costs one step per - /// region index plus one evaluation per distinct region. - pub(crate) fn slot_scalars(&self, slot: usize, coords: &[f32]) -> Vec { - self.subtables - .get(slot) - .map_or_else(Vec::new, |s| self.scalars_for(&s.region_indexes, coords)) + /// `coords`, for [`Self::add_slot_deltas`], and the number of region + /// axes evaluated for them. Costs one step per region index plus one + /// evaluation per distinct region. + pub(crate) fn slot_scalars(&self, slot: usize, coords: &[f32]) -> (Vec, usize) { + self.subtables.get(slot).map_or_else( + || (Vec::new(), 0), + |s| self.scalars_for(&s.region_indexes, coords), + ) } /// [`Self::add_deltas`] for the subtable in `slot`, with its region @@ -589,15 +596,20 @@ impl<'a> TupleFetcher<'a> { /// Scalar of one sparse region at `coords`: the product of its /// per-axis falloffs, stopping early at zero. fn sparse_region_scalar(region: &SparseRegion, coords: &[f32]) -> f32 { + sparse_region_scalar_steps(region, coords).0 +} + +/// [`sparse_region_scalar`], with the number of axes it evaluated. +fn sparse_region_scalar_steps(region: &SparseRegion, coords: &[f32]) -> (f32, usize) { let mut scalar = 1.0_f32; - for axis in ®ion.axes { + for (steps, axis) in region.axes.iter().enumerate() { let coord = *coords.get(axis.axis_index as usize).unwrap_or(&0.0); scalar *= axis_scalar(axis.start, axis.peak, axis.end, coord); if scalar == 0.0 { - return 0.0; + return (0.0, steps + 1); } } - scalar + (scalar, region.axes.len()) } /// Triangular region falloff for one axis. Returns `1.0` at `peak`, diff --git a/src/tables/varc.rs b/src/tables/varc.rs index b38e3b3a..6e4cd93a 100644 --- a/src/tables/varc.rs +++ b/src/tables/varc.rs @@ -50,6 +50,7 @@ //! `sigilbuzz-subset` crate. use alloc::collections::BTreeMap; +use alloc::rc::Rc; use alloc::vec::Vec; use crate::error::{Error, Result}; @@ -127,31 +128,33 @@ const NO_VARIATION: u32 = 0xFFFF_FFFF; /// ignored. const MAX_COMPONENT_AXES: usize = 4096; -/// Condition table visits one `composite_with_font_coords` call makes at -/// most. Each table's result is kept for the call, so a table whose +/// Condition table visits one walk makes at most (see [`VarcMemo`]). +/// Each table's result is kept for the walk and coords, so a table whose /// children share a subtree is evaluated once, not once per path; past /// the budget every condition fails. const MAX_CONDITION_TABLES: u32 = 1 << 16; -/// Delta work one `composite_with_font_coords` call does at most: one -/// unit per region index whose scalar it works out (once per -/// MultiItemVariationData subtable and call) and one per delta value it -/// walks (region indexes times values, per variation it applies). A -/// component of a real font walks a few hundred values; past the budget -/// no more deltas apply and conditions that need them fail. -const MAX_COMPOSITE_WORK: u64 = 1 << 20; +/// Work one walk does at most (see [`VarcMemo`]), in units of: +/// +/// - one per component record read, plus one per axis value in it; +/// - one per offset of each And or Or condition table evaluated; +/// - one per value of each coord vector it builds: a component's coords, +/// and the copy of a glyph's coords its results are kept under; +/// - one per region index and per region axis whose scalar it works out +/// (once per coord vector and MultiItemVariationData subtable); +/// - one per delta value it walks (region indexes times values, per +/// variation it applies). +/// +/// A glyph of a real font costs a few thousand units. Past the budget +/// the delta that ran out adds nothing, a condition that needed it does +/// not hold, and no more components are read: the composite being read +/// ends there, and so does every composite after it. +pub(crate) const MAX_WALK_WORK: u64 = 1 << 20; /// HarfBuzz's `HB_MAX_NESTING_LEVEL`: its sanitizer drops a condition /// nested deeper, which then does not hold. const MAX_CONDITION_DEPTH: usize = 64; -/// Upper bound on the coordinate values one composite carries across -/// all its components. Each component copies a coord vector, widened -/// to the highest axis index it lists, so a few record bytes can ask -/// for up to 65536 values. The cap keeps a long record of such -/// components from turning into a huge allocation. -const MAX_COMPOSITE_COORDS: usize = 1 << 20; - impl<'a> Varc<'a> { /// Parses a `VARC` table. pub fn parse(data: &'a [u8]) -> Result { @@ -240,6 +243,14 @@ impl<'a> Varc<'a> { self.glyph_records.len() } + /// Whether `gid` has a glyph record, so that VARC draws it: it is in + /// the coverage, at an index the glyph records reach. + pub(crate) fn has_record(&self, gid: u16) -> bool { + self.coverage + .index_of(gid) + .is_some_and(|idx| (idx as usize) < self.glyph_records.len()) + } + /// Resolves the component list for `gid` at the given normalized /// axis coords. Returns `None` for uncovered gids. /// @@ -272,9 +283,16 @@ impl<'a> Varc<'a> { /// component lists take its values. /// /// A component whose glyph id does not fit 16 bits names no glyph - /// and is left out. A malformed component ends the list early. So - /// does a component that would push the total length of all - /// component coord vectors past 2^20 values. + /// and is left out. A malformed component ends the list early. + /// + /// One call does at most 2^20 units of work: one per component read + /// and per axis value in it, one per offset of an And or Or + /// condition, one per coord value it writes, one per region index + /// and region axis whose scalar it works out, and one per delta + /// value it walks. Past that the delta that ran out adds nothing, a + /// condition that needed it does not hold, and the list ends. + /// [`crate::Face::glyph_outline_at_coords`] shares one such budget + /// across every composite one glyph draws. #[must_use] pub fn composite_with_font_coords( &self, @@ -282,44 +300,103 @@ impl<'a> Varc<'a> { coords: &[f32], font_coords: &[f32], ) -> Option { - let mut eval = Eval::new(coords); - self.composite_in(gid, font_coords, &mut eval) + self.composite_in(gid, coords, font_coords, &mut VarcMemo::new()) + } + + /// The composite of `gid` at `coords` in a font set to + /// `font_coords`, as [`Varc::composite_with_font_coords`] resolves + /// it, for one walk over many composites: `memo` holds the walk's + /// budgets, and keeps each composite, condition result and set of + /// region scalars by the coords it was worked out at, so a glyph + /// reached again at the same coords costs nothing more. `None` when + /// VARC has no record for `gid`. Every call with one `memo` must + /// pass the same `font_coords`. + pub(crate) fn resolve( + &self, + gid: u16, + coords: &[f32], + font_coords: &[f32], + memo: &mut VarcMemo, + ) -> Option> { + if !self.has_record(gid) { + return None; + } + let id = memo.coords_id(coords); + if let Some(id) = id { + if let Some(known) = memo.per_coords[id].composites.get(&gid) { + return Some(Rc::clone(known)); + } + } + let composite = Rc::new(self.composite_at(gid, coords, font_coords, id, memo)?); + if let Some(id) = id { + memo.per_coords[id] + .composites + .insert(gid, Rc::clone(&composite)); + } + Some(composite) } - /// [`Varc::composite_with_font_coords`] with the evaluation state - /// held by the caller, so tests can read the work it did. + /// [`Varc::composite_with_font_coords`] with the walk's state held + /// by the caller: its budgets, and the condition results and region + /// scalars it keeps by coords. fn composite_in( &self, gid: u16, + coords: &[f32], font_coords: &[f32], - eval: &mut Eval<'_>, + memo: &mut VarcMemo, + ) -> Option { + if !self.has_record(gid) { + return None; + } + let id = memo.coords_id(coords); + self.composite_at(gid, coords, font_coords, id, memo) + } + + /// [`Self::composite_in`] with what the walk keeps for `coords` at + /// index `id`, or with nothing kept when the walk had no work left + /// to keep a copy of the coords. + fn composite_at( + &self, + gid: u16, + coords: &[f32], + font_coords: &[f32], + id: Option, + memo: &mut VarcMemo, ) -> Option { let idx = self.coverage.index_of(gid)? as usize; let raw = *self.glyph_records.get(idx)?; + let mut scratch = CoordsMemo::default(); + let caches = match id { + Some(id) => &mut memo.per_coords[id], + None => &mut scratch, + }; + let mut eval = Eval { + coords, + caches, + budget: &mut memo.budget, + }; let mut composite = VarcComposite::default(); let mut r = Reader::new(raw); - let mut total_coords = 0usize; while !r.is_empty() { // A VarComponent stops when bytes run out. Reaching the // end mid-record means the font is malformed; we skip the // rest rather than error so a single bad glyph doesn't - // tank the rest of the document. - let Ok(record) = self.read_component(&mut r) else { + // tank the rest of the document. Running out of work ends + // the list too. + let Ok(record) = self.read_component(&mut r, eval.budget) else { break; }; if let Some(index) = record.condition_index { - if !self.condition_holds(index, eval) { + if !self.condition_holds(index, &mut eval) { continue; } } - let Some(c) = self.resolve_component(&record, font_coords, eval) else { - continue; - }; - total_coords = total_coords.saturating_add(c.coords.len()); - if total_coords > MAX_COMPOSITE_COORDS { - break; + match self.resolve_component(&record, font_coords, &mut eval) { + Resolved::Component(c) => composite.components.push(c), + Resolved::Nothing => {} + Resolved::OutOfWork => break, } - composite.components.push(c); } Some(composite) } @@ -329,7 +406,22 @@ impl<'a> Varc<'a> { /// index, axis indices index and axis values, the two variation /// indices, the transform fields, then one discarded uint32var per /// reserved flag bit. - fn read_component(&self, r: &mut Reader<'_>) -> Result> { + /// + /// Charges `budget` one unit for the record and one per axis value, + /// before decoding them, and fails without reading on when they do + /// not fit. + fn read_component( + &self, + r: &mut Reader<'_>, + budget: &mut Budget, + ) -> Result> { + const OUT_OF_WORK: Error = Error::Malformed { + offset: 0, + context: "VARC walk out of work", + }; + if !budget.spend(1) { + return Err(OUT_OF_WORK); + } let flags = read_uint32var(r)?; let gid = if flags & VC_GID_IS_24BIT != 0 { let bytes = r.read_bytes(3)?; @@ -350,6 +442,9 @@ impl<'a> Varc<'a> { .axis_indices_lists .get(index) .map_or(&[][..], Vec::as_slice); + if !budget.spend(indices.len() as u64) { + return Err(OUT_OF_WORK); + } let values = decode_tuple_values_in_reader(r, indices.len()).ok_or(Error::Malformed { offset: r.position(), @@ -392,14 +487,15 @@ impl<'a> Varc<'a> { /// Evaluates `record` at the coords of the glyph the component /// belongs to (`eval.coords`), in a font set to `font_coords`. - /// `None` when the component draws nothing. fn resolve_component( &self, record: &ComponentRecord<'_>, font_coords: &[f32], eval: &mut Eval<'_>, - ) -> Option { - let gid = u16::try_from(record.gid).ok()?; + ) -> Resolved { + let Ok(gid) = u16::try_from(record.gid) else { + return Resolved::Nothing; + }; let coords = eval.coords; // Axis values and their deltas, in F2DOT14 units. @@ -421,6 +517,17 @@ impl<'a> Varc<'a> { } else { coords }; + // Building the vector is charged first: a few record bytes can + // ask for 4096 values. + let len = record + .axis_indices + .iter() + .map(|&axis| axis as usize) + .filter(|&axis| axis < MAX_COMPONENT_AXES) + .fold(base.len(), |len, axis| len.max(axis + 1)); + if !eval.budget.spend(len as u64) { + return Resolved::OutOfWork; + } let mut child_coords = base.to_vec(); for (&axis, &value) in record.axis_indices.iter().zip(&axis_values) { let axis = axis as usize; @@ -458,7 +565,7 @@ impl<'a> Varc<'a> { t.sy = t.sx; } - Some(VarcComponent { + Resolved::Component(VarcComponent { gid, transform: t.to_affine(), coords: child_coords, @@ -513,11 +620,12 @@ impl<'a> Varc<'a> { /// its negation does. A table cut short or past the list, an /// unknown format, a table nested deeper than `MAX_CONDITION_DEPTH` /// (HarfBuzz's sanitizer drops those), and every table once the - /// call's visit budget runs out do not hold either. + /// walk's visit budget runs out do not hold either. /// - /// A table's result depends only on the coords, so the call keeps - /// it: a later visit, through another path into a shared subtree, - /// reads it back instead of walking the subtree again. + /// A table's result depends only on the coords, so the walk keeps + /// it under them: a later visit, through another path into a shared + /// subtree or from another glyph at the same coords, reads it back + /// instead of walking the subtree again. fn evaluate_condition( &self, list: &[u8], @@ -525,15 +633,15 @@ impl<'a> Varc<'a> { depth: usize, eval: &mut Eval<'_>, ) -> bool { - if depth >= MAX_CONDITION_DEPTH || eval.condition_visits_left == 0 { + if depth >= MAX_CONDITION_DEPTH || eval.budget.condition_visits_left == 0 { return false; } - eval.condition_visits_left -= 1; - if let Some(&known) = eval.conditions.get(&at) { + eval.budget.condition_visits_left -= 1; + if let Some(&known) = eval.caches.conditions.get(&at) { return known; } let holds = self.condition_table(list, at, depth, eval); - eval.conditions.insert(at, holds); + eval.caches.conditions.insert(at, holds); holds } @@ -571,8 +679,8 @@ impl<'a> Varc<'a> { return false; }; // HarfBuzz sums the deltas on their own, then adds the - // sum to the default value. Out of delta budget the value - // is unknown, so the condition fails. + // sum to the default value. Out of work the value is + // unknown, so the condition fails. let mut delta = [0.0]; self.add_deltas(index, &mut delta, eval) && f32::from(default as i16) + delta[0] > 0.0 @@ -582,8 +690,10 @@ impl<'a> Varc<'a> { return false; }; // The whole offset array has to be there, as for - // HarfBuzz's sanitizer. - if data.len() < 3 + 3 * usize::from(count) { + // HarfBuzz's sanitizer. Reading it costs one unit per + // offset, so offsets that name no table, which cost no + // visit, are bounded too. + if data.len() < 3 + 3 * usize::from(count) || !eval.budget.spend(u64::from(count)) { return false; } let mut fields = (0..usize::from(count)).map(|i| 3 + 3 * i); @@ -603,10 +713,11 @@ impl<'a> Varc<'a> { /// adds them. Nothing is added at the default instance (empty /// coords), for `NO_VARIATION`, or without a store, as in HarfBuzz. /// - /// The region scalars of each subtable are worked out once per call - /// and kept. Working them out and walking the delta set are charged - /// to the call's work budget; once that runs out this adds nothing - /// and returns false. + /// The region scalars of each subtable are worked out once per walk + /// and coords, and kept. Working them out (one unit per region index + /// and per region axis) and walking the delta set (one per value) + /// are charged to the walk's work budget; once that runs out this + /// adds nothing and returns false. fn add_deltas(&self, index: u32, values: &mut [f32], eval: &mut Eval<'_>) -> bool { if eval.coords.is_empty() || index == NO_VARIATION { return true; @@ -619,50 +730,143 @@ impl<'a> Varc<'a> { }; let regions = store.slot_region_count(slot) as u64; let mut cost = regions.saturating_mul(values.len() as u64); - if !eval.scalars.contains_key(&slot) { + let known = eval.caches.scalars.contains_key(&slot); + if !known { cost = cost.saturating_add(regions); } - if cost > eval.work_left { - eval.work_left = 0; + if !eval.budget.spend(cost) { return false; } - eval.work_left -= cost; - let coords = eval.coords; - let scalars = eval - .scalars - .entry(slot) - .or_insert_with(|| store.slot_scalars(slot, coords)); + if !known { + let (scalars, axis_steps) = store.slot_scalars(slot, eval.coords); + // The axes a region constrains are only known once it is + // evaluated, so they are charged after. Past the budget the + // scalars still serve this delta, but nothing after it. + eval.budget.spend_after(axis_steps as u64); + eval.caches.scalars.insert(slot, scalars); + } + let Some(scalars) = eval.caches.scalars.get(&slot) else { + return false; + }; store.add_slot_deltas(slot, index & 0xFFFF, scalars, values); true } } -/// The state of one `composite_with_font_coords` call: what depends only -/// on the glyph's coords, kept so it is worked out once, and the call's -/// budgets. -struct Eval<'c> { - /// The coords of the glyph the components belong to. - coords: &'c [f32], - /// Condition results by the table's offset in the ConditionList. - conditions: BTreeMap, +/// What one walk over VARC composites keeps between the composites it +/// resolves: one walk is one [`Varc::composite_with_font_coords`] call, +/// or every composite [`crate::Face::glyph_outline_at_coords`] resolves +/// for one glyph. +/// +/// It holds the walk's two budgets, and by coord vector what depends +/// only on the coords: condition results, region scalars, and resolved +/// composites. Each coord vector is kept once, charged one unit per +/// value, so what it holds is bounded by the work budget. +pub(crate) struct VarcMemo { + budget: Budget, + /// Each coord vector seen, as `f32` bits, to its index in + /// `per_coords`. + coords_ids: BTreeMap, usize>, + per_coords: Vec, +} + +impl VarcMemo { + /// A new walk, with full budgets and nothing kept. + pub(crate) fn new() -> Self { + Self { + budget: Budget { + condition_visits_left: MAX_CONDITION_TABLES, + work_left: MAX_WALK_WORK, + }, + coords_ids: BTreeMap::new(), + per_coords: Vec::new(), + } + } + + /// The index in `per_coords` of what is kept for `coords`, adding an + /// empty entry the first time, charged one unit plus one per value. + /// `None` once the walk is out of work. + fn coords_id(&mut self, coords: &[f32]) -> Option { + let key: Vec = coords.iter().map(|c| c.to_bits()).collect(); + if let Some(&id) = self.coords_ids.get(&key) { + return Some(id); + } + if !self.budget.spend(coords.len() as u64 + 1) { + return None; + } + let id = self.per_coords.len(); + self.per_coords.push(CoordsMemo::default()); + self.coords_ids.insert(key, id); + Some(id) + } + + /// Work units the walk has spent. + #[cfg(test)] + pub(crate) fn work_done(&self) -> u64 { + MAX_WALK_WORK - self.budget.work_left + } + + /// Condition table visits the walk has made. + #[cfg(test)] + pub(crate) fn condition_visits(&self) -> u32 { + MAX_CONDITION_TABLES - self.budget.condition_visits_left + } +} + +/// A walk's budgets (see [`MAX_CONDITION_TABLES`] and [`MAX_WALK_WORK`]). +struct Budget { condition_visits_left: u32, - /// Region scalars by MultiItemVariationData subtable slot. - scalars: BTreeMap>, work_left: u64, } -impl<'c> Eval<'c> { - fn new(coords: &'c [f32]) -> Self { - Self { - coords, - conditions: BTreeMap::new(), - condition_visits_left: MAX_CONDITION_TABLES, - scalars: BTreeMap::new(), - work_left: MAX_COMPOSITE_WORK, +impl Budget { + /// Takes `cost` units of work, or, when they do not fit, all that is + /// left, so every later charge fails too, and returns false. + fn spend(&mut self, cost: u64) -> bool { + if cost > self.work_left { + self.work_left = 0; + return false; } + self.work_left -= cost; + true + } + + /// Takes `cost` units for work already done, as many as are left. + fn spend_after(&mut self, cost: u64) { + self.work_left = self.work_left.saturating_sub(cost); } } +/// What a walk keeps for one coord vector. +#[derive(Default)] +struct CoordsMemo { + /// Condition results by the table's offset in the ConditionList. + conditions: BTreeMap, + /// Region scalars by MultiItemVariationData subtable slot. + scalars: BTreeMap>, + /// Resolved composites by glyph id. + composites: BTreeMap>, +} + +/// The state one composite is resolved with: the coords of the glyph +/// whose components it reads, what the walk keeps for those coords, and +/// the walk's budgets. +struct Eval<'m> { + coords: &'m [f32], + caches: &'m mut CoordsMemo, + budget: &'m mut Budget, +} + +/// What resolving one component record gives. +enum Resolved { + /// A component to draw. + Component(VarcComponent), + /// A component that draws nothing. + Nothing, + /// No component: the walk ran out of work building it. + OutOfWork, +} + /// One component record as stored, before it is evaluated at any /// coords. struct ComponentRecord<'t> { @@ -986,4 +1190,4 @@ fn sincos_pi(x: f32) -> (f32, f32) { } #[cfg(test)] -mod tests; +pub(crate) mod tests; diff --git a/src/tables/varc/tests.rs b/src/tables/varc/tests.rs index ccb2fda5..54a4ea45 100644 --- a/src/tables/varc/tests.rs +++ b/src/tables/varc/tests.rs @@ -49,7 +49,7 @@ fn build_cff2_index(entries: &[&[u8]]) -> Vec { /// Builds a minimal VARC table with the given coverage gids and /// raw glyph record bytes. `var_store` and `axis_indices` are /// optional; passing `None` leaves their offsets at zero. -fn build_varc( +pub(crate) fn build_varc( coverage_gids: &[u16], glyph_records: &[&[u8]], var_store: Option<&[u8]>, @@ -336,17 +336,20 @@ fn huge_rotation_delta_does_not_hang() { } #[test] -fn wide_axis_components_stop_at_coord_budget() { +fn wide_axis_components_stop_at_the_work_budget() { // Each 6-byte component lists axis 4095, the last one HarfBuzz // keeps, so its coord vector grows to 4096 values. 100 000 of them - // would allocate about 1.6 GB. + // would allocate about 1.6 GB. Each costs 4098 units (the record, + // its one axis value, its 4096 coords) after the one unit that + // keeps the empty coords, so 255 fit. let axis_indices: &[u8] = &[0x40, 0x0F, 0xFF]; // one i16: 4095 let component: [u8; 6] = [VC_HAVE_AXES as u8, 0x00, 0x05, 0x00, 0x00, 0x00]; let record = component.repeat(100_000); let bytes = build_varc(&[1], &[&record], None, Some(&[axis_indices])); let varc = Varc::parse(&bytes).unwrap(); let comp = varc.composite(1, &[]).unwrap(); - assert_eq!(comp.components.len(), MAX_COMPOSITE_COORDS / 4096); + assert_eq!(comp.components.len() as u64, (MAX_WALK_WORK - 1) / 4098); + assert_eq!(comp.components.len(), 255); assert!(comp.components.iter().all(|c| c.coords.len() == 4096)); } @@ -641,7 +644,7 @@ fn axis_indices_walk_malformed_streams_like_harfbuzz() { } /// `table` with `conditions` appended as its ConditionList. -fn with_conditions(mut table: Vec, conditions: &[u8]) -> Vec { +pub(crate) fn with_conditions(mut table: Vec, conditions: &[u8]) -> Vec { let at = table.len() as u32; table[12..16].copy_from_slice(&at.to_be_bytes()); table.extend_from_slice(conditions); @@ -680,7 +683,7 @@ fn cond_not(child: Option<&[u8]>) -> Vec { out } -fn condition_list(conditions: &[Vec]) -> Vec { +pub(crate) fn condition_list(conditions: &[Vec]) -> Vec { let mut out = (conditions.len() as u32).to_be_bytes().to_vec(); let mut at = 4 + 4 * conditions.len(); for c in conditions { @@ -792,19 +795,16 @@ fn shared_condition_trees_are_evaluated_once() { &condition_list(&[conditions]), ); let varc = Varc::parse(&table).unwrap(); - let mut eval = Eval::new(&[0.0]); - let c = varc.composite_in(1, &[0.0], &mut eval).unwrap(); + let mut memo = VarcMemo::new(); + let c = varc.composite_in(1, &[0.0], &[0.0], &mut memo).unwrap(); assert_eq!(c.components.len(), 1); - assert_eq!( - MAX_CONDITION_TABLES - eval.condition_visits_left, - 1 + 5 * 255 - ); + assert_eq!(memo.condition_visits(), 1 + 5 * 255); } /// A MultiItemVariationStore with one region (axis 0: 0, 1, 1) and one /// subtable that names it `mentions` times, whose one delta set holds a /// delta of 1 per mention. -fn mention_store(mentions: u16) -> Vec { +pub(crate) fn mention_store(mentions: u16) -> Vec { let mut store = Vec::new(); store.extend_from_slice(&1u16.to_be_bytes()); // format store.extend_from_slice(&12u32.to_be_bytes()); // region list @@ -836,11 +836,10 @@ fn mention_store(mentions: u16) -> Vec { store } -/// A VARC table with one component of glyph 10 gated by a chain of -/// `levels` And tables, each with two offsets that both name the next, -/// ending in the Value condition `default + delta > 0` over -/// [`mention_store`]. -fn deep_value_condition_table(levels: usize, mentions: u16, default: i16) -> Vec { +/// A chain of `levels` And tables, each with two offsets that both name +/// the next, ending in the Value condition `default + delta > 0`, whose +/// delta is variation index 0. +pub(crate) fn deep_value_condition(levels: usize, default: i16) -> Vec { let mut condition = vec![0, 2]; condition.extend_from_slice(&default.to_be_bytes()); condition.extend_from_slice(&0u32.to_be_bytes()); @@ -852,9 +851,15 @@ fn deep_value_condition_table(levels: usize, mentions: u16, default: i16) -> Vec and.extend_from_slice(&condition); condition = and; } + condition +} + +/// A VARC table with one component of glyph 10 gated by +/// [`deep_value_condition`] over [`mention_store`]. +fn deep_value_condition_table(levels: usize, mentions: u16, default: i16) -> Vec { with_conditions( build_varc(&[1], &[&gated(10, 0)], Some(&mention_store(mentions)), None), - &condition_list(&[condition]), + &condition_list(&[deep_value_condition(levels, default)]), ) } @@ -863,26 +868,30 @@ fn deep_shared_value_conditions_cost_linear_work() { // 20 And levels whose two offsets name the same child (2^20 paths) // over a Value condition whose delta set walks 60,000 mentions of // one region. Each table is evaluated once and the subtable's - // scalars are worked out once: 41 table visits, 60,000 scalar steps, - // and 60,000 delta values. Walking every path took 2^20 times that. + // scalars are worked out once: 41 table visits, 60,000 scalar steps + // and one region axis, and 60,000 delta values, besides keeping the + // coords (2 units), the record (1), the 40 And offsets (40) and the + // component's coords (1). Walking every path took 2^20 times that. let table = deep_value_condition_table(20, 60_000, -30_000); let varc = Varc::parse(&table).unwrap(); // At axis 0 = 1 the deltas add 60,000: the condition holds. - let mut eval = Eval::new(&[1.0]); - let c = varc.composite_in(1, &[1.0], &mut eval).unwrap(); + let mut memo = VarcMemo::new(); + let c = varc.composite_in(1, &[1.0], &[1.0], &mut memo).unwrap(); assert_eq!(c.components.len(), 1); - assert_eq!(MAX_CONDITION_TABLES - eval.condition_visits_left, 41); - assert_eq!(MAX_COMPOSITE_WORK - eval.work_left, 120_000); + assert_eq!(memo.condition_visits(), 41); + assert_eq!(memo.work_done(), 120_045); // At 0.25 they add 15,000: it does not. assert!(varc.composite(1, &[0.25]).unwrap().components.is_empty()); } #[test] -fn delta_work_past_the_budget_adds_nothing() { +fn delta_work_past_the_budget_ends_the_composite() { // Twenty components whose translation varies through the delta set - // of 60,000 region mentions. The first costs 120,000 units (the - // scalars, then the walk), each later one 60,000, so 16 fit the - // 2^20 budget and the rest keep their stored translation. + // of 60,000 region mentions. The first costs 120,004 units (its + // record and coords, the scalars and their one axis, then the + // walk), each later one 60,002, after the 2 that keep the coords: + // 16 fit the 2^20 budget. The 17th is read, but its deltas do not + // fit, so it keeps its stored translation, and the list ends there. let flags = (VC_TRANSFORM_HAS_VARIATION | VC_HAVE_TRANSLATE_X) as u8; let mut record = Vec::new(); for _ in 0..20 { @@ -894,7 +903,32 @@ fn delta_work_past_the_budget_adds_nothing() { let c = varc.composite(1, &[1.0]).unwrap(); let tx: Vec = c.components.iter().map(|c| c.transform[4]).collect(); assert_eq!(tx[..16], [60_000.0; 16]); - assert_eq!(tx[16..], [0.0; 4]); + assert_eq!(tx[16..], [0.0]); +} + +#[test] +fn a_memo_keeps_composites_and_scalars_by_coords() { + // Resolving glyph 1 again at the same coords costs nothing; at + // other coords it costs its record, coords and deltas again. + let flags = (VC_TRANSFORM_HAS_VARIATION | VC_HAVE_TRANSLATE_X) as u8; + let mut record = vec![flags, 0x00, 0x05, 0x00]; + record.extend_from_slice(&0i16.to_be_bytes()); + let table = build_varc(&[1], &[&record], Some(&mention_store(1000)), None); + let varc = Varc::parse(&table).unwrap(); + let mut memo = VarcMemo::new(); + let first = varc.resolve(1, &[1.0], &[], &mut memo).unwrap(); + // Keeping [1.0] (2), the record (1), its coords (1), the scalars + // (1000 and one axis) and the walk (1000). + assert_eq!(memo.work_done(), 2005); + let again = varc.resolve(1, &[1.0], &[], &mut memo).unwrap(); + assert!(Rc::ptr_eq(&first, &again)); + assert_eq!(memo.work_done(), 2005); + let half = varc.resolve(1, &[0.5], &[], &mut memo).unwrap(); + assert_eq!(half.components[0].transform[4], 500.0); + assert_eq!(memo.work_done(), 2 * 2005); + // A glyph VARC has no record for costs nothing. + assert!(varc.resolve(5, &[0.25], &[], &mut memo).is_none()); + assert_eq!(memo.work_done(), 2 * 2005); } #[test] From 1a5e3fd3d397bb692d1367bf467cfb42e8b54c03 Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 13:52:18 -0800 Subject: [PATCH 4/7] Cut VARC cycles and draw missing records as HarfBuzz does Two more differences from HarfBuzz 14.5.0's VARC::get_path_at, found while checking the walk against it: - A cycle longer than a self-reference (A names B, B names A) recursed until the 64-level cap and failed the glyph. HarfBuzz walks the cycle until its hb_decycler_t, a tortoise moving at half the speed of the path, meets the glyph again: a covered glyph at position k of the path of glyphs being drawn is skipped when the glyph at position k / 2 is the same. The walk keeps that path and applies the same rule, so a two-glyph cycle draws its leaves twice over and stops, as in HarfBuzz. - A glyph the VARC coverage names past the end of the glyph records fell back to its glyf or CFF outline. HarfBuzz reads such a record as empty and draws nothing, at the top or as a component. Varc's composite and composite_with_font_coords now return no components for it (still None for an uncovered glyph, as documented). --- src/face/outlines.rs | 8 +++++--- src/face/varc_draw.rs | 40 ++++++++++++++++++++++++++++------------ src/tables/varc.rs | 30 +++++++++++++----------------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/face/outlines.rs b/src/face/outlines.rs index c0cf89af..d3eb3d91 100644 --- a/src/face/outlines.rs +++ b/src/face/outlines.rs @@ -174,15 +174,17 @@ impl<'a> Face<'a> { /// ([`Glyf::outline_at_coords`], [`Cff2::outline`]) take coords as /// given. /// - /// A glyph the font's `VARC` table has a record for is drawn from - /// its components, as HarfBuzz draws it: + /// A glyph the font's `VARC` table covers is drawn from its + /// components, as HarfBuzz draws it: /// /// - Each component is drawn at its own coords, which start from /// the coords of the glyph it belongs to, or from the font's /// (`coords`) when it sets `RESET_UNSPECIFIED_AXES`, however deep /// it sits. /// - A component that names the glyph it belongs to draws that - /// glyph's `glyf` or CFF outline instead of recursing. + /// glyph's `glyf` or CFF outline instead of recursing. A longer + /// cycle is cut where HarfBuzz's decycler cuts it, and a covered + /// glyph past the end of the glyph records draws nothing. /// - The condition and delta work of every composite the glyph /// reaches shares one budget (see /// [`Varc::composite_with_font_coords`](crate::tables::Varc::composite_with_font_coords)), diff --git a/src/face/varc_draw.rs b/src/face/varc_draw.rs index e8fba55d..f19c239b 100644 --- a/src/face/varc_draw.rs +++ b/src/face/varc_draw.rs @@ -4,6 +4,8 @@ use core::cell::OnceCell; +use alloc::vec::Vec; + use super::Face; use crate::error::{Error, Result}; use crate::tables::glyf::PhantomMetrics; @@ -31,12 +33,16 @@ const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; /// The walk resolves composites with one [`VarcMemo`], so one budget /// bounds the condition and delta work of every composite the glyph /// reaches, and a glyph reached again at the same coords is resolved -/// once. Every composite is resolved in the font set to the coords of -/// the glyph drawn, so a `RESET_UNSPECIFIED_AXES` component starts from -/// them however deep it sits. Components that share children can make -/// the walk grow exponentially with depth, so it also draws at most +/// once. Components that share children can make the walk grow +/// exponentially with depth, so it also draws at most /// [`MAX_VARC_COMPONENTS`] components and [`MAX_VARC_OPS`] ops, and /// fails with `Malformed` past either or past [`MAX_VARC_DEPTH`]. +/// +/// It stops cycles as HarfBuzz's decycler does: `path` holds the +/// glyphs whose composites are being drawn, from the glyph drawn down, +/// and a glyph that `VARC` covers is not drawn at position `k` of the +/// path when it is the glyph at position `k / 2`, where HarfBuzz's +/// tortoise sits. pub(crate) struct VarcDraw<'w, 'a> { face: &'w Face<'a>, varc: &'w Varc<'a>, @@ -44,6 +50,8 @@ pub(crate) struct VarcDraw<'w, 'a> { font_coords: &'w [f32], memo: VarcMemo, leaves: Leaves<'a>, + /// The glyphs whose composites are being drawn, outermost first. + path: Vec, components_left: usize, ops_left: usize, } @@ -58,20 +66,23 @@ impl<'w, 'a> VarcDraw<'w, 'a> { font_coords, memo: VarcMemo::new(), leaves: Leaves::default(), + path: Vec::new(), components_left: MAX_VARC_COMPONENTS, ops_left: MAX_VARC_OPS, } } /// Draws `glyph_id` into `sink`, as HarfBuzz draws a glyph `VARC` - /// has a record for. Returns `Ok(false)`, drawing nothing, when it - /// has none; the glyph is then drawn from `glyf` or CFF. + /// covers. Returns `Ok(false)`, drawing nothing, when it does not + /// cover the glyph, which is then drawn from `glyf` or CFF. pub(crate) fn draw(&mut self, glyph_id: u16, sink: &mut S) -> Result { let coords = self.font_coords; let Some(composite) = self.varc.resolve(glyph_id, coords, coords, &mut self.memo) else { return Ok(false); }; + self.path.push(glyph_id); self.draw_composite(glyph_id, &composite, IDENTITY, 0, sink)?; + self.path.pop(); Ok(true) } @@ -126,14 +137,19 @@ impl<'w, 'a> VarcDraw<'w, 'a> { } // A component that names its parent's glyph draws that glyph's // own outline, as in HarfBuzz, which does not recurse on the - // same glyph. Other components VARC has a record for are - // composites, resolved in the font set to the coords of the - // glyph drawn; the rest are drawn from `glyf` or CFF at their - // coords. - if gid != parent { + // same glyph. Other components VARC covers are composites, + // unless the decycler stops them; the rest are drawn from + // `glyf` or CFF at their coords. + if gid != parent && self.varc.covers(gid) { + if self.path.get(self.path.len() / 2) == Some(&gid) { + return Ok(()); + } let font_coords = self.font_coords; if let Some(composite) = self.varc.resolve(gid, coords, font_coords, &mut self.memo) { - return self.draw_composite(gid, &composite, transform, depth, sink); + self.path.push(gid); + let drawn = self.draw_composite(gid, &composite, transform, depth, sink); + self.path.pop(); + return drawn; } } let mut placed = Placed { diff --git a/src/tables/varc.rs b/src/tables/varc.rs index 6e4cd93a..48e845db 100644 --- a/src/tables/varc.rs +++ b/src/tables/varc.rs @@ -243,16 +243,10 @@ impl<'a> Varc<'a> { self.glyph_records.len() } - /// Whether `gid` has a glyph record, so that VARC draws it: it is in - /// the coverage, at an index the glyph records reach. - pub(crate) fn has_record(&self, gid: u16) -> bool { - self.coverage - .index_of(gid) - .is_some_and(|idx| (idx as usize) < self.glyph_records.len()) - } - /// Resolves the component list for `gid` at the given normalized - /// axis coords. Returns `None` for uncovered gids. + /// axis coords. Returns `None` for uncovered gids. A covered gid + /// past the end of the glyph records has no components, as in + /// HarfBuzz, which draws nothing for it. /// /// `coords` are both the glyph's coords and the font's, which is /// right for a glyph drawn on its own. For a glyph reached through @@ -264,9 +258,10 @@ impl<'a> Varc<'a> { /// Resolves the component list for `gid`, whose own coords are /// `coords`, in a font set to `font_coords`. Returns `None` for - /// uncovered gids. The two differ when `gid` is a component of - /// another VARC composite: `coords` are then that component's - /// coords. + /// uncovered gids, and no components for a covered gid past the end + /// of the glyph records. The two coord vectors differ when `gid` is + /// a component of another VARC composite: `coords` are then that + /// component's coords. /// /// Components are read as HarfBuzz's `decompile_record` reads them /// and evaluated as its `VarComponent::get_path_at` does: @@ -309,8 +304,8 @@ impl<'a> Varc<'a> { /// budgets, and keeps each composite, condition result and set of /// region scalars by the coords it was worked out at, so a glyph /// reached again at the same coords costs nothing more. `None` when - /// VARC has no record for `gid`. Every call with one `memo` must - /// pass the same `font_coords`. + /// VARC does not cover `gid`. Every call with one `memo` must pass + /// the same `font_coords`. pub(crate) fn resolve( &self, gid: u16, @@ -318,7 +313,7 @@ impl<'a> Varc<'a> { font_coords: &[f32], memo: &mut VarcMemo, ) -> Option> { - if !self.has_record(gid) { + if !self.covers(gid) { return None; } let id = memo.coords_id(coords); @@ -346,7 +341,7 @@ impl<'a> Varc<'a> { font_coords: &[f32], memo: &mut VarcMemo, ) -> Option { - if !self.has_record(gid) { + if !self.covers(gid) { return None; } let id = memo.coords_id(coords); @@ -365,7 +360,8 @@ impl<'a> Varc<'a> { memo: &mut VarcMemo, ) -> Option { let idx = self.coverage.index_of(gid)? as usize; - let raw = *self.glyph_records.get(idx)?; + // HarfBuzz reads a record past the end of the INDEX as empty. + let raw = self.glyph_records.get(idx).copied().unwrap_or_default(); let mut scratch = CoordsMemo::default(); let caches = match id { Some(id) => &mut memo.per_coords[id], From 3cbb9c79e197770a89d73fa27644f6eb1acfeff8 Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 14:03:27 -0800 Subject: [PATCH 5/7] Give VARC a zero per axis at the default instance hb_font_create sets a face's default variations, so a HarfBuzz font with fvar holds one coord per axis even at the default instance, and VARC applies deltas whenever the font has coords: a region that constrains no axis holds there, and its deltas apply. sigilbuzz reads coords that all round to zero as no coords at all, so the same glyph skipped those deltas (a component on such a region drew 100 units off in a crafted font). A font without fvar has no coords in HarfBuzz either, and still gets none. The walk now starts from one zero per fvar axis when the coords are empty, for Face::glyph_outline_at_coords and GlyphOutlines alike. The walk's work-count tests move with it (their font has three axes, so its coord vectors cost three units each). --- src/face/outlines.rs | 4 +++ src/face/varc_draw.rs | 78 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/face/outlines.rs b/src/face/outlines.rs index d3eb3d91..ea9638c2 100644 --- a/src/face/outlines.rs +++ b/src/face/outlines.rs @@ -185,6 +185,10 @@ impl<'a> Face<'a> { /// glyph's `glyf` or CFF outline instead of recursing. A longer /// cycle is cut where HarfBuzz's decycler cuts it, and a covered /// glyph past the end of the glyph records draws nothing. + /// - At the default instance of a font with `fvar` (`coords` empty or + /// all zero), VARC reads one zero coord per axis, as HarfBuzz's + /// font holds them, so deltas from a region that constrains no + /// axis apply there too. /// - The condition and delta work of every composite the glyph /// reaches shares one budget (see /// [`Varc::composite_with_font_coords`](crate::tables::Varc::composite_with_font_coords)), diff --git a/src/face/varc_draw.rs b/src/face/varc_draw.rs index f19c239b..704be4ab 100644 --- a/src/face/varc_draw.rs +++ b/src/face/varc_draw.rs @@ -4,6 +4,7 @@ use core::cell::OnceCell; +use alloc::vec; use alloc::vec::Vec; use super::Face; @@ -46,8 +47,9 @@ const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; pub(crate) struct VarcDraw<'w, 'a> { face: &'w Face<'a>, varc: &'w Varc<'a>, - /// The font's coords, rounded to F2DOT14. - font_coords: &'w [f32], + /// The font's coords, rounded to F2DOT14: one zero per `fvar` axis + /// at the default instance, as HarfBuzz's font holds them. + font_coords: Vec, memo: VarcMemo, leaves: Leaves<'a>, /// The glyphs whose composites are being drawn, outermost first. @@ -59,7 +61,23 @@ pub(crate) struct VarcDraw<'w, 'a> { impl<'w, 'a> VarcDraw<'w, 'a> { /// A walk over `varc`, the `VARC` table of `face`, in a font set to /// `font_coords` (rounded to F2DOT14). - pub(crate) fn new(face: &'w Face<'a>, varc: &'w Varc<'a>, font_coords: &'w [f32]) -> Self { + /// + /// HarfBuzz gives a font with `fvar` one coord per axis even at the + /// default instance, and VARC applies deltas whenever the font has + /// coords, so there a region that holds everywhere (one that + /// constrains no axis) applies its deltas. Empty coords become a + /// zero per axis for that; a font without `fvar` keeps none. + pub(crate) fn new(face: &'w Face<'a>, varc: &'w Varc<'a>, font_coords: &[f32]) -> Self { + let font_coords = if font_coords.is_empty() { + let axes = face + .fvar() + .ok() + .flatten() + .map_or(0, |fvar| fvar.axes().len()); + vec![0.0; axes] + } else { + font_coords.to_vec() + }; Self { face, varc, @@ -76,7 +94,7 @@ impl<'w, 'a> VarcDraw<'w, 'a> { /// covers. Returns `Ok(false)`, drawing nothing, when it does not /// cover the glyph, which is then drawn from `glyf` or CFF. pub(crate) fn draw(&mut self, glyph_id: u16, sink: &mut S) -> Result { - let coords = self.font_coords; + let coords = &self.font_coords; let Some(composite) = self.varc.resolve(glyph_id, coords, coords, &mut self.memo) else { return Ok(false); }; @@ -144,7 +162,7 @@ impl<'w, 'a> VarcDraw<'w, 'a> { if self.path.get(self.path.len() / 2) == Some(&gid) { return Ok(()); } - let font_coords = self.font_coords; + let font_coords = &self.font_coords; if let Some(composite) = self.varc.resolve(gid, coords, font_coords, &mut self.memo) { self.path.push(gid); let drawn = self.draw_composite(gid, &composite, transform, depth, sink); @@ -451,14 +469,15 @@ mod tests { #[test] fn components_at_distinct_coords_share_one_work_budget() { // 600 components, each reaching glyph 4 at its own coords, whose - // condition and deltas cost 180,047 units there: 2 to keep the - // coords, 2 for the records, 2 for their coords, 40 for the And - // offsets, 120,001 for the Value condition (scalars, one region - // axis, the walk) and 60,000 for the translation. One budget per - // composite let every one of them spend that, 108 million units - // in all. Now glyph 3 (1801 units: its coords, its 600 records - // and their coords) and five copies of glyph 4 fit, the sixth - // runs out at its translation, and the rest draw nothing. + // condition and deltas cost 180,053 units there: 4 to keep the + // coords (one per axis of the font, and one), 2 for the records, + // 6 for their coords, 40 for the And offsets, 120,001 for the + // Value condition (scalars, one region axis, the walk) and 60,000 + // for the translation. One budget per composite let every one of + // them spend that, 108 million units in all. Now glyph 3 (3004 + // units: its coords, its 600 records, their axis values and their + // coords) and five copies of glyph 4 fit, the sixth runs out at + // its translation, and the rest draw nothing. // Axis 0 starts just past 0.5, where the condition starts to hold. let axis0: Vec = (0..600).map(|i| 8193 + i).collect(); let (out, work, visits) = walk(&heavy_fan_out(&axis0)); @@ -478,13 +497,42 @@ mod tests { #[test] fn a_glyph_reached_again_at_the_same_coords_is_resolved_once() { - // 600 components reaching glyph 4 at axis 0 = 1: its 180,047 + // 600 components reaching glyph 4 at axis 0 = 1: its 180,053 // units are spent once, and every copy is drawn with them. let (out, work, visits) = walk(&heavy_fan_out(&[16384; 600])); - assert_eq!(work, 1 + 600 * 3 + 180_047); + assert_eq!(work, 4 + 600 * 5 + 180_053); assert_eq!(visits, 41); let xs = move_xs(&out); assert_eq!(xs.len(), 1200); assert!(xs.chunks(2).all(|pair| pair == [0.0, 60_000.0])); } + + #[test] + fn the_default_instance_applies_regions_without_axes() { + // A region that constrains no axis holds everywhere. HarfBuzz's + // font holds a zero per axis at the default instance, so VARC + // applies its deltas there: glyph 3 moves the box by 100. + let mut store = Vec::new(); + store.extend_from_slice(&[0, 1, 0, 0, 0, 12, 0, 1, 0, 0, 0, 20]); + store.extend_from_slice(&[0, 1, 0, 0, 0, 6, 0, 0]); // one region, no axes + store.extend_from_slice(&[1, 0, 1, 0, 0]); // one region index: 0 + store.extend_from_slice(&[0, 0, 0, 1, 1, 1, 3, 0x00, 100]); // one set: 100 + // TRANSFORM_HAS_VARIATION and HAVE_TRANSLATE_X, the box, index 0. + let record = [0x18, 0x00, 0x01, 0x00, 0x00, 0x00]; + let table = build_varc(&[3], &[&record], Some(&store), None); + let font = with_varc( + include_bytes!("../../tests/fixtures/varc_parity.ttf"), + &table, + ); + let (out, _, _) = walk(&font); + assert_eq!(move_xs(&out), [100.0]); + // Coords given as zeros draw the same, and so does a font set + // away from the default, where the region holds too. + let blob = Blob::new(&font); + let face = Face::parse(&blob, 0).unwrap(); + let at = + |coords: &[f32]| move_xs(&face.glyph_outline_at_coords(3, coords).unwrap().unwrap()); + assert_eq!(at(&[0.0, 0.0, 0.0]), [100.0]); + assert_eq!(at(&[0.5]), [100.0]); + } } From 64206292d449e8984c035709b766271f76eeeac0 Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 14:04:35 -0800 Subject: [PATCH 6/7] Check nested VARC composites against HarfBuzz tests/fixtures/varc_face.ttf has the axes of varc_parity.ttf and 41 glyphs the VARC coverage names: reset components two and three composites down, a reset in the middle of a chain and one whose axis values vary; components naming their own glyph (which carries a varied glyf outline of its own), at the top, one composite down, and with a reset; cycles of two and three glyphs; a covered glyph past the glyph records and a glyph using it; delta sets that end early, cut a run in the first, a middle or the last tuple, are empty, or cut an i32 run, for translations, axis values and a Value condition; conditions read inside nested composites, one gating a reset component; one glyph reached three times at the same coords and once elsewhere; and a region without axes, for a translation, a condition and a nested composite. tests/tools/build_varc_face_fixtures.py writes it byte by byte with the encoders of build_varc_morx_parity_fixtures.py, and tests/tools/varc_face_expected.py records HarfBuzz 14.5.0's outlines (uharfbuzz 0.56.2) at the eight locations varc_parity.expected uses. tests/varc_parity.rs now checks every glyph of both fonts through Face::glyph_outline_at_coords, GlyphOutlines::outline and GlyphOutlines::draw, and the parity font also through composite_with_font_coords. v_outer, the nested reset, no longer needs its own path. Before this branch the new font matched HarfBuzz on 14 to 22 of its 41 glyphs per location; now all 41 at all eight. --- tests/fixtures/varc_face.expected | 330 ++++++++++++++++++++++++ tests/fixtures/varc_face.ttf | Bin 0 -> 3080 bytes tests/tools/build_varc_face_fixtures.py | 322 +++++++++++++++++++++++ tests/tools/varc_face_expected.py | 57 ++++ tests/varc_parity.rs | 121 +++++++-- 5 files changed, 807 insertions(+), 23 deletions(-) create mode 100644 tests/fixtures/varc_face.expected create mode 100644 tests/fixtures/varc_face.ttf create mode 100644 tests/tools/build_varc_face_fixtures.py create mode 100644 tests/tools/varc_face_expected.py diff --git a/tests/fixtures/varc_face.expected b/tests/fixtures/varc_face.expected new file mode 100644 index 00000000..d99fccfa --- /dev/null +++ b/tests/fixtures/varc_face.expected @@ -0,0 +1,330 @@ +# HarfBuzz 14.5.0 (uharfbuzz 0.56.2) outlines of varc_face.ttf. +# outline : M/L x y, Q cx cy x y, C ..., Z. +outline default 3 n2_outer M 5 10 L 105 10 L 105 110 L 0.9998 122.0007 Z +outline default 4 n2_mid M 5 0 L 105 0 L 105 100 L 0.9998 112.0007 Z +outline default 5 n3_top M 20 10 L 120 10 L 120 110 L 14.0002 127.9993 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline default 6 n3_mid1 M 20 0 L 120 0 L 120 100 L 14.0002 117.9993 Z M 170 0 L 270 0 L 270 100 L 164.0002 117.9993 Z +outline default 7 n3_mid2 M 0 0 L 100 0 L 100 100 L -5.9998 117.9993 Z M 150 0 L 250 0 L 250 100 L 144.0002 117.9993 Z +outline default 8 nr_top M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 250 20 L 250 120 L 150 120 Z +outline default 9 nr_mid M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 250 20 L 250 120 L 150 120 Z +outline default 10 nr_low M 0 0 L 125 0 L 125 100 L 0 100 Z M 150 0 L 250 0 L 250 100 L 150 100 Z +outline default 11 nv_outer M 0 0 L 106.25 0 L 106.4453 102.6953 L 0 102.5 Z +outline default 12 nv_mid M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 13 s_self M 250 0 L 320 50 L 250 100 L 200 50 Z M 0 -120 L 100 -120 L 100 -20 L 0 -20 Z +outline default 14 s_nest M 375 0 L 400 25 L 375 42.5 L 350 25 Z M 50 150 L 150 150 L 150 250 L 50 250 Z +outline default 15 s_inner M 325 0 L 350 25 L 325 50 L 300 25 Z M 0 150 L 100 150 L 100 250 L 0 250 Z +outline default 16 s_reset M 44.1942 26.5165 L 35.3553 106.066 L -35.3553 106.066 L -47.7297 22.981 Z +outline default 17 d_full M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 18 d_short M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 19 d_cut_first M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 20 d_cut_last M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 21 d_empty M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 22 d_long M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 23 d_three_first M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 24 d_three_mid M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 25 d_three_last M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 26 d_axes M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 27 d_axes_short M 0 0 L 100 0 L 100 100 L 0 100 Z M 200 0 L 400 0 L 300 300 Z +outline default 28 c_outer M 0 0 L 129.9988 0 L 129.9988 100 L 0 100 Z M 150 0 L 350 0 L 250 300 Z M 450 0 L 650 0 L 550 300 Z M 600 0 L 729.9988 0 L 729.9988 100 L 600 100 Z M 450 300 L 650 300 L 550 600 Z M 600 600 L 700 600 L 700 731.9995 L 600 731.9995 Z +outline default 29 c_inner M 450 0 L 650 0 L 550 300 Z +outline default 30 c_reset_outer M 10 0 L 110 0 L 110 100 L 10 100 Z +outline default 31 c_reset_inner +outline default 32 m_outer M 0 0 L 100 0 L 100 100 L -10 130 Z M 300 0 L 400 0 L 400 100 L 290 130 Z M 0 300 L 100 300 L 100 400 L -10 430 Z M 315 300 L 415 300 L 415 400 L 315 400 Z +outline default 33 m_inner M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 34 y_a M 0 0 L 100 0 L 100 100 L 0 100 Z M 200 150 L 400 150 L 300 450 Z M 300 0 L 389.978 0 L 389.978 79.9805 L 300 79.9805 Z +outline default 35 y_b M 0 150 L 200 150 L 100 450 Z M 100 0 L 179.9805 0 L 179.9805 79.9805 L 100 79.9805 Z M 259.9609 119.9707 L 419.9219 119.9707 L 339.9414 359.9121 Z +outline default 36 z_a M 150 200 L 350 200 L 250 500 Z M 252.5391 -163.2812 L 404.2782 -163.2812 L 320.8984 61.824 Z M 150 -300 L 218.3594 -300 L 218.3594 -221.627 L 150 -221.627 Z M 0 0 L 100 0 L 100 114.6484 L 0 114.6484 Z M 0 0 L 100 0 L 100 100 L 0 100 Z +outline default 37 z_b M 0 200 L 200 200 L 100 500 Z M 102.5391 -163.2812 L 254.2782 -163.2812 L 170.8984 61.824 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L 0 -221.627 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L 0 -221.627 Z M -150 0 L -50 0 L -50 114.6484 L -150 114.6484 Z +outline default 38 z_c M 102.5391 -163.2812 L 239.2578 -163.2812 L 170.8984 41.7969 Z M 102.5391 -505.0781 L 149.2691 -505.0781 L 149.2691 -451.5029 L 102.5391 -451.5029 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L 0 -221.627 Z M 0 -300 L 68.3594 -300 L 68.3594 -231.6406 L 0 -231.6406 Z M -150 0 L -50 0 L -50 100 L -150 100 Z +outline default 39 x_user M 250 0 L 450 0 L 350 300 Z +outline default 40 k_const M 60 -40 L 160 -40 L 160 60 L 60 60 Z +outline default 41 k_cond M 0 0 L 200 0 L 100 300 Z M 300 0 L 400 0 L 400 100 L 300 100 Z +outline default 42 k_outer M 60 160 L 160 160 L 160 260 L 60 260 Z M 400 0 L 600 0 L 500 300 Z M 700 0 L 800 0 L 800 100 L 700 100 Z +outline default 43 x_norecord +outline AAAA=50,BBBB=-30,CCCC=70 3 n2_outer M 5 10 L 130 10 L 130 110 L 0.9998 122.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 4 n2_mid M 5 0 L 130 0 L 130 100 L 0.9998 112.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 5 n3_top M 20 10 L 145 10 L 145 110 L 14.0002 127.9993 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=50,BBBB=-30,CCCC=70 6 n3_mid1 M 20 0 L 145 0 L 145 100 L 14.0002 117.9993 Z M 170 0 L 270 0 L 270 100 L 164.0002 117.9993 Z +outline AAAA=50,BBBB=-30,CCCC=70 7 n3_mid2 M 0 0 L 125 0 L 125 100 L -5.9998 117.9993 Z M 150 0 L 275 0 L 275 100 L 144.0002 117.9993 Z +outline AAAA=50,BBBB=-30,CCCC=70 8 nr_top M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 275 20 L 275 120 L 135.9998 162.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 9 nr_mid M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 275 20 L 275 120 L 135.9998 162.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 10 nr_low M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z M 150 0 L 275 0 L 275 100 L 135.9998 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 11 nv_outer M 0 0 L 106.25 0 L 106.4453 102.6953 L -14.0002 144.5007 Z +outline AAAA=50,BBBB=-30,CCCC=70 12 nv_mid M 0 0 L 100 0 L 100 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 13 s_self M 250 -17.5003 L 320 50 L 250 91.0004 L 175.4996 50 Z M 0 -120 L 125 -120 L 125 -20 L -14.0002 22.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 14 s_nest M 375 -8.7502 L 410 25 L 375 42.5 L 337.7498 25 Z M 50 150 L 175 150 L 175 250 L 35.9998 292.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 15 s_inner M 325 -8.7502 L 360 25 L 325 45.5002 L 287.7498 25 Z M 0 150 L 125 150 L 125 250 L -14.0002 292.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 16 s_reset M 44.1942 26.5165 L 49.4975 120.2082 L -28.9916 99.7023 L -47.7297 22.981 Z +outline AAAA=50,BBBB=-30,CCCC=70 17 d_full M 20 5 L 145 5 L 145 105 L 5.9998 147.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 18 d_short M 20 5 L 145 5 L 145 105 L 5.9998 147.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 19 d_cut_first M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 20 d_cut_last M 20 5 L 145 5 L 145 105 L 5.9998 147.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 21 d_empty M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 22 d_long M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 23 d_three_first M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 24 d_three_mid M 19.2001 -10 L 144.2001 -10 L 144.2001 90 L 5.1998 132.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 25 d_three_last M 40.0006 10 L 165.0006 10 L 165.0006 110 L 26.0004 152.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 26 d_axes M 0 0 L 100 0 L 100 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 27 d_axes_short M 0 0 L 100 0 L 100 100 L -14.0002 142.0007 Z M 200 0 L 407.5 0 L 300 310 Z +outline AAAA=50,BBBB=-30,CCCC=70 28 c_outer M 0 0 L 129.9988 0 L 129.9988 100 L -14.0002 142.0007 Z M 150 -14.0002 L 350 0 L 221.9995 300 Z M 450 -14.0002 L 650 0 L 521.9995 300 Z M 600 0 L 729.9988 0 L 729.9988 100 L 585.9998 142.0007 Z M 450 285.9998 L 650 300 L 521.9995 600 Z M 0 600 L 125 600 L 134.9998 741.9994 L -14.0002 774.0002 Z M 150 585.9998 L 397.9993 600 L 221.9995 963.999 Z M 600 600 L 725 600 L 734.9999 741.9994 L 585.9998 774.0002 Z +outline AAAA=50,BBBB=-30,CCCC=70 29 c_inner M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z M 150 -14.0002 L 350 0 L 221.9995 300 Z M 450 -14.0002 L 650 0 L 521.9995 300 Z M 600 0 L 725 0 L 725 100 L 585.9998 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 30 c_reset_outer M 10 0 L 135 0 L 135 100 L -4.0002 142.0007 Z M 200 -14.0002 L 400 0 L 271.9995 300 Z +outline AAAA=50,BBBB=-30,CCCC=70 31 c_reset_inner M 10 0 L 135 0 L 135 100 L -4.0002 142.0007 Z M 200 -14.0002 L 400 0 L 271.9995 300 Z +outline AAAA=50,BBBB=-30,CCCC=70 32 m_outer M 20 5 L 145 5 L 145 105 L 10 135 Z M 120 -10 L 320 0 L 200 300 Z M 320 5 L 445 5 L 445 105 L 310 135 Z M 420 -10 L 620 0 L 500 300 Z M 20 305 L 145 305 L 145 405 L 10 435 Z M 120 290 L 320 300 L 200 600 Z M 335 305 L 460 305 L 460 405 L 335 405 Z M 420 300 L 620 300 L 520 600 Z +outline AAAA=50,BBBB=-30,CCCC=70 33 m_inner M 20 5 L 145 5 L 145 105 L 5.9998 147.0007 Z M 120 -14.0002 L 320 0 L 191.9995 300 Z +outline AAAA=50,BBBB=-30,CCCC=70 34 y_a M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z M 200 135.9998 L 400 150 L 271.9995 450 Z M 300 0 L 389.978 0 L 389.978 79.9805 L 288.8026 113.5729 Z +outline AAAA=50,BBBB=-30,CCCC=70 35 y_b M 0 135.9998 L 200 150 L 71.9995 450 Z M 100 0 L 199.9756 0 L 199.9756 79.9805 L 88.8025 113.5729 Z M 259.9609 108.7732 L 419.9219 119.9707 L 317.5465 359.9121 Z +outline AAAA=50,BBBB=-30,CCCC=70 36 z_a M 150 185.9998 L 350 200 L 221.9995 500 Z M 252.5391 -172.8517 L 404.2782 -163.2812 L 301.7575 61.824 Z M 150 -300 L 235.4492 -300 L 238.5785 -218.4978 L 140.4295 -192.9156 Z M 0 0 L 125 0 L 129.5776 119.2261 L -14.0002 156.6492 Z M 0 0 L 125 0 L 125 100 L -14.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 37 z_b M 0 185.9998 L 200 200 L 71.9995 500 Z M 102.5391 -172.8517 L 254.2782 -163.2812 L 151.7575 61.824 Z M 0 -300 L 85.4492 -300 L 88.5785 -218.4978 L -9.5705 -192.9156 Z M 0 -300 L 85.4492 -300 L 88.5785 -218.4978 L -9.5705 -192.9156 Z M -150 0 L -25 0 L -20.4224 119.2261 L -164.0002 156.6492 Z +outline AAAA=50,BBBB=-30,CCCC=70 38 z_c M 102.5391 -172.8517 L 239.2578 -163.2812 L 151.7575 41.7969 Z M 102.5391 -505.0781 L 160.9516 -505.0781 L 163.0907 -449.3637 L 95.9967 -431.8759 Z M 0 -300 L 85.4492 -300 L 88.5785 -218.4978 L -9.5705 -192.9156 Z M 0 -300 L 85.4492 -300 L 85.4492 -231.6406 L -9.5705 -202.9292 Z M -150 0 L -25 0 L -25 100 L -164.0002 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 39 x_user M 250 -14.0002 L 450 0 L 321.9995 300 Z +outline AAAA=50,BBBB=-30,CCCC=70 40 k_const M 60 -40 L 185 -40 L 185 60 L 45.9998 102.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 41 k_cond M 0 -14.0002 L 200 0 L 71.9995 300 Z M 300 0 L 425 0 L 425 100 L 285.9998 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 42 k_outer M 60 160 L 185 160 L 185 260 L 45.9998 302.0007 Z M 400 -14.0002 L 600 0 L 471.9995 300 Z M 700 0 L 825 0 L 825 100 L 685.9998 142.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 43 x_norecord +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 3 n2_outer M 5 10 L 121.6504 10 L 127.2032 142.2325 L 0.9998 148.6804 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 4 n2_mid M 5 0 L 121.6504 0 L 127.2032 132.2325 L 0.9998 138.6804 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 5 n3_top M 20 10 L 136.6504 10 L 142.2032 142.2325 L 14.0002 154.679 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 6 n3_mid1 M 20 0 L 136.6504 0 L 142.2032 132.2325 L 14.0002 144.679 Z M 170 0 L 270 0 L 270 126.6797 L 164.0002 144.679 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 7 n3_mid2 M 0 0 L 116.6504 0 L 122.2032 132.2325 L -5.9998 144.679 Z M 150 0 L 266.6504 0 L 272.2032 132.2325 L 144.0002 144.679 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 8 nr_top M 12.0007 20 L 137.0007 20 L 145.3381 155.0171 L 12.0007 146.6797 Z M 153.75 20 L 270.4004 20 L 275.9532 152.2325 L 153.75 146.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 9 nr_mid M 12.0007 20 L 137.0007 20 L 145.3381 155.0171 L 12.0007 146.6797 Z M 153.75 20 L 270.4004 20 L 275.9532 152.2325 L 153.75 146.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 10 nr_low M 3.75 0 L 128.75 0 L 137.0874 135.0171 L 3.75 126.6797 Z M 153.75 0 L 270.4004 0 L 275.9532 132.2325 L 153.75 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 11 nv_outer M 3.75 0 L 110 0 L 110.1953 102.6953 L 3.75 102.5 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 12 nv_mid M 3.75 0 L 112.0874 0 L 112.435 103.6825 L 3.75 103.335 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 13 s_self M 250 0 L 320 50 L 250 100 L 200 50 Z M 3.75 -120 L 120.4004 -120 L 125.9532 12.2325 L 3.75 6.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 14 s_nest M 375 0 L 406.6602 25 L 375 42.5 L 350 25 Z M 53.75 150 L 170.4004 150 L 170.4004 250 L 53.75 250 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 15 s_inner M 325 0 L 356.6602 25 L 325 50 L 300 25 Z M 3.75 150 L 120.4004 150 L 125.9532 282.2325 L 3.75 276.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 16 s_reset M 44.1942 26.5165 L 44.7742 115.4849 L -35.3553 106.066 L -47.7297 22.981 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 17 d_full M -2.9395 16.6699 L 113.7109 16.6699 L 119.2638 148.9025 L -2.9395 143.3496 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 18 d_short M -2.9395 3.3301 L 113.7109 3.3301 L 119.2638 135.5626 L -2.9395 130.0098 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 19 d_cut_first M 7.085 0 L 123.7354 0 L 129.2882 132.2325 L 7.085 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 20 d_cut_last M 17.0703 3.3301 L 133.7207 3.3301 L 139.2735 135.5626 L 17.0703 130.0098 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 21 d_empty M 3.75 0 L 120.4004 0 L 125.9532 132.2325 L 3.75 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 22 d_long M 3.75 66.6992 L 120.4004 66.6992 L 125.9532 198.9317 L 3.75 193.3789 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 23 d_three_first M 7.085 0 L 123.7354 0 L 129.2882 132.2325 L 7.085 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 24 d_three_mid M 13.7402 -6.6602 L 130.3906 -6.6602 L 135.9435 125.5724 L 13.7402 120.0195 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 25 d_three_last M 27.0898 33.3398 L 143.7402 33.3398 L 149.2931 165.5724 L 27.0898 160.0195 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 26 d_axes M 3.75 0 L 112.0874 0 L 112.435 103.6825 L 3.75 103.335 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 27 d_axes_short M 3.75 0 L 112.0874 0 L 112.0874 100 L 3.75 100 Z M 200 0 L 409.9976 0 L 300 313.3301 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 28 c_outer M 3.75 0 L 133.7488 0 L 143.7533 136.6842 L 3.75 126.6797 Z M 150 0 L 390.0195 0 L 250 353.3594 Z M 450 0 L 690.0195 0 L 550 353.3594 Z M 603.75 0 L 733.7488 0 L 743.7532 136.6842 L 603.75 126.6797 Z M 603.75 300 L 703.75 300 L 703.75 426.6797 L 603.75 426.6797 Z M 3.75 600 L 120.4004 600 L 127.0604 738.6595 L 3.75 731.9995 Z M 150 600 L 397.9993 600 L 250 963.999 Z M 603.75 600 L 720.4004 600 L 727.0604 738.6595 L 603.75 731.9995 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 29 c_inner M 3.75 0 L 120.4004 0 L 125.9532 132.2325 L 3.75 126.6797 Z M 150 0 L 390.0195 0 L 250 353.3594 Z M 603.75 0 L 720.4004 0 L 725.9532 132.2325 L 603.75 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 30 c_reset_outer M 13.75 0 L 130.4004 0 L 135.9532 132.2325 L 13.75 126.6797 Z M 200 0 L 440.0195 0 L 300 353.3594 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 31 c_reset_inner M 200 0 L 440.0195 0 L 300 353.3594 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 32 m_outer M -6.6895 16.6699 L 109.9609 16.6699 L 115.5138 148.9025 L -16.6895 173.3496 Z M 120 -10 L 360.0195 0 L 200 353.3594 Z M 293.3105 16.6699 L 409.9609 16.6699 L 415.5138 148.9025 L 283.3105 173.3496 Z M 420 -10 L 660.0195 0 L 500 353.3594 Z M -6.6895 316.6699 L 109.9609 316.6699 L 115.5138 448.9025 L -16.6895 473.3496 Z M 120 290 L 360.0195 300 L 200 653.3594 Z M 308.3105 316.6699 L 424.9609 316.6699 L 430.5138 448.9025 L 308.3105 443.3496 Z M 420 300 L 660.0195 300 L 520 653.3594 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 33 m_inner M -2.9395 16.6699 L 113.7109 16.6699 L 119.2638 148.9025 L -2.9395 143.3496 Z M 120 0 L 360.0195 0 L 220 353.3594 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 34 y_a M 3.75 0 L 120.4004 0 L 125.9532 132.2325 L 3.75 126.6797 Z M 200 150 L 440.0195 150 L 300 503.3594 Z M 302.9993 0 L 392.9773 0 L 396.3114 104.6532 L 302.9993 101.319 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 35 y_b M 0 150 L 240.0195 150 L 100 503.3594 Z M 102.9993 0 L 196.2968 0 L 200.738 105.7602 L 102.9993 101.319 Z M 259.9609 119.9707 L 451.9297 119.9707 L 339.9414 402.5892 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 36 z_a M 150 200 L 390.0195 200 L 250 553.3594 Z M 252.5391 -163.2812 L 404.2782 -163.2812 L 320.8984 61.824 Z M 152.5635 -300 L 232.305 -300 L 234.3891 -219.5429 L 152.5635 -221.627 Z M 3.75 0 L 120.4004 0 L 123.4492 117.6972 L 3.75 114.6484 Z M 3.75 0 L 120.4004 0 L 125.9532 132.2325 L 3.75 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 37 z_b M 0 200 L 240.0195 200 L 100 553.3594 Z M 102.5391 -163.2812 L 254.2782 -163.2812 L 170.8984 61.824 Z M 2.5635 -300 L 82.305 -300 L 84.3891 -219.5429 L 2.5635 -221.627 Z M 2.5635 -300 L 82.305 -300 L 84.3891 -219.5429 L 2.5635 -221.627 Z M -146.25 0 L -29.5996 0 L -26.5508 117.6972 L -146.25 114.6484 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 38 z_c M 102.5391 -163.2812 L 266.6149 -163.2812 L 170.8984 78.273 Z M 104.2914 -505.0781 L 158.8022 -505.0781 L 160.2269 -450.0782 L 104.2914 -451.5029 Z M 2.5635 -300 L 82.305 -300 L 84.3891 -219.5429 L 2.5635 -221.627 Z M 2.5635 -300 L 82.305 -300 L 86.1008 -209.6067 L 2.5635 -213.4026 Z M -146.25 0 L -29.5996 0 L -24.0468 132.2325 L -146.25 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 39 x_user M 250 0 L 490.0195 0 L 350 353.3594 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 40 k_const M 63.75 -40 L 180.4004 -40 L 185.9532 92.2325 L 63.75 86.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 41 k_cond M 0 0 L 240.0195 0 L 100 353.3594 Z M 303.75 0 L 420.4004 0 L 425.9532 132.2325 L 303.75 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 42 k_outer M 63.75 160 L 180.4004 160 L 185.9532 292.2325 L 63.75 286.6797 Z M 400 0 L 640.0195 0 L 500 353.3594 Z M 703.75 0 L 820.4004 0 L 825.9532 132.2325 L 703.75 126.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 43 x_norecord +outline AAAA=-80,BBBB=90,CCCC=55 3 n2_outer M 5 10 L 105 10 L 105 146.001 L 0.9998 158.0017 Z +outline AAAA=-80,BBBB=90,CCCC=55 4 n2_mid M 5 0 L 105 0 L 105 136.001 L 0.9998 148.0017 Z +outline AAAA=-80,BBBB=90,CCCC=55 5 n3_top M 20 10 L 120 10 L 120 146.001 L 14.0002 164.0002 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=-80,BBBB=90,CCCC=55 6 n3_mid1 M 20 0 L 120 0 L 120 136.001 L 14.0002 154.0002 Z M 170 0 L 270 0 L 270 136.001 L 164.0002 154.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 7 n3_mid2 M 0 0 L 100 0 L 100 136.001 L -5.9998 154.0002 Z M 150 0 L 250 0 L 250 136.001 L 144.0002 154.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 8 nr_top M 12.0007 20 L 137.0007 20 L 148.251 167.2513 L 12.0007 156.001 Z M 150 20 L 250 20 L 250 156.001 L 139.0002 189.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 9 nr_mid M 12.0007 20 L 137.0007 20 L 148.251 167.2513 L 12.0007 156.001 Z M 150 20 L 250 20 L 250 156.001 L 139.0002 189.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 10 nr_low M 0 0 L 125 0 L 136.2503 147.2513 L -10.9998 169.0002 Z M 150 0 L 250 0 L 250 136.001 L 139.0002 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 11 nv_outer M 0 0 L 100 0 L 100 104.4995 L -10.9998 137.4988 Z +outline AAAA=-80,BBBB=90,CCCC=55 12 nv_mid M 0 0 L 101.2512 0 L 101.3529 106.6007 L -10.9998 139.4983 Z +outline AAAA=-80,BBBB=90,CCCC=55 13 s_self M 250 -13.7497 L 320 50 L 250 100 L 180.7504 50 Z M 0 -120 L 100 -120 L 100 16.001 L -10.9998 49.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 14 s_nest M 375 -6.8748 L 400 25 L 375 42.5 L 340.3752 25 Z M 50 150 L 150 150 L 150 250 L 39.0002 282.9993 Z +outline AAAA=-80,BBBB=90,CCCC=55 15 s_inner M 325 -6.8748 L 350 25 L 325 50 L 290.3752 25 Z M 0 150 L 100 150 L 100 286.001 L -10.9998 319.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 16 s_reset M 44.1942 26.5165 L 35.3553 106.066 L -35.3553 106.066 L -47.7297 22.981 Z +outline AAAA=-80,BBBB=90,CCCC=55 17 d_full M -27.0007 18.0005 L 72.9993 18.0005 L 72.9993 154.0015 L -38.0005 187.0007 Z +outline AAAA=-80,BBBB=90,CCCC=55 18 d_short M -27.0007 0 L 72.9993 0 L 72.9993 136.001 L -38.0005 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 19 d_cut_first M 4.5001 0 L 104.5001 0 L 104.5001 136.001 L -6.4996 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 20 d_cut_last M 0 0 L 100 0 L 100 136.001 L -10.9998 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 21 d_empty M 0 0 L 100 0 L 100 136.001 L -10.9998 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 22 d_long M 0 90.0024 L 100 90.0024 L 100 226.0034 L -10.9998 259.0027 Z +outline AAAA=-80,BBBB=90,CCCC=55 23 d_three_first M 4.5001 0 L 104.5001 0 L 104.5001 136.001 L -6.4996 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 24 d_three_mid M 3.2999 0 L 103.2999 0 L 103.2999 136.001 L -7.6998 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 25 d_three_last M 54.5001 36.001 L 154.5001 36.001 L 154.5001 172.002 L 43.5004 205.0012 Z +outline AAAA=-80,BBBB=90,CCCC=55 26 d_axes M 0 0 L 101.2512 0 L 101.3529 106.6007 L -10.9998 139.4983 Z +outline AAAA=-80,BBBB=90,CCCC=55 27 d_axes_short M 0 0 L 111.2518 0 L 111.2518 100 L -10.9998 132.9993 Z M 200 0 L 406.7493 0 L 300 308.999 Z +outline AAAA=-80,BBBB=90,CCCC=55 28 c_outer M 0 0 L 129.9988 0 L 143.4986 149.5008 L -10.9998 169.0002 Z M 150 -10.9998 L 404.0015 0 L 228.0005 372.002 Z M 600 0 L 729.9988 0 L 743.4986 149.5008 L 589.0002 169.0002 Z M 600 300 L 700 300 L 700 436.001 L 589.0002 469.0002 Z M 600 600 L 700 600 L 700 731.9995 L 589.0002 764.9988 Z +outline AAAA=-80,BBBB=90,CCCC=55 29 c_inner M 600 0 L 700 0 L 700 136.001 L 589.0002 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 30 c_reset_outer M 10 0 L 110 0 L 110 136.001 L -0.9998 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 31 c_reset_inner +outline AAAA=-80,BBBB=90,CCCC=55 32 m_outer M -27.0007 18.0005 L 72.9993 18.0005 L 72.9993 154.0015 L -37.0007 184.0015 Z M 272.9993 18.0005 L 372.9993 18.0005 L 372.9993 154.0015 L 262.9993 184.0015 Z M -27.0007 318.0005 L 72.9993 318.0005 L 72.9993 454.0015 L -37.0007 484.0015 Z M 287.9993 318.0005 L 387.9993 318.0005 L 387.9993 454.0015 L 287.9993 454.0015 Z +outline AAAA=-80,BBBB=90,CCCC=55 33 m_inner M -27.0007 18.0005 L 72.9993 18.0005 L 72.9993 154.0015 L -38.0005 187.0007 Z +outline AAAA=-80,BBBB=90,CCCC=55 34 y_a M 0 0 L 100 0 L 100 136.001 L -10.9998 169.0002 Z M 200 139.0002 L 454.0015 150 L 278.0005 522.002 Z M 300 0 L 389.978 0 L 394.4771 113.2732 L 291.2023 135.1672 Z +outline AAAA=-80,BBBB=90,CCCC=55 35 y_b M 0 139.0002 L 254.0015 150 L 78.0005 522.002 Z M 100 0 L 179.9805 0 L 179.9805 108.7742 L 91.2023 135.1672 Z M 259.9609 111.173 L 463.1125 119.9707 L 322.3461 417.4996 Z +outline AAAA=-80,BBBB=90,CCCC=55 36 z_a M 150 189.0002 L 404.0015 200 L 228.0005 572.002 Z M 252.5391 -170.8006 L 404.2782 -163.2812 L 305.8597 61.824 Z M 150 -300 L 218.3594 -300 L 218.3594 -221.627 L 142.4806 -199.069 Z M 0 0 L 100 0 L 100 114.6484 L -10.9998 147.6477 Z M 0 0 L 100 0 L 100 136.001 L -10.9998 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 37 z_b M 0 189.0002 L 254.0015 200 L 78.0005 572.002 Z M 102.5391 -170.8006 L 254.2782 -163.2812 L 155.8597 61.824 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L -7.5194 -199.069 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L -7.5194 -199.069 Z M -150 0 L -50 0 L -50 114.6484 L -160.9998 147.6477 Z +outline AAAA=-80,BBBB=90,CCCC=55 38 z_c M 102.5391 -170.8006 L 276.1729 -163.2812 L 155.8597 91.017 Z M 102.5391 -505.0781 L 149.2691 -505.0781 L 149.2691 -451.5029 L 97.3989 -436.0823 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L -7.5194 -199.069 Z M 0 -300 L 68.3594 -300 L 68.3594 -207.0306 L -7.5194 -184.4725 Z M -150 0 L -50 0 L -50 136.001 L -160.9998 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 39 x_user M 250 -10.9998 L 504.0015 0 L 328.0005 372.002 Z +outline AAAA=-80,BBBB=90,CCCC=55 40 k_const M 60 -40 L 160 -40 L 160 96.001 L 49.0002 129.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 41 k_cond M 0 -10.9998 L 254.0015 0 L 78.0005 372.002 Z M 300 0 L 400 0 L 400 136.001 L 289.0002 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 42 k_outer M 60 160 L 160 160 L 160 296.001 L 49.0002 329.0002 Z M 400 -10.9998 L 654.0015 0 L 478.0005 372.002 Z M 700 0 L 800 0 L 800 136.001 L 689.0002 169.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 43 x_norecord +outline AAAA=70,BBBB=-100,CCCC=20 3 n2_outer M 5 10 L 140.0006 10 L 140.0006 110 L 0.9998 122.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 4 n2_mid M 5 0 L 140.0006 0 L 140.0006 100 L 0.9998 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 5 n3_top M 20 10 L 155.0006 10 L 155.0006 110 L 14.0002 127.9993 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=70,BBBB=-100,CCCC=20 6 n3_mid1 M 20 0 L 155.0006 0 L 155.0006 100 L 14.0002 117.9993 Z M 170 0 L 270 0 L 270 100 L 164.0002 117.9993 Z +outline AAAA=70,BBBB=-100,CCCC=20 7 n3_mid2 M 0 0 L 135.0006 0 L 135.0006 100 L -5.9998 117.9993 Z M 150 0 L 285.0006 0 L 285.0006 100 L 144.0002 117.9993 Z +outline AAAA=70,BBBB=-100,CCCC=20 8 nr_top M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 285.0006 20 L 285.0006 120 L 145.9998 132.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 9 nr_mid M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 285.0006 20 L 285.0006 120 L 145.9998 132.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 10 nr_low M 0 0 L 125 0 L 125 100 L -4.0002 112.0007 Z M 150 0 L 285.0006 0 L 285.0006 100 L 145.9998 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 11 nv_outer M 0 0 L 106.25 0 L 106.4453 102.6953 L -4.0002 114.5007 Z +outline AAAA=70,BBBB=-100,CCCC=20 12 nv_mid M 0 0 L 100 0 L 100 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 13 s_self M 250 -5.0003 L 320 50 L 250 70 L 192.9996 50 Z M 0 -120 L 135.0006 -120 L 135.0006 -20 L -4.0002 -7.9993 Z +outline AAAA=70,BBBB=-100,CCCC=20 14 s_nest M 375 -2.5002 L 414.0002 25 L 375 42.5 L 346.4998 25 Z M 50 150 L 185.0006 150 L 185.0006 250 L 45.9998 262.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 15 s_inner M 325 -2.5002 L 364.0002 25 L 325 35 L 296.4998 25 Z M 0 150 L 135.0006 150 L 135.0006 250 L -4.0002 262.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 16 s_reset M 44.1942 26.5165 L 55.1547 125.8653 L -14.1421 84.8528 L -47.7297 22.981 Z +outline AAAA=70,BBBB=-100,CCCC=20 17 d_full M 28.0005 7.0001 L 163.0011 7.0001 L 163.0011 107.0001 L 24.0002 119.0009 Z +outline AAAA=70,BBBB=-100,CCCC=20 18 d_short M 28.0005 7.0001 L 163.0011 7.0001 L 163.0011 107.0001 L 24.0002 119.0009 Z +outline AAAA=70,BBBB=-100,CCCC=20 19 d_cut_first M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 20 d_cut_last M 28.0005 7.0001 L 163.0011 7.0001 L 163.0011 107.0001 L 24.0002 119.0009 Z +outline AAAA=70,BBBB=-100,CCCC=20 21 d_empty M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 22 d_long M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 23 d_three_first M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 24 d_three_mid M 22.2004 -14.0002 L 157.201 -14.0002 L 157.201 85.9998 L 18.2002 98.0005 Z +outline AAAA=70,BBBB=-100,CCCC=20 25 d_three_last M 17.0007 14.0002 L 152.0013 14.0002 L 152.0013 114.0002 L 13.0005 126.001 Z +outline AAAA=70,BBBB=-100,CCCC=20 26 d_axes M 0 0 L 100 0 L 100 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 27 d_axes_short M 0 0 L 100 0 L 100 100 L -4.0002 112.0007 Z M 200 0 L 410.4993 0 L 300 313.999 Z +outline AAAA=70,BBBB=-100,CCCC=20 28 c_outer M 0 0 L 129.9988 0 L 129.9988 100 L -4.0002 112.0007 Z M 150 -4.0002 L 350 0 L 241.9995 300 Z M 450 -4.0002 L 650 0 L 541.9995 300 Z M 600 0 L 729.9988 0 L 729.9988 100 L 595.9998 112.0007 Z M 450 295.9998 L 650 300 L 541.9995 600 Z M 0 600 L 135.0006 600 L 149.0006 745.9995 L -4.0002 744.0002 Z M 150 595.9998 L 397.9993 600 L 241.9995 963.999 Z M 450 595.9998 L 697.9993 600 L 541.9995 963.999 Z M 600 600 L 735.0006 600 L 749.0006 745.9995 L 595.9998 744.0002 Z +outline AAAA=70,BBBB=-100,CCCC=20 29 c_inner M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z M 150 -4.0002 L 350 0 L 241.9995 300 Z M 450 -4.0002 L 650 0 L 541.9995 300 Z M 600 0 L 735.0006 0 L 735.0006 100 L 595.9998 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 30 c_reset_outer M 10 0 L 145.0006 0 L 145.0006 100 L 5.9998 112.0007 Z M 200 -4.0002 L 400 0 L 291.9995 300 Z +outline AAAA=70,BBBB=-100,CCCC=20 31 c_reset_inner M 10 0 L 145.0006 0 L 145.0006 100 L 5.9998 112.0007 Z M 200 -4.0002 L 400 0 L 291.9995 300 Z +outline AAAA=70,BBBB=-100,CCCC=20 32 m_outer M 28.0005 7.0001 L 163.0011 7.0001 L 163.0011 107.0001 L 18.0005 137.0001 Z M 120 -10 L 320 0 L 200 300 Z M 328.0005 7.0001 L 463.0011 7.0001 L 463.0011 107.0001 L 318.0005 137.0001 Z M 420 -10 L 620 0 L 500 300 Z M 28.0005 307.0001 L 163.0011 307.0001 L 163.0011 407.0001 L 18.0005 437.0001 Z M 120 290 L 320 300 L 200 600 Z M 343.0005 307.0001 L 478.0011 307.0001 L 478.0011 407.0001 L 343.0005 407.0001 Z M 420 300 L 620 300 L 520 600 Z +outline AAAA=70,BBBB=-100,CCCC=20 33 m_inner M 28.0005 7.0001 L 163.0011 7.0001 L 163.0011 107.0001 L 24.0002 119.0009 Z M 120 -4.0002 L 320 0 L 211.9995 300 Z +outline AAAA=70,BBBB=-100,CCCC=20 34 y_a M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z M 200 145.9998 L 400 150 L 291.9995 450 Z M 300 0 L 389.978 0 L 389.978 79.9805 L 296.8006 89.5787 Z +outline AAAA=70,BBBB=-100,CCCC=20 35 y_b M 0 145.9998 L 200 150 L 91.9995 450 Z M 100 0 L 207.9741 0 L 207.9741 79.9805 L 96.8006 89.5787 Z M 259.9609 116.7713 L 419.9219 119.9707 L 333.5426 359.9121 Z +outline AAAA=70,BBBB=-100,CCCC=20 36 z_a M 150 195.9998 L 350 200 L 241.9995 500 Z M 252.5391 -166.0158 L 404.2782 -163.2812 L 315.4294 61.824 Z M 150 -300 L 242.2856 -300 L 246.6666 -217.246 L 147.2655 -213.4234 Z M 0 0 L 135.0006 0 L 141.4094 121.0572 L -4.0002 126.6492 Z M 0 0 L 135.0006 0 L 135.0006 100 L -4.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 37 z_b M 0 195.9998 L 200 200 L 91.9995 500 Z M 102.5391 -166.0158 L 254.2782 -163.2812 L 165.4294 61.824 Z M 0 -300 L 92.2856 -300 L 96.6666 -217.246 L -2.7345 -213.4234 Z M 0 -300 L 92.2856 -300 L 96.6666 -217.246 L -2.7345 -213.4234 Z M -150 0 L -14.9994 0 L -8.5906 121.0572 L -154.0002 126.6492 Z +outline AAAA=70,BBBB=-100,CCCC=20 38 z_c M 102.5391 -166.0158 L 239.2578 -163.2812 L 165.4294 41.7969 Z M 102.5391 -505.0781 L 165.6249 -505.0781 L 168.6197 -448.508 L 100.6697 -445.8949 Z M 0 -300 L 92.2856 -300 L 96.6666 -217.246 L -2.7345 -213.4234 Z M 0 -300 L 92.2856 -300 L 92.2856 -231.6406 L -2.7345 -223.437 Z M -150 0 L -14.9994 0 L -14.9994 100 L -154.0002 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 39 x_user M 250 -4.0002 L 450 0 L 341.9995 300 Z +outline AAAA=70,BBBB=-100,CCCC=20 40 k_const M 60 -40 L 195.0006 -40 L 195.0006 60 L 55.9998 72.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 41 k_cond M 0 -4.0002 L 200 0 L 91.9995 300 Z M 300 0 L 435.0006 0 L 435.0006 100 L 295.9998 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 42 k_outer M 60 160 L 195.0006 160 L 195.0006 260 L 55.9998 272.0007 Z M 400 -4.0002 L 600 0 L 491.9995 300 Z M 700 0 L 835.0006 0 L 835.0006 100 L 695.9998 112.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 43 x_norecord +outline AAAA=100,BBBB=100,CCCC=-100 3 n2_outer M 5 10 L 155 10 L 180 175 L 0.9998 162.0007 Z +outline AAAA=100,BBBB=100,CCCC=-100 4 n2_mid M 5 0 L 155 0 L 180 165 L 0.9998 152.0007 Z +outline AAAA=100,BBBB=100,CCCC=-100 5 n3_top M 20 10 L 170 10 L 195 175 L 14.0002 167.9993 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=100,BBBB=100,CCCC=-100 6 n3_mid1 M 20 0 L 170 0 L 195 165 L 14.0002 157.9993 Z M 170 0 L 270 0 L 270 140 L 164.0002 157.9993 Z +outline AAAA=100,BBBB=100,CCCC=-100 7 n3_mid2 M 0 0 L 150 0 L 175 165 L -5.9998 157.9993 Z M 150 0 L 300 0 L 325 165 L 144.0002 157.9993 Z +outline AAAA=100,BBBB=100,CCCC=-100 8 nr_top M 12.0007 20 L 137.0007 20 L 149.5007 172.5 L 12.0007 160 Z M 180 20 L 330 20 L 355 185 L 180 160 Z +outline AAAA=100,BBBB=100,CCCC=-100 9 nr_mid M 12.0007 20 L 137.0007 20 L 149.5007 172.5 L 12.0007 160 Z M 180 20 L 330 20 L 355 185 L 180 160 Z +outline AAAA=100,BBBB=100,CCCC=-100 10 nr_low M 30 0 L 155 0 L 167.5 152.5 L 30 140 Z M 180 0 L 330 0 L 355 165 L 180 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 11 nv_outer M 30 0 L 136.25 0 L 136.4453 102.6953 L 30 102.5 Z +outline AAAA=100,BBBB=100,CCCC=-100 12 nv_mid M 30 0 L 142.5 0 L 143.2812 105.7812 L 30 105 Z +outline AAAA=100,BBBB=100,CCCC=-100 13 s_self M 250 0 L 320 50 L 250 100 L 200 50 Z M 30 -120 L 180 -120 L 205 45 L 30 20 Z +outline AAAA=100,BBBB=100,CCCC=-100 14 s_nest M 375 0 L 420 25 L 375 42.5 L 350 25 Z M 80 150 L 230 150 L 230 250 L 80 250 Z +outline AAAA=100,BBBB=100,CCCC=-100 15 s_inner M 325 0 L 370 25 L 325 50 L 300 25 Z M 30 150 L 180 150 L 205 315 L 30 290 Z +outline AAAA=100,BBBB=100,CCCC=-100 16 s_reset M 44.1942 26.5165 L 63.6396 134.3503 L -35.3553 106.066 L -47.7297 22.981 Z +outline AAAA=100,BBBB=100,CCCC=-100 17 d_full M 40 30 L 190 30 L 215 195 L 40 170 Z +outline AAAA=100,BBBB=100,CCCC=-100 18 d_short M 40 10 L 190 10 L 215 175 L 40 150 Z +outline AAAA=100,BBBB=100,CCCC=-100 19 d_cut_first M 35 0 L 185 0 L 210 165 L 35 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 20 d_cut_last M 70 10 L 220 10 L 245 175 L 70 150 Z +outline AAAA=100,BBBB=100,CCCC=-100 21 d_empty M 30 0 L 180 0 L 205 165 L 30 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 22 d_long M 30 100 L 180 100 L 205 265 L 30 240 Z +outline AAAA=100,BBBB=100,CCCC=-100 23 d_three_first M 35 0 L 185 0 L 210 165 L 35 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 24 d_three_mid M 60 -20 L 210 -20 L 235 145 L 60 120 Z +outline AAAA=100,BBBB=100,CCCC=-100 25 d_three_last M 70 60 L 220 60 L 245 225 L 70 200 Z +outline AAAA=100,BBBB=100,CCCC=-100 26 d_axes M 30 0 L 142.5 0 L 143.2812 105.7812 L 30 105 Z +outline AAAA=100,BBBB=100,CCCC=-100 27 d_axes_short M 30 0 L 142.5 0 L 142.5 100 L 30 100 Z M 200 0 L 422.5 0 L 300 330 Z +outline AAAA=100,BBBB=100,CCCC=-100 28 c_outer M 30 0 L 159.9988 0 L 174.9982 154.9994 L 30 140 Z M 150 0 L 410 0 L 250 380 Z M 630 0 L 759.9988 0 L 774.9982 154.9994 L 630 140 Z M 630 300 L 730 300 L 730 440 L 630 440 Z M 30 600 L 180 600 L 199.9997 751.9992 L 30 731.9995 Z M 150 600 L 397.9993 600 L 250 963.999 Z M 450 600 L 697.9993 600 L 550 963.999 Z M 630 600 L 780 600 L 799.9997 751.9992 L 630 731.9995 Z +outline AAAA=100,BBBB=100,CCCC=-100 29 c_inner M 30 0 L 180 0 L 205 165 L 30 140 Z M 150 0 L 410 0 L 250 380 Z M 630 0 L 780 0 L 805 165 L 630 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 30 c_reset_outer M 40 0 L 190 0 L 215 165 L 40 140 Z M 200 0 L 460 0 L 300 380 Z +outline AAAA=100,BBBB=100,CCCC=-100 31 c_reset_inner M 200 0 L 460 0 L 300 380 Z +outline AAAA=100,BBBB=100,CCCC=-100 32 m_outer M 10 30 L 160 30 L 185 195 L 0 200 Z M 120 -10 L 380 0 L 200 380 Z M 310 30 L 460 30 L 485 195 L 300 200 Z M 420 -10 L 680 0 L 500 380 Z M 10 330 L 160 330 L 185 495 L 0 500 Z M 120 290 L 380 300 L 200 680 Z M 325 330 L 475 330 L 500 495 L 325 470 Z M 420 300 L 680 300 L 520 680 Z +outline AAAA=100,BBBB=100,CCCC=-100 33 m_inner M 40 30 L 190 30 L 215 195 L 40 170 Z M 120 0 L 380 0 L 220 380 Z +outline AAAA=100,BBBB=100,CCCC=-100 34 y_a M 30 0 L 180 0 L 205 165 L 30 140 Z M 200 150 L 460 150 L 300 530 Z M 323.9941 0 L 413.9722 0 L 418.9709 116.9714 L 323.9941 111.9727 Z +outline AAAA=100,BBBB=100,CCCC=-100 35 y_b M 0 150 L 260 150 L 100 530 Z M 123.9941 0 L 243.9648 0 L 263.96 131.9678 L 123.9941 111.9727 Z M 259.9609 119.9707 L 467.9102 119.9707 L 339.9414 423.8965 Z +outline AAAA=100,BBBB=100,CCCC=-100 36 z_a M 150 200 L 410 200 L 250 580 Z M 252.5391 -163.2812 L 404.2782 -163.2812 L 320.8984 61.824 Z M 170.5078 -300 L 273.0469 -300 L 279.3054 -215.3686 L 170.5078 -221.627 Z M 30 0 L 180 0 L 189.1553 123.8037 L 30 114.6484 Z M 30 0 L 180 0 L 205 165 L 30 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 37 z_b M 0 200 L 260 200 L 100 580 Z M 102.5391 -163.2812 L 254.2782 -163.2812 L 170.8984 61.824 Z M 20.5078 -300 L 123.0469 -300 L 129.3054 -215.3686 L 20.5078 -221.627 Z M 20.5078 -300 L 123.0469 -300 L 129.3054 -215.3686 L 20.5078 -221.627 Z M -120 0 L 30 0 L 39.1553 123.8037 L -120 114.6484 Z +outline AAAA=100,BBBB=100,CCCC=-100 38 z_c M 102.5391 -163.2812 L 280.2734 -163.2812 L 170.8984 96.4844 Z M 116.5581 -505.0781 L 186.6531 -505.0781 L 190.9314 -447.2246 L 116.5581 -451.5029 Z M 20.5078 -300 L 123.0469 -300 L 129.3054 -215.3686 L 20.5078 -221.627 Z M 20.5078 -300 L 123.0469 -300 L 140.1367 -187.207 L 20.5078 -204.2969 Z M -120 0 L 30 0 L 55 165 L -120 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 39 x_user M 250 0 L 510 0 L 350 380 Z +outline AAAA=100,BBBB=100,CCCC=-100 40 k_const M 90 -40 L 240 -40 L 265 125 L 90 100 Z +outline AAAA=100,BBBB=100,CCCC=-100 41 k_cond M 0 0 L 260 0 L 100 380 Z M 330 0 L 480 0 L 505 165 L 330 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 42 k_outer M 90 160 L 240 160 L 265 325 L 90 300 Z M 400 0 L 660 0 L 500 380 Z M 730 0 L 880 0 L 905 165 L 730 140 Z +outline AAAA=100,BBBB=100,CCCC=-100 43 x_norecord +outline AAAA=25,BBBB=0,CCCC=0 3 n2_outer M 5 10 L 117.5 10 L 117.5 110 L 0.9998 122.0007 Z +outline AAAA=25,BBBB=0,CCCC=0 4 n2_mid M 5 0 L 117.5 0 L 117.5 100 L 0.9998 112.0007 Z +outline AAAA=25,BBBB=0,CCCC=0 5 n3_top M 20 10 L 132.5 10 L 132.5 110 L 14.0002 127.9993 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=25,BBBB=0,CCCC=0 6 n3_mid1 M 20 0 L 132.5 0 L 132.5 100 L 14.0002 117.9993 Z M 170 0 L 270 0 L 270 100 L 164.0002 117.9993 Z +outline AAAA=25,BBBB=0,CCCC=0 7 n3_mid2 M 0 0 L 112.5 0 L 112.5 100 L -5.9998 117.9993 Z M 150 0 L 262.5 0 L 262.5 100 L 144.0002 117.9993 Z +outline AAAA=25,BBBB=0,CCCC=0 8 nr_top M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 262.5 20 L 262.5 120 L 150 120 Z +outline AAAA=25,BBBB=0,CCCC=0 9 nr_mid M 12.0007 20 L 137.0007 20 L 137.0007 120 L 12.0007 120 Z M 150 20 L 262.5 20 L 262.5 120 L 150 120 Z +outline AAAA=25,BBBB=0,CCCC=0 10 nr_low M 0 0 L 125 0 L 125 100 L 0 100 Z M 150 0 L 262.5 0 L 262.5 100 L 150 100 Z +outline AAAA=25,BBBB=0,CCCC=0 11 nv_outer M 0 0 L 106.25 0 L 106.4453 102.6953 L 0 102.5 Z +outline AAAA=25,BBBB=0,CCCC=0 12 nv_mid M 0 0 L 100 0 L 100 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 13 s_self M 250 0 L 320 50 L 250 100 L 200 50 Z M 0 -120 L 112.5 -120 L 112.5 -20 L 0 -20 Z +outline AAAA=25,BBBB=0,CCCC=0 14 s_nest M 375 0 L 405 25 L 375 42.5 L 350 25 Z M 50 150 L 162.5 150 L 162.5 250 L 50 250 Z +outline AAAA=25,BBBB=0,CCCC=0 15 s_inner M 325 0 L 355 25 L 325 50 L 300 25 Z M 0 150 L 112.5 150 L 112.5 250 L 0 250 Z +outline AAAA=25,BBBB=0,CCCC=0 16 s_reset M 44.1942 26.5165 L 42.4264 113.1371 L -35.3553 106.066 L -47.7297 22.981 Z +outline AAAA=25,BBBB=0,CCCC=0 17 d_full M 10 2.5 L 122.5 2.5 L 122.5 102.5 L 10 102.5 Z +outline AAAA=25,BBBB=0,CCCC=0 18 d_short M 10 2.5 L 122.5 2.5 L 122.5 102.5 L 10 102.5 Z +outline AAAA=25,BBBB=0,CCCC=0 19 d_cut_first M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 20 d_cut_last M 10 2.5 L 122.5 2.5 L 122.5 102.5 L 10 102.5 Z +outline AAAA=25,BBBB=0,CCCC=0 21 d_empty M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 22 d_long M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 23 d_three_first M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 24 d_three_mid M 7.5 -5 L 120 -5 L 120 95 L 7.5 95 Z +outline AAAA=25,BBBB=0,CCCC=0 25 d_three_last M 2.5 5 L 115 5 L 115 105 L 2.5 105 Z +outline AAAA=25,BBBB=0,CCCC=0 26 d_axes M 0 0 L 100 0 L 100 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 27 d_axes_short M 0 0 L 100 0 L 100 100 L 0 100 Z M 200 0 L 403.75 0 L 300 305 Z +outline AAAA=25,BBBB=0,CCCC=0 28 c_outer M 0 0 L 129.9988 0 L 129.9988 100 L 0 100 Z M 150 0 L 350 0 L 250 300 Z M 450 0 L 650 0 L 550 300 Z M 600 0 L 729.9988 0 L 729.9988 100 L 600 100 Z M 450 300 L 650 300 L 550 600 Z M 0 600 L 112.5 600 L 117.4999 736.9995 L 0 731.9995 Z M 150 600 L 397.9993 600 L 250 963.999 Z M 600 600 L 712.5 600 L 717.4999 736.9995 L 600 731.9995 Z +outline AAAA=25,BBBB=0,CCCC=0 29 c_inner M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z M 150 0 L 350 0 L 250 300 Z M 450 0 L 650 0 L 550 300 Z M 600 0 L 712.5 0 L 712.5 100 L 600 100 Z +outline AAAA=25,BBBB=0,CCCC=0 30 c_reset_outer M 10 0 L 122.5 0 L 122.5 100 L 10 100 Z M 200 0 L 400 0 L 300 300 Z +outline AAAA=25,BBBB=0,CCCC=0 31 c_reset_inner M 200 0 L 400 0 L 300 300 Z +outline AAAA=25,BBBB=0,CCCC=0 32 m_outer M 10 2.5 L 122.5 2.5 L 122.5 102.5 L 0 132.5 Z M 120 -10 L 320 0 L 200 300 Z M 310 2.5 L 422.5 2.5 L 422.5 102.5 L 300 132.5 Z M 420 -10 L 620 0 L 500 300 Z M 10 302.5 L 122.5 302.5 L 122.5 402.5 L 0 432.5 Z M 120 290 L 320 300 L 200 600 Z M 325 302.5 L 437.5 302.5 L 437.5 402.5 L 325 402.5 Z M 420 300 L 620 300 L 520 600 Z +outline AAAA=25,BBBB=0,CCCC=0 33 m_inner M 10 2.5 L 122.5 2.5 L 122.5 102.5 L 10 102.5 Z M 120 0 L 320 0 L 220 300 Z +outline AAAA=25,BBBB=0,CCCC=0 34 y_a M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z M 200 150 L 400 150 L 300 450 Z M 300 0 L 389.978 0 L 389.978 79.9805 L 300 79.9805 Z +outline AAAA=25,BBBB=0,CCCC=0 35 y_b M 0 150 L 200 150 L 100 450 Z M 100 0 L 189.978 0 L 189.978 79.9805 L 100 79.9805 Z M 259.9609 119.9707 L 419.9219 119.9707 L 339.9414 359.9121 Z +outline AAAA=25,BBBB=0,CCCC=0 36 z_a M 150 200 L 350 200 L 250 500 Z M 252.5391 -163.2812 L 404.2782 -163.2812 L 320.8984 61.824 Z M 150 -300 L 226.9043 -300 L 228.4689 -220.0624 L 150 -221.627 Z M 0 0 L 112.5 0 L 114.7888 116.9373 L 0 114.6484 Z M 0 0 L 112.5 0 L 112.5 100 L 0 100 Z +outline AAAA=25,BBBB=0,CCCC=0 37 z_b M 0 200 L 200 200 L 100 500 Z M 102.5391 -163.2812 L 254.2782 -163.2812 L 170.8984 61.824 Z M 0 -300 L 76.9043 -300 L 78.4689 -220.0624 L 0 -221.627 Z M 0 -300 L 76.9043 -300 L 78.4689 -220.0624 L 0 -221.627 Z M -150 0 L -37.5 0 L -35.2112 116.9373 L -150 114.6484 Z +outline AAAA=25,BBBB=0,CCCC=0 38 z_c M 102.5391 -163.2812 L 239.2578 -163.2812 L 170.8984 41.7969 Z M 102.5391 -505.0781 L 155.1104 -505.0781 L 156.1799 -450.4333 L 102.5391 -451.5029 Z M 0 -300 L 76.9043 -300 L 78.4689 -220.0624 L 0 -221.627 Z M 0 -300 L 76.9043 -300 L 76.9043 -231.6406 L 0 -231.6406 Z M -150 0 L -37.5 0 L -37.5 100 L -150 100 Z +outline AAAA=25,BBBB=0,CCCC=0 39 x_user M 250 0 L 450 0 L 350 300 Z +outline AAAA=25,BBBB=0,CCCC=0 40 k_const M 60 -40 L 172.5 -40 L 172.5 60 L 60 60 Z +outline AAAA=25,BBBB=0,CCCC=0 41 k_cond M 0 0 L 200 0 L 100 300 Z M 300 0 L 412.5 0 L 412.5 100 L 300 100 Z +outline AAAA=25,BBBB=0,CCCC=0 42 k_outer M 60 160 L 172.5 160 L 172.5 260 L 60 260 Z M 400 0 L 600 0 L 500 300 Z M 700 0 L 812.5 0 L 812.5 100 L 700 100 Z +outline AAAA=25,BBBB=0,CCCC=0 43 x_norecord +outline AAAA=-50,BBBB=10,CCCC=60 3 n2_outer M 5 10 L 105 10 L 105 114.0015 L 0.9998 126.0022 Z +outline AAAA=-50,BBBB=10,CCCC=60 4 n2_mid M 5 0 L 105 0 L 105 104.0015 L 0.9998 116.0022 Z +outline AAAA=-50,BBBB=10,CCCC=60 5 n3_top M 20 10 L 120 10 L 120 114.0015 L 14.0002 132.0007 Z M 170 10 L 270 10 L 270 138.0005 L 164.0002 155.9998 Z +outline AAAA=-50,BBBB=10,CCCC=60 6 n3_mid1 M 20 0 L 120 0 L 120 104.0015 L 14.0002 122.0007 Z M 170 0 L 270 0 L 270 104.0015 L 164.0002 122.0007 Z +outline AAAA=-50,BBBB=10,CCCC=60 7 n3_mid2 M 0 0 L 100 0 L 100 104.0015 L -5.9998 122.0007 Z M 150 0 L 250 0 L 250 104.0015 L 144.0002 122.0007 Z +outline AAAA=-50,BBBB=10,CCCC=60 8 nr_top M 12.0007 20 L 137.0007 20 L 138.2512 125.2519 L 12.0007 124.0015 Z M 150 20 L 250 20 L 250 124.0015 L 137.9993 160.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 9 nr_mid M 12.0007 20 L 137.0007 20 L 138.2512 125.2519 L 12.0007 124.0015 Z M 150 20 L 250 20 L 250 124.0015 L 137.9993 160.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 10 nr_low M 0 0 L 125 0 L 126.2505 105.2519 L -12.0007 140.0037 Z M 150 0 L 250 0 L 250 104.0015 L 137.9993 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 11 nv_outer M 0 0 L 100 0 L 100 103.75 L -12.0007 139.7522 Z +outline AAAA=-50,BBBB=10,CCCC=60 12 nv_mid M 0 0 L 100 0 L 100 101.7505 L -12.0007 137.7527 Z +outline AAAA=-50,BBBB=10,CCCC=60 13 s_self M 250 -15.0009 L 320 50 L 250 100 L 178.9987 50 Z M 0 -120 L 100 -120 L 100 -15.9985 L -12.0007 20.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 14 s_nest M 375 -7.5005 L 400 25 L 375 42.5 L 339.4994 25 Z M 50 150 L 150 150 L 150 250 L 37.9993 286.0022 Z +outline AAAA=-50,BBBB=10,CCCC=60 15 s_inner M 325 -7.5005 L 350 25 L 325 50 L 289.4994 25 Z M 0 150 L 100 150 L 100 254.0015 L -12.0007 290.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 16 s_reset M 44.1942 26.5165 L 35.3553 106.066 L -35.3553 106.066 L -47.7297 22.981 Z +outline AAAA=-50,BBBB=10,CCCC=60 17 d_full M -3.0011 2.0007 L 96.9989 2.0007 L 96.9989 106.0022 L -15.0018 142.0044 Z +outline AAAA=-50,BBBB=10,CCCC=60 18 d_short M -3.0011 0 L 96.9989 0 L 96.9989 104.0015 L -15.0018 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 19 d_cut_first M 0.5002 0 L 100.5002 0 L 100.5002 104.0015 L -11.5005 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 20 d_cut_last M 0 0 L 100 0 L 100 104.0015 L -12.0007 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 21 d_empty M 0 0 L 100 0 L 100 104.0015 L -12.0007 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 22 d_long M 0 10.0037 L 100 10.0037 L 100 114.0051 L -12.0007 150.0073 Z +outline AAAA=-50,BBBB=10,CCCC=60 23 d_three_first M 0.5002 0 L 100.5002 0 L 100.5002 104.0015 L -11.5005 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 24 d_three_mid M 3.6002 0 L 103.6002 0 L 103.6002 104.0015 L -8.4005 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 25 d_three_last M 33.0029 4.0015 L 133.0029 4.0015 L 133.0029 108.0029 L 21.0022 144.0051 Z +outline AAAA=-50,BBBB=10,CCCC=60 26 d_axes M 0 0 L 100 0 L 100 101.7505 L -12.0007 137.7527 Z +outline AAAA=-50,BBBB=10,CCCC=60 27 d_axes_short M 0 0 L 101.2512 0 L 101.2512 100 L -12.0007 136.0022 Z M 200 0 L 400.7507 0 L 300 301.001 Z +outline AAAA=-50,BBBB=10,CCCC=60 28 c_outer M 0 0 L 129.9988 0 L 131.4993 105.502 L -12.0007 140.0037 Z M 150 -12.0007 L 356.0022 0 L 225.9985 308.0029 Z M 450 -12.0007 L 656.0022 0 L 525.9985 308.0029 Z M 600 0 L 729.9988 0 L 731.4993 105.502 L 587.9993 140.0037 Z M 450 287.9993 L 656.0022 300 L 525.9985 608.0029 Z M 600 600 L 700 600 L 700 731.9995 L 587.9993 768.0017 Z +outline AAAA=-50,BBBB=10,CCCC=60 29 c_inner M 450 -12.0007 L 656.0022 0 L 525.9985 308.0029 Z +outline AAAA=-50,BBBB=10,CCCC=60 30 c_reset_outer M 10 0 L 110 0 L 110 104.0015 L -2.0007 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 31 c_reset_inner +outline AAAA=-50,BBBB=10,CCCC=60 32 m_outer M -3.0011 2.0007 L 96.9989 2.0007 L 96.9989 106.0022 L -13.0011 136.0022 Z M 296.9989 2.0007 L 396.9989 2.0007 L 396.9989 106.0022 L 286.9989 136.0022 Z M -3.0011 302.0007 L 96.9989 302.0007 L 96.9989 406.0022 L -13.0011 436.0022 Z M 311.9989 302.0007 L 411.9989 302.0007 L 411.9989 406.0022 L 311.9989 406.0022 Z +outline AAAA=-50,BBBB=10,CCCC=60 33 m_inner M -3.0011 2.0007 L 96.9989 2.0007 L 96.9989 106.0022 L -15.0018 142.0044 Z +outline AAAA=-50,BBBB=10,CCCC=60 34 y_a M 0 0 L 100 0 L 100 104.0015 L -12.0007 140.0037 Z M 200 137.9993 L 406.0022 150 L 275.9985 458.0029 Z M 300 0 L 389.978 0 L 390.4781 83.6809 L 290.4018 111.9756 Z +outline AAAA=-50,BBBB=10,CCCC=60 35 y_b M 0 137.9993 L 206.0022 150 L 75.9985 458.0029 Z M 100 0 L 179.9805 0 L 179.9805 83.1809 L 90.4018 111.9756 Z M 259.9609 110.3725 L 424.7225 119.9707 L 320.7449 366.3129 Z +outline AAAA=-50,BBBB=10,CCCC=60 36 z_a M 150 187.9993 L 356.0022 200 L 225.9985 508.0029 Z M 252.5391 -171.4849 L 404.2782 -163.2812 L 304.4912 61.824 Z M 150 -300 L 218.3594 -300 L 218.3594 -221.627 L 141.7964 -197.0162 Z M 0 0 L 100 0 L 100 114.6484 L -12.0007 150.6506 Z M 0 0 L 100 0 L 100 104.0015 L -12.0007 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 37 z_b M 0 187.9993 L 206.0022 200 L 75.9985 508.0029 Z M 102.5391 -171.4849 L 254.2782 -163.2812 L 154.4912 61.824 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L -8.2036 -197.0162 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L -8.2036 -197.0162 Z M -150 0 L -50 0 L -50 114.6484 L -162.0007 150.6506 Z +outline AAAA=-50,BBBB=10,CCCC=60 38 z_c M 102.5391 -171.4849 L 243.3609 -163.2812 L 154.4912 47.2676 Z M 102.5391 -505.0781 L 149.2691 -505.0781 L 149.2691 -451.5029 L 96.9311 -434.679 Z M 0 -300 L 68.3594 -300 L 68.3594 -221.627 L -8.2036 -197.0162 Z M 0 -300 L 68.3594 -300 L 68.3594 -228.9052 L -8.2036 -204.2944 Z M -150 0 L -50 0 L -50 104.0015 L -162.0007 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 39 x_user M 250 -12.0007 L 456.0022 0 L 325.9985 308.0029 Z +outline AAAA=-50,BBBB=10,CCCC=60 40 k_const M 60 -40 L 160 -40 L 160 64.0015 L 47.9993 100.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 41 k_cond M 0 -12.0007 L 206.0022 0 L 75.9985 308.0029 Z M 300 0 L 400 0 L 400 104.0015 L 287.9993 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 42 k_outer M 60 160 L 160 160 L 160 264.0015 L 47.9993 300.0037 Z M 400 -12.0007 L 606.0022 0 L 475.9985 308.0029 Z M 700 0 L 800 0 L 800 104.0015 L 687.9993 140.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 43 x_norecord diff --git a/tests/fixtures/varc_face.ttf b/tests/fixtures/varc_face.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d1f5344bc308253bffcdabacaae2e4ec87c0261f GIT binary patch literal 3080 zcmd5;Z){Ul6#t#~USGFvm9h=SpE1fdp+KY1f$#ye-69&rXaJEQ@mbn#L%OzUI~3iNq=_*@jm{VnV?6C3&$eR<&{z-|Dn&eU+SfM$FE7;V(A z7}%98e)8RCzXE{>`SpX@-2=wp>no{$jr?1K)L)o?;$`Z~jQQ+PCYcWUo@}P>B;lqZ z3d~>3F6uO1&(Lsr)XoVFLgU*exwqxTqXWE+IVVXZtg0G*^^e$mcN|dY#!xg(eX1K~R zG#fdDNjFkW=^}9@QCKxi-Sd)-X>6gVj~4&jG|#Da3mcgPc20?|B*I>1SBn2dUgu*H z+W6J%+BlCeu~nP#&T5rhqFSv^{B3QTF!QRt8{Jrjwb#_|z~%iuj^nLQ8fndGdGE^Z zbUIy^>fSNi<-O))TDS2s+ul`+{4uZncN!*)03NMnB0Mqmu?_gixf$|fp|wT}>Gm3J zke;inZZ%Re3-fEVf$Ol!qv_j#TWhqCzNa(joJtQ|3!p7D$(LOOISH3uTc+WU(xf zr4lu_=SR(QaYsWg=Hy4pnW9Z%ct_gK^*ZHz!B0ePc@4+xTv5e}T%)3s%|G0b+g117 z<%&yADU%(rOHPifAxR@zr%68fr7|CY+X{R)lFP59qPHLp=4D2YD$^nng zCh3)SGQ)-PZdzyaxxrxCDGwDh8PASB9>*M->$G~&IysstQKrZnz@PGxBX;u$ro22n z=@jSXI9#(G_H51FPLi$d~* z(8<~jV*$E&`g?Fa7Ge=1jJ5k#!g)?gIK{fBDdKCI zPD4{V&m~}6fws;_>{3+fP19x^ZS3@Ka><=;2d9GDFM6n)f-qRwMEo+XuN%3WM zXQi(tUnuA+7AHJF7@z~1kCHWbx9Su;?2TbiVo}%#E;wU{)sM^PQ?CiIQ8>ZDT`%ud z@-b1LKM-`42K6EuHljHPxMv;Qla*ZA+prN^xfn$}f+uho&*N3Rfw%Diw z7kO4!$<4A}3i6mdCC|w#@}|5eAIVYqMteu??%9gpBX@#CKeC76Qs6TNc+L71mG`73oDKSOok!VCt5H~ zG-_;bh*Z=7r5Nu=$e@kIg{2c4u}cu^)RotbR;-(&SZh%65ydsC?sLT52Nr%ki785E z!mwy&W71%$#}^KCDf;#WiIR|bZ?CbXQehUALo5M|6Y*AzDrd@v%2~!11M_tfmBX4v zfQUIV+yb3Li-$Cv%S$w-#3@KrqDJ|KfVA+8vFdbn42*M8x{x+|L)_tUYE{U< z3nkuO*c(RApI;g8vqY;8g?JQWA!`3%$dBEaVA_UFtwh5-u9G1w=6?)>5Y*D-Stq4W zO7$hrLl2S~^s!wb6Mb^7km)w---X3*4*KT>9B{uqh+Vbo)1%MSwe(%MydTuaadxjw zxG2tV#m8HT8%i+YuekCFeOU>Wx<^G~vHb@R?v+Sn%H", len(regions)) + bodies = b"" + for r in regions: + rl += struct.pack(">L", 2 + 4 * len(regions) + len(bodies)) + bodies += struct.pack(">H", len(r)) + b"".join( + struct.pack(">H", a) + p.f2(s) + p.f2(pk) + p.f2(e) for a, s, pk, e in r) + rl += bodies + head_len = 2 + 4 + 2 + 4 * len(subtables) + subs = [] + for indices, sets in subtables: + sub = struct.pack(">BH", 1, len(indices)) + b"".join(struct.pack(">H", i) for i in indices) + sub += p.cff2_index(sets) + subs.append(sub) + out = struct.pack(">HLH", 1, head_len, len(subtables)) + off = head_len + len(rl) + for s in subs: + out += struct.pack(">L", off); off += len(s) + return out + rl + b"".join(subs) + + +def tv(values): + return p.tuple_values(values) + + +# Region indices. +RA, RB, RC, RAN, RK = 0, 1, 2, 3, 4 +REGIONS = [ + [(0, 0.0, 1.0, 1.0)], # RA: AAAA up + [(1, 0.0, 1.0, 1.0)], # RB: BBBB up + [(2, 0.0, 1.0, 1.0)], # RC: CCCC up + [(0, -1.0, -1.0, 0.0)], # RAN: AAAA down + [], # RK: no axis, so everywhere +] + +# Variation indices, outer << 16 | inner. +def vi(outer, inner): + return (outer << 16) | inner + +SUBTABLES = [ + # outer 0: regions RA, RB; sets of two values per region. + ([RA, RB], [ + tv([40, 10, -30, 20]), # 0: full + bytes([0x02, 40, 10, (-30) & 0xFF]), # 1: region RB's tuple has one value + bytes([0x41, 0x00, 0x05]), # 2: run of two words cut in RA's tuple + bytes([0x01, 40, 10, 0x41, 0x00, 0x07]), # 3: run cut in RB's (the last) tuple + b"", # 4: empty set + bytes([0xC1, 0, 0, 0, 100, 0, 0]), # 5: i32 run cut: RB reads 0x00 0x00, 0x00 0x64 + bytes([0x42, 0x10, 0x00, 0xF0, 0x00, 0x08, 0x00]), # 6: 4096 -4096 2048: axis values + ]), + # outer 1: regions RA, RB, RC; sets of two values per region. + ([RA, RB, RC], [ + bytes([0x41, 0x00, 0x05, 0x03]), # 0: cut in RA's tuple; RB reads 5, RC nothing + bytes([0x01, 30, -20 & 0xFF, 0x41, 0x00, 0x06]), # 1: cut in RB's; RC reads 6 + tv([10, 20, 30, 40, 50]), # 2: RC's tuple has one value + ]), + # outer 2: one value per region (conditions), regions RA, RB. + ([RA, RB], [ + bytes([0x00, 5]), # 0: RA's value only + b"", # 1: nothing + tv([2, -3]), # 2: full + ]), + # outer 3: axis values, regions RB, RAN. + ([RB, RAN], [ + tv([4096, 2048, -4096, 1024]), # 0: full, two values per region + bytes([0x40, 0x10, 0x00]), # 1: one word: RB's first value + ]), + # outer 4: a region that holds everywhere, the default included. + ([RK], [ + tv([60, -40]), # 0: two translations + tv([2]), # 1: one value (a condition) + ]), +] + +CONDS = [ + p.cond_axis(0, 0.2, 1.0), # 0: AAAA in [0.2, 1] + p.cond_value(-1, vi(2, 0)), # 1: -1 + 5*sA > 0 over a short set + p.cond_value(-1, vi(2, 1)), # 2: -1 + nothing + p.cond_value(1, vi(2, 2)), # 3: 1 + 2*sA - 3*sB > 0 + p.cond_list_op(4, [p.cond_axis(1, 0.5, 1.0), p.cond_not(p.cond_axis(0, -1.0, 0.0))]), # 4 + p.cond_axis(1, -1.0, -0.25), # 5: BBBB in [-1, -0.25] + p.cond_value(-1, vi(4, 1)), # 6: -1 + 2 everywhere +] + +# Axis indices lists. +AX_A, AX_B, AX_C, AX_AB, AX_BC = 0, 1, 2, 3, 4 +AXIS_LISTS = [[0], [1], [2], [0, 1], [1, 2]] + +C = p.component + +VARC_NAMES = [ + # nested reset + "n2_outer", "n2_mid", + "n3_top", "n3_mid1", "n3_mid2", + "nr_top", "nr_mid", "nr_low", + "nv_outer", "nv_mid", + # self reference (these have glyf outlines of their own) + "s_self", "s_nest", "s_inner", "s_reset", + # short delta sets + "d_full", "d_short", "d_cut_first", "d_cut_last", "d_empty", "d_long", + "d_three_first", "d_three_mid", "d_three_last", "d_axes", "d_axes_short", + # conditions inside nested composites + "c_outer", "c_inner", "c_reset_outer", "c_reset_inner", + # one glyph reached many times + "m_outer", "m_inner", + # cycles longer than a self reference + "y_a", "y_b", "z_a", "z_b", "z_c", + # a component VARC covers without a glyph record + "x_user", + # a region without axes, which holds at the default instance too + "k_const", "k_cond", "k_outer", +] +# Covered by the VARC coverage, past the glyph records: last in the +# glyph order, so last in the coverage. +NO_RECORD = "x_norecord" +SELF_OUTLINED = {"s_self", "s_inner", "s_reset", "y_b", "x_norecord"} + + +def make(g): + box, tri = g["box"], g["tri"] + rec = {} + + # A RESET_UNSPECIFIED_AXES component two composites down: box takes + # the font's AAAA and BBBB, not n2_mid's BBBB. + rec["n2_outer"] = C(g["n2_mid"], axes=(AX_B, [11469]), fields=dict(TY=10)) + rec["n2_mid"] = C(box, reset=True, axes=(AX_C, [3277]), fields=dict(TX=5)) + + # Three composites: the reset sits under n3_mid1 (BBBB 0.7) and + # n3_mid2 (AAAA -0.5), and a sibling without reset inherits both. + rec["n3_top"] = C(g["n3_mid1"], axes=(AX_B, [11469]), fields=dict(TY=10)) + rec["n3_mid1"] = C(g["n3_mid2"], axes=(AX_A, [-8192]), fields=dict(TX=20)) + rec["n3_mid2"] = (C(box, reset=True, axes=(AX_C, [4915])) + + C(box, axes=(AX_C, [4915]), fields=dict(TX=150))) + + # A reset in the middle: nr_mid starts from the font's coords, and + # nr_low, below it, inherits nr_mid's coords with its own AAAA. + rec["nr_top"] = C(g["nr_mid"], axes=(AX_AB, [6000, -9000])) + rec["nr_mid"] = C(g["nr_low"], reset=True, axes=(AX_C, [-6554]), fields=dict(TY=20)) + rec["nr_low"] = (C(box, axes=(AX_A, [8192])) + + C(box, reset=True, fields=dict(TX=150))) + + # A reset component whose axis values vary: the deltas are read at + # nv_mid's coords (BBBB 0.5 from nv_outer), then the reset coords + # take them. + rec["nv_outer"] = C(g["nv_mid"], axes=(AX_B, [8192])) + rec["nv_mid"] = C(box, reset=True, axes=(AX_AB, [0, 0]), axis_var=vi(3, 0)) + + # Self reference: HarfBuzz draws the glyph's glyf outline. + rec["s_self"] = (C(g["s_self"], axes=(AX_A, [8192]), fields=dict(TX=200)) + + C(box, fields=dict(TY=-120))) + rec["s_nest"] = C(g["s_inner"], axes=(AX_B, [-8192]), fields=dict(TX=50)) + rec["s_inner"] = (C(g["s_inner"], fields=dict(SX=512, TX=300)) + + C(box, fields=dict(TY=150))) + rec["s_reset"] = C(g["s_reset"], reset=True, axes=(AX_C, [8192]), fields=dict(ROT=1024)) + + # Short delta sets, on two translations. + for name, inner in [("d_full", 0), ("d_short", 1), ("d_cut_first", 2), + ("d_cut_last", 3), ("d_empty", 4), ("d_long", 5)]: + rec[name] = C(box, transform_var=vi(0, inner), fields=dict(TX=0, TY=0)) + for name, inner in [("d_three_first", 0), ("d_three_mid", 1), ("d_three_last", 2)]: + rec[name] = C(box, transform_var=vi(1, inner), fields=dict(TX=0, TY=0)) + rec["d_axes"] = C(box, axes=(AX_AB, [0, 0]), axis_var=vi(3, 0)) + rec["d_axes_short"] = (C(box, axes=(AX_AB, [0, 0]), axis_var=vi(3, 1)) + + C(tri, axes=(AX_BC, [0, 0]), axis_var=vi(0, 6), fields=dict(TX=200))) + + # Conditions inside nested composites, read at c_inner's coords. + rec["c_outer"] = (C(g["c_inner"], axes=(AX_A, [9830])) + + C(g["c_inner"], axes=(AX_A, [-9830]), fields=dict(TY=300)) + + C(g["c_inner"], axes=(AX_B, [13107]), fields=dict(TY=600))) + rec["c_inner"] = (C(box, cond=0) + + C(tri, cond=1, fields=dict(TX=150)) + + C(box, cond=2, fields=dict(TX=300)) + + C(tri, cond=3, fields=dict(TX=450)) + + C(box, cond=4, fields=dict(TX=600))) + # A condition gates a reset component: it is read at the coords of + # the glyph it belongs to, before the reset. + rec["c_reset_outer"] = C(g["c_reset_inner"], axes=(AX_B, [-8192])) + rec["c_reset_inner"] = (C(box, cond=5, reset=True, fields=dict(TX=10)) + + C(tri, cond=0, reset=True, fields=dict(TX=200))) + + # The same glyph at the same coords three times, then elsewhere. + rec["m_outer"] = (C(g["m_inner"], axes=(AX_C, [8192])) + + C(g["m_inner"], axes=(AX_C, [8192]), fields=dict(TX=300)) + + C(g["m_inner"], axes=(AX_C, [8192]), fields=dict(TY=300)) + + C(g["m_inner"], axes=(AX_C, [-8192]), fields=dict(TX=300, TY=300))) + rec["m_inner"] = (C(box, transform_var=vi(0, 0), fields=dict(TX=0, TY=0)) + + C(tri, cond=0, fields=dict(TX=120))) + + # Cycles: HarfBuzz's decycler stops a glyph met again on the way + # down, at a depth that depends on the cycle's length. + rec["y_a"] = C(box) + C(g["y_b"], axes=(AX_A, [4096]), fields=dict(TX=200)) + rec["y_b"] = C(tri, fields=dict(TY=150)) + C(g["y_a"], fields=dict(TX=100, SX=819)) + rec["z_a"] = C(g["z_b"], fields=dict(TX=150)) + C(box) + rec["z_b"] = C(tri, fields=dict(TY=200)) + C(g["z_c"], axes=(AX_B, [6000])) + rec["z_c"] = C(g["z_a"], fields=dict(TY=-300, SX=700)) + C(box, fields=dict(TX=-150)) + + # A region that constrains no axis holds everywhere: HarfBuzz's font + # has a zero per axis at the default instance, so it applies there. + rec["k_const"] = C(box, transform_var=vi(4, 0), fields=dict(TX=0, TY=0)) + rec["k_cond"] = C(tri, cond=6) + C(box, fields=dict(TX=300)) + rec["k_outer"] = C(g["k_const"], fields=dict(TY=200)) + C(g["k_cond"], fields=dict(TX=400)) + + # A component VARC covers but has no record for. + rec["x_user"] = C(g[NO_RECORD], fields=dict(TX=50)) + C(tri, fields=dict(TX=250)) + + store = mvs_raw(REGIONS, SUBTABLES) + gids = sorted((g[n], rec[n]) for n in VARC_NAMES) + assert g[NO_RECORD] > gids[-1][0] + return p.varc_table([x for x, _ in gids] + [g[NO_RECORD]], [r for _, r in gids], store, + p.condition_list(CONDS), AXIS_LISTS) + + +def build(path): + order = [".notdef", "box", "tri"] + VARC_NAMES + [NO_RECORD] + fb = FontBuilder(1000, isTTF=True) + fb.setupGlyphOrder(order) + fb.setupCharacterMap({0x41 + i: n for i, n in enumerate(order[1:])}) + glyphs = {".notdef": p.empty(), "box": p.rect(0, 0, 100, 100), "tri": p.tri()} + for n in VARC_NAMES + [NO_RECORD]: + glyphs[n] = diamond() if n in SELF_OUTLINED else p.empty() + fb.setupGlyf(glyphs) + glyf = fb.font["glyf"] + fb.setupHorizontalMetrics({n: (600, getattr(glyf[n], "xMin", 0)) for n in order}) + fb.setupHorizontalHeader(ascent=800, descent=-200) + fb.setupNameTable({"familyName": "VarcFaceTest", "styleName": "Regular"}) + fb.setupOS2() + fb.setupPost() + fb.setupFvar(axes=[(t, -100, 0, 100, t) for t in p.AXES], instances=[]) + z = (0, 0) + variations = { + "box": [ + TupleVariation({"AAAA": (0, 1, 1)}, [z, (50, 0), (50, 0), z, z, z, z, z]), + TupleVariation({"BBBB": (0, 1, 1)}, [z, z, (0, 40), (0, 40), z, z, z, z]), + TupleVariation({"CCCC": (-1, -1, 0)}, [(30, 0), (30, 0), (30, 0), (30, 0), z, z, z, z]), + TupleVariation({"CCCC": (0, 1, 1)}, [z, z, z, (-20, 60), z, z, z, z]), + TupleVariation({"AAAA": (0, 1, 1), "BBBB": (0, 1, 1)}, [z, z, (25, 25), z, z, z, z, z]), + ], + "tri": [ + TupleVariation({"BBBB": (0, 1, 1)}, [z, (60, 0), (0, 80), z, z, z, z]), + TupleVariation({"CCCC": (0, 1, 1)}, [(0, -20), z, (-40, 0), z, z, z, z]), + ], + } + # The glyphs that name themselves vary too, so the coords their own + # outline is drawn at show. + for n in SELF_OUTLINED: + variations[n] = [ + TupleVariation({"AAAA": (0, 1, 1)}, [z, (40, 0), z, z, z, z, z, z]), + TupleVariation({"BBBB": (-1, -1, 0)}, [z, z, (0, -30), z, z, z, z, z]), + TupleVariation({"CCCC": (0, 1, 1)}, [(0, -25), z, z, (-35, 0), z, z, z, z]), + ] + fb.setupGvar(variations) + gid = {n: i for i, n in enumerate(order)} + t = DefaultTable("VARC") + t.data = make(gid) + fb.font["VARC"] = t + p.fixed_timestamps(fb) + fb.save(path) + # fontTools' VARC decoder rejects the short delta sets, which is + # the point of them, so the table is not read back here; + # varc_face_expected.py checks that HarfBuzz draws every glyph. + + +if __name__ == "__main__": + build(FIXTURES / "varc_face.ttf") diff --git a/tests/tools/varc_face_expected.py b/tests/tools/varc_face_expected.py new file mode 100644 index 00000000..32573933 --- /dev/null +++ b/tests/tools/varc_face_expected.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Record HarfBuzz's outlines for tests/fixtures/varc_face.ttf. + +Writes tests/fixtures/varc_face.expected: the outline of every glyph +the VARC coverage names, at the eight locations of +varc_parity.expected, in its format. Build the font with +tests/tools/build_varc_face_fixtures.py first. + +Run: + uv run --no-project --with uharfbuzz==0.56.2 --with fonttools python tests/tools/varc_face_expected.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import uharfbuzz as hb +from fontTools.ttLib import TTFont + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from build_varc_face_fixtures import NO_RECORD, VARC_NAMES # noqa: E402 +from varc_morx_parity_expected import LOCATIONS, outline # noqa: E402 + +FIXTURES = Path(__file__).resolve().parent.parent / "fixtures" + + +def main() -> None: + path = FIXTURES / "varc_face.ttf" + order = TTFont(path).getGlyphOrder() + face = hb.Face(hb.Blob.from_file_path(str(path))) + lines = [ + f"# HarfBuzz {hb.version_string()} (uharfbuzz {hb.__version__}) outlines of varc_face.ttf.", + "# outline : M/L x y, Q cx cy x y, C ..., Z.", + ] + drawn = set() + for loc in LOCATIONS: + font = hb.Font(face) + if loc != "default": + font.set_variations({kv.split("=")[0]: float(kv.split("=")[1]) for kv in loc.split(",")}) + for name in VARC_NAMES + [NO_RECORD]: + gid = order.index(name) + ops = outline(font, gid) + if ops: + drawn.add(name) + lines.append(f"outline {loc} {gid} {name} {ops}") + # Every glyph but the one without a record draws something + # somewhere, so none of them is checked only against an empty + # outline. + assert NO_RECORD not in drawn + missing = [n for n in VARC_NAMES if n not in drawn] + assert not missing, missing + (FIXTURES / "varc_face.expected").write_text("\n".join(lines) + "\n", newline="\n") + + +if __name__ == "__main__": + main() diff --git a/tests/varc_parity.rs b/tests/varc_parity.rs index ae3f7daa..f86e8aab 100644 --- a/tests/varc_parity.rs +++ b/tests/varc_parity.rs @@ -9,22 +9,30 @@ //! RESET_UNSPECIFIED_AXES at the top and one level down, and an axis //! index past HarfBuzz's 4096-axis limit. //! -//! `tests/fixtures/varc_parity.expected` holds HarfBuzz 14.5.0's outline -//! of each glyph at eight locations. `tests/tools/build_varc_morx_parity_fixtures.py` -//! builds the font and `tests/tools/varc_morx_parity_expected.py` writes -//! the expected file. +//! `tests/fixtures/varc_face.ttf` has the same axes and 41 glyphs the +//! VARC coverage names, for what the face's walk over nested composites +//! does: reset components two and three composites down and in the +//! middle of a chain, components that name their own glyph, cycles of +//! two and three glyphs, a covered glyph without a record, delta sets +//! that end early or cut a run short, conditions inside nested +//! composites, one glyph reached many times at the same coords, and a +//! region without axes, which applies at the default instance too. +//! +//! Each `.expected` file holds HarfBuzz 14.5.0's outline of each glyph +//! at eight locations, which every glyph must match through +//! `Face::glyph_outline_at_coords`, `GlyphOutlines::outline` and +//! `GlyphOutlines::draw`. `tests/tools/build_varc_morx_parity_fixtures.py` +//! and `tests/tools/build_varc_face_fixtures.py` build the fonts, and +//! `tests/tools/varc_morx_parity_expected.py` and +//! `tests/tools/varc_face_expected.py` write the expected files. -use sigilbuzz::tables::PathOp; +use sigilbuzz::tables::{Outline, OutlineSink, PathOp}; use sigilbuzz::{Blob, Face}; const FONT: &[u8] = include_bytes!("fixtures/varc_parity.ttf"); const EXPECTED: &str = include_str!("fixtures/varc_parity.expected"); - -/// The glyph whose RESET_UNSPECIFIED_AXES component sits one composite -/// down. `Face::glyph_outline_at_coords` hands nested composites the -/// component's coords as the font's, so it is checked through -/// `Varc::composite_with_font_coords` instead. -const NESTED_RESET: &str = "v_outer"; +const FACE_FONT: &[u8] = include_bytes!("fixtures/varc_face.ttf"); +const FACE_EXPECTED: &str = include_str!("fixtures/varc_face.expected"); type Ops = Vec<(char, Vec)>; @@ -143,6 +151,35 @@ fn flatten(face: &Face<'_>, gid: u16, coords: &[f32], font: &[f32], t: [f32; 6], } } +/// An [`OutlineSink`] that records what `GlyphOutlines::draw` sends it. +#[derive(Default)] +struct Recorder(Outline); + +impl OutlineSink for Recorder { + fn move_to(&mut self, x: f32, y: f32) { + self.0.push(PathOp::MoveTo { x, y }); + } + fn line_to(&mut self, x: f32, y: f32) { + self.0.push(PathOp::LineTo { x, y }); + } + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + self.0.push(PathOp::QuadTo { cx, cy, x, y }); + } + fn curve_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) { + self.0.push(PathOp::CubicTo { + c1x, + c1y, + c2x, + c2y, + x, + y, + }); + } + fn close(&mut self) { + self.0.push(PathOp::Close); + } +} + #[track_caller] fn assert_close(got: &Ops, want: &Ops, what: &str) { let kinds = |ops: &Ops| ops.iter().map(|(op, _)| *op).collect::(); @@ -154,12 +191,16 @@ fn assert_close(got: &Ops, want: &Ops, what: &str) { } } -#[test] -fn varc_outlines_match_harfbuzz() { - let blob = Blob::new(FONT); +/// Checks every outline of `expected` against what `font` draws through +/// `Face::glyph_outline_at_coords`, `GlyphOutlines::outline` and +/// `GlyphOutlines::draw`, and `flatten`'s walk over +/// `Varc::composite_with_font_coords` when `walk` is set. Returns the +/// number of outlines checked. +fn check_against_harfbuzz(font: &[u8], expected: &str, walk: bool) -> usize { + let blob = Blob::new(font); let face = Face::parse(&blob, 0).unwrap(); let mut checked = 0; - for line in EXPECTED.lines().filter(|l| l.starts_with("outline ")) { + for line in expected.lines().filter(|l| l.starts_with("outline ")) { let mut fields = line.splitn(5, ' ').skip(1); let location = fields.next().unwrap(); let gid: u16 = fields.next().unwrap().parse().unwrap(); @@ -167,17 +208,51 @@ fn varc_outlines_match_harfbuzz() { let want = parse_ops(fields.next().unwrap_or("")); let coords = coords_at(&face, location); let what = format!("{name} at {location}"); - let got = if name == NESTED_RESET { + + let outline = face.glyph_outline_at_coords(gid, &coords).unwrap(); + assert_close( + &outline.map_or_else(Vec::new, |o| ops_of(o.ops())), + &want, + &format!("{what}, Face"), + ); + let outlines = face.glyph_outlines(&coords); + let outline = outlines.outline(gid).unwrap(); + assert_close( + &outline.map_or_else(Vec::new, |o| ops_of(o.ops())), + &want, + &format!("{what}, GlyphOutlines::outline"), + ); + let mut sink = Recorder::default(); + assert!(outlines.draw(gid, &mut sink).unwrap(), "{what}"); + assert_close( + &ops_of(sink.0.ops()), + &want, + &format!("{what}, GlyphOutlines::draw"), + ); + if walk { let mut ops = Vec::new(); let identity = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; flatten(&face, gid, &coords, &coords, identity, &mut ops); - ops - } else { - let outline = face.glyph_outline_at_coords(gid, &coords).unwrap(); - outline.map_or_else(Vec::new, |o| ops_of(o.ops())) - }; - assert_close(&got, &want, &what); + assert_close(&ops, &want, &format!("{what}, composite_with_font_coords")); + } checked += 1; } - assert_eq!(checked, 14 * 8); + checked +} + +#[test] +fn varc_outlines_match_harfbuzz() { + assert_eq!(check_against_harfbuzz(FONT, EXPECTED, true), 14 * 8); +} + +#[test] +fn varc_walks_through_nested_composites_match_harfbuzz() { + // Nested resets, self-references, cycles, a missing record, short + // delta sets, conditions inside nested composites, regions without + // axes. `flatten` recurses on a glyph that names itself, so only + // the face's walk draws this font. + assert_eq!( + check_against_harfbuzz(FACE_FONT, FACE_EXPECTED, false), + 41 * 8 + ); } From e0ae3534c21a669b0c839ce7e2e5c109fc9dc0e6 Mon Sep 17 00:00:00 2001 From: albedosehen Date: Sun, 4 Oct 2026 14:34:05 -0800 Subject: [PATCH 7/7] Work out VARC region scalars with one sort A subtable's region scalars were deduplicated through a BTreeMap, one insert per distinct region, which made them the costliest unit of the walk's budget. A crafted font whose glyph names, 2,047 times at distinct coords, a glyph varying through a subtable of 30,000 distinct regions took 16.8 s on main and 34 to 42 ms with the shared budget. The region slots and positions are now packed into one u32 each (both fit 16 bits) and sorted once, and each distinct region is evaluated as the sorted list reaches it. The font above now takes 11 to 13 ms per glyph, and every hostile font built for this change draws its slowest glyph in under 19 ms. The scalars and the axis count charged for them are unchanged; a new test checks that scalars keep the subtable's order with indexes out of order, repeated, and past the region list. --- src/tables/multi_var_store.rs | 71 +++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/src/tables/multi_var_store.rs b/src/tables/multi_var_store.rs index e7c519d0..1e4d8300 100644 --- a/src/tables/multi_var_store.rs +++ b/src/tables/multi_var_store.rs @@ -344,26 +344,41 @@ impl<'a> MultiVarStore<'a> { /// Scalars for each entry of `region_indexes`, in order, and the /// number of region axes evaluated for them. Each distinct region is /// evaluated once, so a subtable that names one large region many - /// times does not repeat the work. + /// times does not repeat the work. The entries are put in region + /// order by one sort, so a subtable of many distinct regions costs + /// no map insert per region either. fn scalars_for(&self, region_indexes: &[u16], coords: &[f32]) -> (Vec, usize) { - let mut cache: BTreeMap = BTreeMap::new(); - let mut axis_steps = 0usize; - let scalars = region_indexes + let mut scalars = vec![0.0_f32; region_indexes.len()]; + // Region slot and position, each under 65536 (both counts are + // u16), packed into one word to sort; an index past the region + // list keeps a zero scalar. + let mut order: Vec = region_indexes .iter() - .map(|&ri| { - let Some(&slot) = self.region_slots.get(ri as usize) else { - return 0.0; - }; - let Some(region) = self.regions.get(slot) else { - return 0.0; - }; - *cache.entry(slot).or_insert_with(|| { - let (scalar, steps) = sparse_region_scalar_steps(region, coords); - axis_steps = axis_steps.saturating_add(steps); - scalar - }) + .zip(0u32..) + .filter_map(|(&ri, at)| { + let slot = *self.region_slots.get(usize::from(ri))?; + Some(((slot as u32) << 16) | at) }) .collect(); + order.sort_unstable(); + let mut axis_steps = 0usize; + let mut last: Option<(usize, f32)> = None; + for packed in order { + let (slot, at) = ((packed >> 16) as usize, (packed & 0xFFFF) as usize); + let scalar = match last { + Some((known, scalar)) if known == slot => scalar, + _ => { + let scalar = self.regions.get(slot).map_or(0.0, |region| { + let (scalar, steps) = sparse_region_scalar_steps(region, coords); + axis_steps = axis_steps.saturating_add(steps); + scalar + }); + last = Some((slot, scalar)); + scalar + } + }; + scalars[at] = scalar; + } (scalars, axis_steps) } @@ -1010,6 +1025,30 @@ mod tests { assert!(s.resolve_deltas(0, 0, 1 << 40, &[0.0]).is_none()); } + #[test] + fn region_scalars_keep_the_subtable_order() { + // Indexes out of order, repeated, and one past the region list: + // each scalar lands at its own position, and the one past the + // list is zero. + let bytes = build_store( + &[ + vec![(0, 0.0, 1.0, 1.0)], + vec![(0, 0.0, 0.5, 1.0)], + vec![(1, 0.0, 1.0, 1.0)], + ], + &[2, 0, 2, 1, 7], + &[&[0x00, 0x01]], + ); + let s = MultiVarStore::parse(&bytes).unwrap(); + let coords = [0.75, 0.25]; + let want: Vec = [2, 0, 2, 1, 7] + .iter() + .map(|&ri| s.region_scalar(ri, &coords)) + .collect(); + assert_eq!(want, [0.25, 0.75, 0.25, 0.5, 0.0]); + assert_eq!(s.region_scalars(0, &coords).unwrap(), want); + } + #[test] fn repeated_region_index_is_evaluated_once() { // One region with 65535 axes, named 65535 times by the