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/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..ea9638c2 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,46 @@ 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 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. 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)), + /// 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, 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 +336,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..704be4ab --- /dev/null +++ b/src/face/varc_draw.rs @@ -0,0 +1,538 @@ +//! 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 alloc::vec; +use alloc::vec::Vec; + +use super::Face; +use crate::error::{Error, Result}; +use crate::tables::glyf::PhantomMetrics; +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` +/// 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`. +/// +/// 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. 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>, + /// 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. + path: Vec, + 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). + /// + /// 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, + 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` + /// 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) + } + + /// 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 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) { + self.path.push(gid); + let drawn = self.draw_composite(gid, &composite, transform, depth, sink); + self.path.pop(); + return drawn; + } + } + 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, + }) +} + +#[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,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)); + 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,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, 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]); + } +} diff --git a/src/tables/multi_var_store.rs b/src/tables/multi_var_store.rs index c517ab82..1e4d8300 100644 --- a/src/tables/multi_var_store.rs +++ b/src/tables/multi_var_store.rs @@ -341,25 +341,45 @@ 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 { - let mut cache: BTreeMap = BTreeMap::new(); - region_indexes + /// 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. 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 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(|| sparse_region_scalar(region, coords)) + .zip(0u32..) + .filter_map(|(&ri, at)| { + let slot = *self.region_slots.get(usize::from(ri))?; + Some(((slot as u32) << 16) | at) }) - .collect() + .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) } /// Per-region scalars for all regions referenced by subtable `outer`, @@ -368,7 +388,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`. @@ -398,9 +418,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,12 +451,19 @@ 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; }; - let scalars = self.slot_scalars(slot, coords); + let (scalars, _) = self.slot_scalars(slot, coords); self.add_slot_deltas(slot, inner, &scalars, out); } @@ -453,12 +482,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 @@ -477,10 +508,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 +522,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 +543,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 +553,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 +562,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 +589,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); @@ -597,15 +611,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`, @@ -1006,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 diff --git a/src/tables/varc.rs b/src/tables/varc.rs index 94800daa..48e845db 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 { @@ -241,7 +244,9 @@ impl<'a> Varc<'a> { } /// 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 @@ -253,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: @@ -272,9 +278,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 +295,104 @@ 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 does not cover `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.covers(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.covers(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)?; + // 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], + 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 +402,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 +438,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 +483,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 +513,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 +561,7 @@ impl<'a> Varc<'a> { t.sy = t.sx; } - Some(VarcComponent { + Resolved::Component(VarcComponent { gid, transform: t.to_affine(), coords: child_coords, @@ -513,11 +616,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 +629,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 } @@ -570,17 +674,22 @@ 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 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 } Some(format @ (3 | 4)) => { let Some(&count) = data.get(2) else { 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); @@ -600,10 +709,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; @@ -616,48 +726,141 @@ 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 @@ -983,4 +1186,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 d0bcd91f..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)); } @@ -501,7 +504,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 +514,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 +535,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 @@ -604,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); @@ -643,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 { @@ -755,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 @@ -799,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()); @@ -815,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)]), ) } @@ -826,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 { @@ -857,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] 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 00000000..d1f5344b Binary files /dev/null and b/tests/fixtures/varc_face.ttf differ diff --git a/tests/tools/build_varc_face_fixtures.py b/tests/tools/build_varc_face_fixtures.py new file mode 100644 index 00000000..608a13e0 --- /dev/null +++ b/tests/tools/build_varc_face_fixtures.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +"""Write tests/fixtures/varc_face.ttf, which tests/varc_parity.rs checks. + +A TrueType font with the three axes of varc_parity.ttf (AAAA, BBBB, +CCCC, -100 to 100, default 0) whose VARC glyphs exercise what +`Face::glyph_outline_at_coords` and `GlyphOutlines` do across nested +composites: + +- RESET_UNSPECIFIED_AXES two and three composites down, reset in the + middle of a chain, and reset components whose axis values vary at the + coords of the glyph they belong to; +- components that name their own glyph (HarfBuzz draws the glyph's + `glyf` outline), at the top and one composite down, with a reset; +- delta sets that end before they fill their tuples, runs cut short in + the first, a middle and the last tuple, an empty set, a cut i32 run, + short axis-value sets and a Value condition over a short set; +- conditions read inside nested composites, at the coords of the glyph + they gate, beside a reset component; +- one glyph reached many times at the same coords, and at others; +- cycles of two and three glyphs, which HarfBuzz's decycler cuts; +- a component the coverage names past the end of the glyph records, + which HarfBuzz draws as nothing; +- a region that constrains no axis, whose deltas HarfBuzz applies at + the default instance too, since its font holds a zero per axis there. + +The VARC table is written byte by byte with the encoders of +build_varc_morx_parity_fixtures.py. tests/tools/varc_face_expected.py +records HarfBuzz's outlines for it. + +Run: + uv run --no-project --with fonttools python tests/tools/build_varc_face_fixtures.py +""" + +from __future__ import annotations + +import struct +import sys +from pathlib import Path + +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.ttLib.tables.DefaultTable import DefaultTable +from fontTools.ttLib.tables.TupleVariation import TupleVariation + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build_varc_morx_parity_fixtures as p # noqa: E402 + +FIXTURES = Path(__file__).resolve().parent.parent / "fixtures" + + +def diamond(): + pen = TTGlyphPen(None) + pen.moveTo((50, 0)); pen.lineTo((100, 50)); pen.lineTo((50, 100)); pen.lineTo((0, 50)); pen.closePath() + return pen.glyph() + + +def mvs_raw(regions, subtables): + """p.multi_var_store, with each delta set given as raw bytes, so a + set can end early or cut a run short.""" + rl = struct.pack(">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 + ); }