diff --git a/crates/sigilbuzz-subset/src/lib.rs b/crates/sigilbuzz-subset/src/lib.rs index 326b6ca..d9187a4 100644 --- a/crates/sigilbuzz-subset/src/lib.rs +++ b/crates/sigilbuzz-subset/src/lib.rs @@ -706,16 +706,22 @@ fn push_layout_and_variation_tables( // are rewritten to point at the new gid namespace; the // MultiVarStore is pruned to the entries the kept records use. The // core parse only validates the table: `subset_varc` walks the raw - // bytes itself. - if face.varc()?.is_some() { - let varc_bytes = face.table_bytes(tag::VARC).map_err(SubsetError::from)?; - let lookup = |old: GlyphId| -> Option { - let i = gid_map.binary_search_by_key(&old, |(o, _)| *o).ok()?; - gid_map.get(i).map(|&(_, new)| new) - }; - if let Some(b) = varc::subset_varc(varc_bytes, kept, &lookup)? { - tables.push((tag::VARC, b)); + // bytes itself. A VARC that does not parse counts as absent, as the + // face's outlines read it and as HarfBuzz drops it: it is left out + // with a warning, and its glyphs keep their glyf or CFF outlines. + match face.varc() { + Ok(Some(_)) => { + let varc_bytes = face.table_bytes(tag::VARC).map_err(SubsetError::from)?; + let lookup = |old: GlyphId| -> Option { + let i = gid_map.binary_search_by_key(&old, |(o, _)| *o).ok()?; + gid_map.get(i).map(|&(_, new)| new) + }; + if let Some(b) = varc::subset_varc(varc_bytes, kept, &lookup)? { + tables.push((tag::VARC, b)); + } } + Ok(None) => {} + Err(_) => warnings.push(tag::VARC, 0, "VARC that does not parse", "the whole table"), } Ok(()) } diff --git a/crates/sigilbuzz-subset/tests/varc_unparsable.rs b/crates/sigilbuzz-subset/tests/varc_unparsable.rs new file mode 100644 index 0000000..e7f0b0b --- /dev/null +++ b/crates/sigilbuzz-subset/tests/varc_unparsable.rs @@ -0,0 +1,134 @@ +//! A `VARC` table the core cannot parse is left out of a subset with a +//! warning, as the face's outlines read the font without it and as +//! HarfBuzz drops it. Before, `subset` failed on such a font. +//! +//! The table splices in a MultiItemVariationStore in the layout HarfBuzz +//! adopted after 14.5.0 (a 32-bit region count, and an Offset32 from each +//! MultiVarData to its delta-set INDEX), which the core rejects. + +use sigilbuzz::Face; +use sigilbuzz_subset::{subset, SubsetInput}; + +const GLYF_FONT: &[u8] = include_bytes!("../../../tests/fixtures/varc_parity.ttf"); + +/// A `VARC` table covering `covered`, each glyph one component naming +/// glyph 1, over a MultiItemVariationStore in the post-14.5.0 layout. +fn new_layout_varc(covered: &[u16]) -> Vec { + let mut coverage = vec![0, 1]; + coverage.extend_from_slice(&(covered.len() as u16).to_be_bytes()); + for g in covered { + coverage.extend_from_slice(&g.to_be_bytes()); + } + + let mut store = Vec::new(); + store.extend_from_slice(&1u16.to_be_bytes()); // format + store.extend_from_slice(&12u32.to_be_bytes()); // region list + store.extend_from_slice(&1u16.to_be_bytes()); // one MultiVarData + store.extend_from_slice(&30u32.to_be_bytes()); // at 30 + store.extend_from_slice(&1u32.to_be_bytes()); // region count (u32) + store.extend_from_slice(&8u32.to_be_bytes()); // Offset32 to the region + store.extend_from_slice(&1u16.to_be_bytes()); // axis count + store.extend_from_slice(&0u16.to_be_bytes()); // axis 0 + store.extend_from_slice(&0i16.to_be_bytes()); + store.extend_from_slice(&0x4000i16.to_be_bytes()); + store.extend_from_slice(&0x4000i16.to_be_bytes()); + assert_eq!(store.len(), 30); + store.push(1); // MultiVarData format + store.extend_from_slice(&1u16.to_be_bytes()); + store.extend_from_slice(&0u16.to_be_bytes()); + store.extend_from_slice(&9u32.to_be_bytes()); // Offset32 to the INDEX + store.extend_from_slice(&1u32.to_be_bytes()); + store.extend_from_slice(&[1, 1, 3, 0x00, 100]); + + let record = [0x18, 0x00, 0x01, 0x00, 0x00, 0x00]; + let mut records = (covered.len() as u32).to_be_bytes().to_vec(); + records.push(1); // offSize + for i in 0..=covered.len() { + records.push((1 + i * record.len()) as u8); + } + for _ in covered { + records.extend_from_slice(&record); + } + + let mut out = Vec::new(); + out.extend_from_slice(&[0, 1, 0, 0]); // version 1.0 + let mut at = 24u32; + for part in [&coverage, &store] { + out.extend_from_slice(&at.to_be_bytes()); + at += part.len() as u32; + } + out.extend_from_slice(&0u32.to_be_bytes()); // no conditions + out.extend_from_slice(&0u32.to_be_bytes()); // no axis indices + out.extend_from_slice(&at.to_be_bytes()); // glyph records + out.extend_from_slice(&coverage); + out.extend_from_slice(&store); + out.extend_from_slice(&records); + out +} + +/// `font` with its tables rewritten into a new directory, `VARC` +/// replaced by `varc`, or dropped when `varc` is `None`. +fn with_varc(font: &[u8], varc: Option<&[u8]>) -> Vec { + let face = Face::parse_bytes(font, 0).unwrap(); + let mut tables: Vec<([u8; 4], &[u8])> = face + .records() + .iter() + .filter(|r| &r.tag != b"VARC") + .map(|r| (r.tag, face.table_bytes(r.tag).unwrap())) + .collect(); + if let Some(varc) = varc { + tables.push((*b"VARC", varc)); + } + tables.sort_by_key(|t| t.0); + let header = 12 + 16 * tables.len(); + let mut out = Vec::new(); + out.extend_from_slice(&font[..4]); + out.extend_from_slice(&(tables.len() as u16).to_be_bytes()); + out.extend_from_slice(&[0; 6]); + 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(&((header + body.len()) 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); + } + } + out.extend_from_slice(&body); + out +} + +#[test] +fn a_subset_leaves_out_a_varc_that_does_not_parse() { + let bad_varc = new_layout_varc(&[2, 3]); + let with_bad = with_varc(GLYF_FONT, Some(&bad_varc)); + let without = with_varc(GLYF_FONT, None); + let bad_face = Face::parse_bytes(&with_bad, 0).unwrap(); + assert!(bad_face.varc().is_err(), "the spliced VARC must not parse"); + let plain_face = Face::parse_bytes(&without, 0).unwrap(); + + for drop_unhandled in [false, true] { + let input = SubsetInput { + gids: vec![0, 1, 2, 3], + drop_unhandled, + ..Default::default() + }; + let got = subset(&bad_face, &input).expect("subset succeeds without VARC"); + let want = subset(&plain_face, &input).expect("subset of the font without VARC"); + assert_eq!( + got.bytes, want.bytes, + "the subset equals the subset of the font without VARC (drop_unhandled {drop_unhandled})" + ); + let out = Face::parse_bytes(&got.bytes, 0).unwrap(); + assert!(out.record(*b"VARC").is_none(), "VARC is left out"); + assert!( + got.warnings + .iter() + .any(|w| w.table == *b"VARC" && w.dropped == "the whole table"), + "a warning names the dropped VARC: {:?}", + got.warnings + ); + } +} diff --git a/src/face/glyph_outlines.rs b/src/face/glyph_outlines.rs index 76d53ff..5bb662a 100644 --- a/src/face/glyph_outlines.rs +++ b/src/face/glyph_outlines.rs @@ -24,7 +24,8 @@ use crate::tables::{tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, Outline, OutlineSink /// them: the `CFF2` or `CFF ` table, or `glyf` with `loca`, `gvar` and /// the metrics its phantom points read, and `VARC`. Each glyph comes out /// exactly as [`Face::glyph_outline_at_coords`] draws it, errors -/// included: a table that does not read fails every glyph that needs it. +/// included: a table that does not read fails every glyph that needs it, +/// except `VARC`, which counts as absent then, as for the face. /// /// It caches through cells that are not thread-safe, so it is `Send` but /// not `Sync`: build one per thread. @@ -48,7 +49,8 @@ pub struct GlyphOutlines<'a> { face: Face<'a>, /// The coords as shaping reads them, rounded to F2DOT14. coords: Vec, - varc: OnceCell>>>, + /// The face's `VARC`, `None` when it has none or it does not parse. + varc: OnceCell>>, tables: OnceCell>>, } @@ -111,11 +113,7 @@ impl<'a> GlyphOutlines<'a> { /// The errors [`Face::glyph_outline_at_coords`] returns for the /// glyph. pub fn draw(&self, glyph_id: u16, sink: &mut S) -> Result { - let varc = self - .varc - .get_or_init(|| self.face.varc()) - .as_ref() - .map_err(Clone::clone)?; + let varc = self.varc.get_or_init(|| self.face.drawable_varc()); // 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. diff --git a/src/face/outlines.rs b/src/face/outlines.rs index ea9638c..04b8f53 100644 --- a/src/face/outlines.rs +++ b/src/face/outlines.rs @@ -175,16 +175,24 @@ impl<'a> Face<'a> { /// given. /// /// A glyph the font's `VARC` table covers is drawn from its - /// components, as HarfBuzz draws it: + /// components, as HarfBuzz draws it (a `VARC` table that does not + /// parse counts as absent, as HarfBuzz drops 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's `glyf` or CFF outline instead of recursing. A covered /// glyph past the end of the glyph records draws nothing. + /// - A cycle of `n` glyphs is cut where HarfBuzz's decycler cuts it, + /// `2n - 1` levels down. A cycle of more than 32 glyphs reaches + /// the 64-level cap first and fails the glyph, where HarfBuzz stops + /// expanding at that depth and draws the rest. + /// - Leaves (glyphs drawn from `glyf` or CFF) share HarfBuzz's 2^24 + /// glyph budget for their `gvar` and charstring work, and draw + /// nothing once it runs out, as HarfBuzz's do; a leaf reached again + /// at the same coords is drawn once. /// - 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 @@ -201,7 +209,7 @@ impl<'a> Face<'a> { coords: &[f32], ) -> Result> { let coords = f2dot14_coords(coords); - if let Some(varc) = self.varc()? { + if let Some(varc) = self.drawable_varc() { let mut out = Outline::new(); if VarcDraw::new(self, &varc, &coords).draw(glyph_id, &mut out)? { return Ok(Some(out)); diff --git a/src/face/varc_draw.rs b/src/face/varc_draw.rs index 704be4a..fa7ac63 100644 --- a/src/face/varc_draw.rs +++ b/src/face/varc_draw.rs @@ -4,14 +4,21 @@ use core::cell::OnceCell; +use alloc::collections::BTreeMap; +use alloc::rc::Rc; use alloc::vec; use alloc::vec::Vec; use super::Face; use crate::error::{Error, Result}; +use crate::tables::cff::{MAX_CHARSTRING_OPS, OUT_OF_CHARSTRING_OPS}; +use crate::tables::cff2::Cff2Shared; use crate::tables::glyf::PhantomMetrics; +use crate::tables::gvar::OUT_OF_TUPLE_WORK; use crate::tables::varc::{VarcComposite, VarcMemo}; -use crate::tables::{tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, OutlineSink, Varc, Vmtx}; +use crate::tables::{ + tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, Outline, OutlineSink, PathOp, Varc, Vmtx, +}; /// Deepest a component may sit below the glyph drawn, as the `glyf` /// composite walk caps its depth. @@ -43,7 +50,9 @@ const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; /// 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. +/// tortoise sits. That cuts a cycle of `n` glyphs `2n - 1` levels down, +/// so one of more than 32 glyphs reaches [`MAX_VARC_DEPTH`] first, which +/// fails the glyph where HarfBuzz stops expanding at that depth. pub(crate) struct VarcDraw<'w, 'a> { face: &'w Face<'a>, varc: &'w Varc<'a>, @@ -95,7 +104,11 @@ impl<'w, 'a> VarcDraw<'w, 'a> { /// 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 { + let max = self.components_left.saturating_add(1); + let Some(composite) = self + .varc + .resolve(glyph_id, coords, coords, &mut self.memo, max) + else { return Ok(false); }; self.path.push(glyph_id); @@ -163,20 +176,27 @@ impl<'w, 'a> VarcDraw<'w, 'a> { return Ok(()); } let font_coords = &self.font_coords; - if let Some(composite) = self.varc.resolve(gid, coords, font_coords, &mut self.memo) { + let max = self.components_left.saturating_add(1); + if let Some(composite) = + self.varc + .resolve(gid, coords, font_coords, &mut self.memo, max) + { self.path.push(gid); let drawn = self.draw_composite(gid, &composite, transform, depth, sink); self.path.pop(); return drawn; } } + let Some(leaf) = self.leaves.outline(self.face, gid, coords)? else { + return Ok(()); + }; 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)?; + replay(&leaf, &mut placed); self.ops_left = self .ops_left .checked_sub(placed.drawn) @@ -272,15 +292,70 @@ impl OutlineSink for Placed<'_, S> { } } -/// 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)] +/// Work the leaves of one glyph may do between them: HarfBuzz's +/// `HB_BUDGET_GLYPH`, which its VARC walk shares with every leaf it +/// draws. In units of: one per `gvar` tuple header and per point of +/// each tuple decoded (as a `glyf` walk charges `gvar`), one per point a +/// `glyf` leaf lays down and [`GLYPH_VISIT_WORK`] per glyph record it +/// visits, [`CHARSTRING_OP_WORK`] per charstring operation, and the +/// regions times axes of a `CFF2` variation store for each coord vector +/// its leaves are drawn at. Past it, a leaf draws nothing, as +/// HarfBuzz's leaves draw nothing once its budget runs out. +const MAX_LEAF_WORK: usize = 1 << 24; + +/// Leaf work per glyph record a `glyf` leaf visits. +const GLYPH_VISIT_WORK: usize = 64; + +/// Most ops the leaf outlines a walk keeps may hold between them. A +/// leaf drawn past that is not kept, so drawing it again costs its work +/// again; the walk's own op cap bounds what it draws. +const MAX_KEPT_LEAF_OPS: usize = 1 << 18; + +/// Leaf work per charstring operation a CFF or `CFF2` leaf runs: about +/// what an operation costs next to one point of one `gvar` tuple. +const CHARSTRING_OP_WORK: usize = 8; + +/// A leaf outline [`Leaves`] keeps: the coords it was drawn at, as `f32` +/// bits, and the outline, `None` when it drew nothing. +type KeptLeaf = (Vec, Option>); + +/// The leaves of a walk: the tables they 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); every +/// leaf outline drawn so far (up to [`MAX_KEPT_LEAF_OPS`]), so a glyph +/// reached again at the same coords is drawn once; and the work left +/// for them. struct Leaves<'a> { tables: OnceCell>>, /// Read for the first leaf away from the default instance. gvar: OnceCell>>>, + /// Outlines by glyph and coords (as `f32` bits), bucketed by the + /// glyph and [`coords_hash`] so a lookup compares long coord + /// vectors once; `None` for a leaf that drew nothing. The coords + /// were charged to the walk's work when its components were built, + /// so the keys are bounded by it. + drawn: BTreeMap<(u16, u64), Vec>, + /// What the `CFF2` leaves drawn at one coord vector share, bucketed + /// the same way. + cff2_shared: BTreeMap, Cff2Shared<'a>)>>, + /// See [`MAX_LEAF_WORK`]. + work: usize, + /// See [`MAX_KEPT_LEAF_OPS`]. + kept_ops_left: usize, +} + +impl Default for Leaves<'_> { + fn default() -> Self { + Self { + tables: OnceCell::new(), + gvar: OnceCell::new(), + drawn: BTreeMap::new(), + cff2_shared: BTreeMap::new(), + work: MAX_LEAF_WORK, + kept_ops_left: MAX_KEPT_LEAF_OPS, + } + } } /// The table a face draws glyphs from without `VARC`. @@ -297,23 +372,83 @@ enum LeafTables<'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, + /// The outline of glyph `gid` of `face` at `coords`, from `glyf` or + /// CFF as [`Face::glyph_outline_at_coords`] draws a glyph `VARC` + /// does not, or `None` when it has none or the leaves are out of + /// work. Drawn once per glyph and coords. + fn outline( + &mut self, face: &Face<'a>, gid: u16, coords: &[f32], - sink: &mut S, - ) -> Result { + ) -> Result>> { + let bits: Vec = coords.iter().map(|c| c.to_bits()).collect(); + let hash = coords_hash(&bits); + let known = self + .drawn + .get(&(gid, hash)) + .and_then(|bucket| bucket.iter().find(|(key, _)| *key == bits)); + if let Some((_, outline)) = known { + return Ok(outline.clone()); + } + let outline = self.draw(face, gid, coords, &bits, hash)?.map(Rc::new); + let ops = outline.as_ref().map_or(0, |o| o.len()); + if ops <= self.kept_ops_left { + self.kept_ops_left -= ops; + let bucket = self.drawn.entry((gid, hash)).or_default(); + bucket.push((bits, outline.clone())); + } + Ok(outline) + } + + /// Draws a leaf [`Leaves::outline`] has not drawn yet, charging its + /// work. `bits` are the coords as `f32` bits, `hash` their + /// [`coords_hash`]. + fn draw( + &mut self, + face: &Face<'a>, + gid: u16, + coords: &[f32], + bits: &[u32], + hash: u64, + ) -> Result> { + if self.work == 0 { + return Ok(None); + } 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), + let mut out = Outline::new(); + let drew = match tables { + LeafTables::Cff2(cff2) => { + let bucket = self.cff2_shared.entry(hash).or_default(); + let at = bucket.iter().position(|(key, _)| key == bits); + let fresh = at.is_none(); + let at = at.unwrap_or_else(|| { + bucket.push((bits.to_vec(), Cff2Shared::default())); + bucket.len() - 1 + }); + let shared = &bucket[at].1; + let limit = charstring_limit(self.work); + let mut ops = limit; + let drew = cff2.outline_limited(gid, coords, shared, &mut out, &mut ops); + let mut cost = (limit - ops) as usize * CHARSTRING_OP_WORK; + if fresh { + cost = cost.saturating_add(shared.scalar_work()); + } + self.work = self.work.saturating_sub(cost); + charstring_drew(drew, limit, &mut self.work)? + } + LeafTables::Cff(cff) => { + let limit = charstring_limit(self.work); + let mut ops = limit; + let drew = cff.draw_limited(gid, &mut out, &mut ops); + let cost = (limit - ops) as usize * CHARSTRING_OP_WORK; + self.work = self.work.saturating_sub(cost); + charstring_drew(drew, limit, &mut self.work)? + } LeafTables::Glyf { glyf, loca, @@ -333,8 +468,86 @@ impl<'a> Leaves<'a> { hmtx, vmtx: Some(vmtx), }; - glyf.outline_at_coords(loca, gid, gvar, coords, Some(&metrics), sink) + let mut tuple_work = self.work; + let drew = glyf.outline_at_coords_with_work( + loca, + gid, + gvar, + coords, + Some(&metrics), + &mut out, + &mut tuple_work, + ); + match drew { + Ok((drew, visits, points)) => { + let laid = visits + .saturating_mul(GLYPH_VISIT_WORK) + .saturating_add(points); + self.work = tuple_work.saturating_sub(laid); + drew + } + Err(Error::Malformed { context, .. }) if context == OUT_OF_TUPLE_WORK => { + self.work = 0; + false + } + Err(e) => return Err(e), + } } + }; + Ok(drew.then_some(out)) + } +} + +/// Whether a CFF or `CFF2` leaf drew, from its result: a charstring +/// stopped by the leaves' work (a `limit` below one glyph's own) draws +/// nothing and ends the leaves' `work`; other errors fail the walk. +fn charstring_drew(drew: Result, limit: u32, work: &mut usize) -> Result { + match drew { + Err(Error::Malformed { context, .. }) + if context == OUT_OF_CHARSTRING_OPS && limit < MAX_CHARSTRING_OPS => + { + *work = 0; + Ok(false) + } + drew => drew, + } +} + +/// FNV-1a over the coords' `f32` bits and their count: a bucket key, so +/// a map of long coord vectors compares them once per lookup. +fn coords_hash(bits: &[u32]) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + for &b in bits { + hash ^= u64::from(b); + hash = hash.wrapping_mul(0x0100_0000_01b3); + } + hash ^ bits.len() as u64 +} + +/// The charstring operations `work` leaf work pays for, at most one +/// glyph's own limit. +fn charstring_limit(work: usize) -> u32 { + u32::try_from(work / CHARSTRING_OP_WORK) + .unwrap_or(u32::MAX) + .min(MAX_CHARSTRING_OPS) +} + +/// 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(), } } } @@ -535,4 +748,61 @@ mod tests { assert_eq!(at(&[0.0, 0.0, 0.0]), [100.0]); assert_eq!(at(&[0.5]), [100.0]); } + + /// `varc_parity.ttf` with glyph 3 made of one component per entry of + /// `axis0`, each naming the box (glyph 1, varied by `gvar`) with + /// axis 0 set to that F2DOT14 value. + fn boxes_at(axis0: &[i16]) -> Vec { + let mut record = Vec::new(); + for &value in axis0 { + // HAVE_AXES, glyph 1, axis indices list 0, one i16 value. + record.extend_from_slice(&[0x02, 0x00, 0x01, 0x00, 0x40]); + record.extend_from_slice(&value.to_be_bytes()); + } + let table = build_varc(&[3], &[&record], None, Some(&[&[0x00, 0x00]])); + with_varc( + include_bytes!("../../tests/fixtures/varc_parity.ttf"), + &table, + ) + } + + #[test] + fn a_leaf_reached_again_at_the_same_coords_is_drawn_once() { + // 500 boxes at one coords: the box is drawn, and its gvar work + // charged, once; every copy is placed from that outline. + let font = boxes_at(&[8192; 500]); + 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()); + assert_eq!(move_xs(&out).len(), 500); + assert_eq!(draw.leaves.drawn.len(), 1); + let once = MAX_LEAF_WORK - draw.leaves.work; + // One box at axis 0 = 0.5 costs its 4 tuple headers, the 8 + // points (4 and 4 phantoms) of the one tuple that holds there, + // one glyph visit and its 4 points. + assert_eq!(once, 4 + 8 + GLYPH_VISIT_WORK + 4); + } + + #[test] + fn leaves_past_their_work_draw_nothing() { + // Three boxes at distinct coords with work for two of them: the + // third draws nothing and the glyph does not fail, as HarfBuzz's + // leaves stop once its budget runs out. + let font = boxes_at(&[8192, 8193, 8194]); + 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, &[]); + draw.leaves.work = 2 * (4 + 8 + GLYPH_VISIT_WORK + 4); + let mut out = Outline::new(); + assert!(draw.draw(3, &mut out).unwrap()); + assert_eq!(move_xs(&out).len(), 2); + assert_eq!(draw.leaves.work, 0); + // With the whole budget, all three draw. + let all = face.glyph_outline(3).unwrap().unwrap(); + assert_eq!(move_xs(&all).len(), 3); + } } diff --git a/src/face/variations.rs b/src/face/variations.rs index 7aee318..e134f55 100644 --- a/src/face/variations.rs +++ b/src/face/variations.rs @@ -84,6 +84,10 @@ impl<'a> Face<'a> { /// shipped to date only by Chrome's experimental VARC fonts and /// a handful of demo files. Returns `Ok(None)` for the vast /// majority of fonts that don't carry it. + /// + /// The outline methods, [`crate::GlyphOutlines`] and shaping treat a + /// `VARC` table this fails to parse as absent, as HarfBuzz's + /// sanitizer drops it, and draw every glyph from `glyf` or CFF. pub fn varc(&self) -> Result>> { match self.table_bytes(tag::VARC) { Ok(bytes) => Ok(Some(Varc::parse(bytes)?)), @@ -91,4 +95,15 @@ impl<'a> Face<'a> { Err(e) => Err(e), } } + + /// The `VARC` table glyphs are drawn through: [`Face::varc`], with a + /// table that does not parse (or whose bytes the directory does not + /// hold) read as absent. HarfBuzz 14.5.0 drops a `VARC` its + /// sanitizer rejects, such as one in the MultiItemVariationStore + /// layout HarfBuzz adopted after 14.5.0, and draws and shapes the + /// font as if it had none; failing every glyph instead would leave + /// the font unusable. + pub(crate) fn drawable_varc(&self) -> Option> { + self.varc().ok().flatten() + } } diff --git a/src/shape/fallback/extents.rs b/src/shape/fallback/extents.rs index 9fa8a4f..fbfaf15 100644 --- a/src/shape/fallback/extents.rs +++ b/src/shape/fallback/extents.rs @@ -38,8 +38,10 @@ pub(in crate::shape) struct ExtentsTables<'a> { /// [`Face::phantom_vmtx`]). A `vmtx` that does not parse counts as /// absent, as HarfBuzz's sanitizer drops it. vmtx: OnceCell>>, - /// `VARC`, whose composites take precedence over `CFF2` and `CFF `. - varc: OnceCell>>>, + /// `VARC`, whose composites take precedence over `CFF2` and `CFF `; + /// `None` when the font has none or it does not parse, as the face's + /// outlines read it. + varc: OnceCell>>, /// The CFF table a glyph's outline comes from, `None` when the font /// draws its glyphs some other way. cff: OnceCell>>>, @@ -86,11 +88,7 @@ impl<'a> ExtentsTables<'a> { if !has_cff { return Ok(None); } - let varc = self - .varc - .get_or_init(|| face.varc()) - .as_ref() - .map_err(Clone::clone)?; + let varc = self.varc.get_or_init(|| face.drawable_varc()); let cff = self.cff.get_or_init(|| { if face.record(tag::CFF2).is_some() { Some( diff --git a/src/tables/cff.rs b/src/tables/cff.rs index b67ae64..e5b0dd2 100644 --- a/src/tables/cff.rs +++ b/src/tables/cff.rs @@ -47,7 +47,9 @@ use crate::error::{Error, Result}; use crate::tables::outline::OutlineSink; use crate::tables::parse::Reader; -pub(crate) use charstring::{BlendContext, CharstringSink, Interp2, RegionCache}; +pub(crate) use charstring::{ + BlendContext, CharstringSink, Interp2, RegionCache, MAX_CHARSTRING_OPS, OUT_OF_CHARSTRING_OPS, +}; pub(crate) use dict::FdSelect; pub(crate) use index::{read_index2, Index}; @@ -211,13 +213,38 @@ impl<'a> Cff<'a> { /// [`Cff::outline`] into any [`CharstringSink`], such as one that /// keeps the points in the `f64` the charstring is evaluated in. pub(crate) fn draw(&self, glyph_id: u16, sink: &mut S) -> Result { + self.draw_with(glyph_id, sink, None) + } + + /// [`Cff::draw`] for a caller that bounds many glyphs together: the + /// glyph's charstrings (a seac's base and accent included) may run + /// at most `ops_left` operations between them, which they take from + /// `ops_left`, failing with the operation-limit error past them. + pub(crate) fn draw_limited( + &self, + glyph_id: u16, + sink: &mut S, + ops_left: &mut u32, + ) -> Result { + self.draw_with(glyph_id, sink, Some(ops_left)) + } + + /// [`Cff::draw`], each charstring limited to its own operations, or + /// with `ops_left` shared as [`Cff::draw_limited`] shares it. + fn draw_with( + &self, + glyph_id: u16, + sink: &mut S, + mut ops_left: Option<&mut u32>, + ) -> Result { let gid = usize::from(glyph_id); if gid >= self.char_strings.len() { return Ok(false); } let local_subrs = self.local_subrs(gid)?; - if let Some(seac) = self.run_charstring(gid, local_subrs, sink, None)? { - self.draw_seac(seac, local_subrs, sink)?; + let seac = self.run_charstring(gid, local_subrs, sink, None, ops_left.as_deref_mut())?; + if let Some(seac) = seac { + self.draw_seac(seac, local_subrs, sink, ops_left)?; } Ok(true) } @@ -231,13 +258,23 @@ impl<'a> Cff<'a> { local_subrs: Index<'a>, sink: &mut S, component: Option<(f64, f64)>, + ops_left: Option<&mut u32>, ) -> Result> { let cs = self.char_strings.get(gid)?; let mut interp = Interp::new(self.global_subrs, local_subrs, sink, false); if let Some((x, y)) = component { interp.start_seac_component(x, y); } - interp.run(cs, 0)?; + let ran = match ops_left { + Some(ops_left) => { + interp.limit_ops(*ops_left); + let ran = interp.run(cs, 0); + *ops_left = ops_left.saturating_sub(interp.ops()); + ran + } + None => interp.run(cs, 0), + }; + ran?; // A well-formed CFF1 charstring has already closed its last // contour at endchar; this only covers charstrings that end // without one. @@ -254,6 +291,7 @@ impl<'a> Cff<'a> { seac: Seac, local_subrs: Index<'a>, sink: &mut S, + mut ops_left: Option<&mut u32>, ) -> Result<()> { if let FontDicts::Cid { .. } = self.fonts { return Err(Error::Unsupported { @@ -263,8 +301,10 @@ impl<'a> Cff<'a> { let n_glyphs = self.char_strings.len(); let base = charset::glyph_for_sid(self.data, self.charset, seac.base, n_glyphs)?; let accent = charset::glyph_for_sid(self.data, self.charset, seac.accent, n_glyphs)?; - self.run_charstring(base, local_subrs, sink, Some((0.0, 0.0)))?; - self.run_charstring(accent, local_subrs, sink, Some((seac.adx, seac.ady)))?; + let origin = Some((0.0, 0.0)); + self.run_charstring(base, local_subrs, sink, origin, ops_left.as_deref_mut())?; + let accent_origin = Some((seac.adx, seac.ady)); + self.run_charstring(accent, local_subrs, sink, accent_origin, ops_left)?; Ok(()) } diff --git a/src/tables/cff/charstring.rs b/src/tables/cff/charstring.rs index dc40fa9..bcc6607 100644 --- a/src/tables/cff/charstring.rs +++ b/src/tables/cff/charstring.rs @@ -25,7 +25,11 @@ const CFF2_STACK_LIMIT: usize = 513; /// the work: a subroutine that calls the next one many times, ten /// levels deep, runs for an exponential number of steps. Real glyphs /// stay far below this limit. -const MAX_CHARSTRING_OPS: u32 = 100_000; +pub(crate) const MAX_CHARSTRING_OPS: u32 = 100_000; + +/// The `context` of the error a charstring fails with past its +/// operation limit. +pub(crate) const OUT_OF_CHARSTRING_OPS: &str = "CFF charstring exceeds operation limit"; // ---------------------------------------------------------------------------- // Type 2 charstring interpreter. @@ -89,8 +93,11 @@ pub(crate) struct Interp<'a, 'b, S: CharstringSink> { /// next move operator, endchar, or [`Self::finish`] closes it. in_contour: bool, /// Operands and operators executed so far, checked against - /// [`MAX_CHARSTRING_OPS`]. + /// `ops_limit`. ops: u32, + /// [`MAX_CHARSTRING_OPS`], or less for a caller that bounds many + /// charstrings together (see [`Self::limit_ops`]). + ops_limit: u32, /// CFF2 blend support. pub(crate) blend: Option>, /// Region count and scalars for the blend context's current @@ -170,6 +177,7 @@ impl<'a, 'b, S: CharstringSink> Interp<'a, 'b, S> { done: false, in_contour: false, ops: 0, + ops_limit: MAX_CHARSTRING_OPS, blend: None, blend_regions: None, seac: None, @@ -191,6 +199,19 @@ impl<'a, 'b, S: CharstringSink> Interp<'a, 'b, S> { self.seac.take() } + /// Lowers the operation limit to `limit`, if that is below + /// [`MAX_CHARSTRING_OPS`]; past it the charstring fails with + /// [`OUT_OF_CHARSTRING_OPS`]. + pub(crate) fn limit_ops(&mut self, limit: u32) { + self.ops_limit = limit.min(MAX_CHARSTRING_OPS); + } + + /// Operands and operators executed so far, the one that crossed the + /// limit included. + pub(crate) fn ops(&self) -> u32 { + self.ops + } + pub(crate) fn run(&mut self, code: &'a [u8], depth: u8) -> Result<()> { if depth > MAX_SUBR_DEPTH { return Err(Error::Malformed { @@ -204,10 +225,10 @@ impl<'a, 'b, S: CharstringSink> Interp<'a, 'b, S> { return Ok(()); } self.ops += 1; - if self.ops > MAX_CHARSTRING_OPS { + if self.ops > self.ops_limit { return Err(Error::Malformed { offset: r.position(), - context: "CFF charstring exceeds operation limit", + context: OUT_OF_CHARSTRING_OPS, }); } let b0 = r.read_u8()?; @@ -869,6 +890,16 @@ impl<'a, 'b, S: CharstringSink> Interp2<'a, 'b, S> { self.inner.run(code, depth) } + /// See [`Interp::limit_ops`]. + pub(crate) fn limit_ops(&mut self, limit: u32) { + self.inner.limit_ops(limit); + } + + /// See [`Interp::ops`]. + pub(crate) fn ops(&self) -> u32 { + self.inner.ops() + } + /// Closes the final contour. See [`Interp::finish`]. pub(crate) fn finish(&mut self) { self.inner.finish(); diff --git a/src/tables/cff2.rs b/src/tables/cff2.rs index 3af09f5..0f597b5 100644 --- a/src/tables/cff2.rs +++ b/src/tables/cff2.rs @@ -31,7 +31,9 @@ use alloc::vec::Vec; use core::cell::{OnceCell, RefCell}; use crate::error::{Error, Result}; -use crate::tables::cff::{read_index2, BlendContext, CharstringSink, FdSelect, Index, RegionCache}; +use crate::tables::cff::{ + read_index2, BlendContext, CharstringSink, FdSelect, Index, RegionCache, MAX_CHARSTRING_OPS, +}; use crate::tables::outline::OutlineSink; use crate::tables::parse::Reader; use crate::tables::variation_store::ItemVariationStore; @@ -157,6 +159,22 @@ impl<'a> Cff2<'a> { coords: &[f32], shared: &Cff2Shared<'a>, sink: &mut S, + ) -> Result { + let mut ops = MAX_CHARSTRING_OPS; + self.outline_limited(glyph_id, coords, shared, sink, &mut ops) + } + + /// [`Cff2::outline_shared`] for a caller that bounds many glyphs + /// together: the charstring may run at most `ops_left` operations + /// (and never more than one glyph may), which it takes from + /// `ops_left`, failing with the operation-limit error past them. + pub(crate) fn outline_limited( + &self, + glyph_id: u16, + coords: &[f32], + shared: &Cff2Shared<'a>, + sink: &mut S, + ops_left: &mut u32, ) -> Result { let gid = usize::from(glyph_id); if gid >= self.char_strings.len() { @@ -207,7 +225,10 @@ impl<'a> Cff2<'a> { }); let mut interp = crate::tables::cff::Interp2::new(self.global_subrs, private.local_subrs, sink, blend); - interp.run(cs, 0)?; + interp.limit_ops(*ops_left); + let ran = interp.run(cs, 0); + *ops_left = ops_left.saturating_sub(interp.ops()); + ran?; // CFF2 has no endchar, so the last contour is still open when // the charstring runs out. Close it here. interp.finish(); @@ -264,6 +285,18 @@ pub(crate) struct Cff2Shared<'a> { regions: RegionCache, } +impl Cff2Shared<'_> { + /// Regions times axes of the variation store read so far: what + /// working out every region scalar at one coord vector costs, at + /// most. Zero before a glyph has read the store, or without one. + pub(crate) fn scalar_work(&self) -> usize { + match self.ivs.get() { + Some(Ok(Some(ivs))) => usize::from(ivs.region_count()) * usize::from(ivs.axis_count()), + _ => 0, + } + } +} + // ---------------------------------------------------------------------------- // CFF2 Top DICT. // ---------------------------------------------------------------------------- diff --git a/src/tables/glyf.rs b/src/tables/glyf.rs index 7cb5236..1b18346 100644 --- a/src/tables/glyf.rs +++ b/src/tables/glyf.rs @@ -513,6 +513,42 @@ impl<'a> Glyf<'a> { self.outline_with(&cx, glyph_id, None, sink) } + /// [`Glyf::outline_at_coords`] for a caller that draws many glyphs + /// for one result and bounds them together: the walk takes its + /// `gvar` tuple work from `work` (see [`MAX_TUPLE_WORK`]) and leaves + /// the rest there, and returns, with whether it drew, the glyph + /// records it visited and the points it laid down, which its own + /// caps bound. Draws into `sink` only once the whole glyph is + /// flattened, so a glyph that fails sends no ops. + #[allow(clippy::too_many_arguments)] + pub(crate) fn outline_at_coords_with_work( + &self, + loca: &Loca<'_>, + glyph_id: u16, + gvar: Option<&Gvar<'_>>, + coords: &[f32], + metrics: Option<&PhantomMetrics<'_>>, + sink: &mut S, + work: &mut usize, + ) -> Result<(bool, usize, usize)> { + let cx = FlattenCtx { + loca, + metrics, + var: Variation::new(gvar, coords), + }; + let mut budget = FlattenBudget::new(); + budget.work = (*work).min(MAX_TUPLE_WORK); + let flat = self.flatten_root(&cx, glyph_id, None, &mut budget); + *work = budget.work; + let visits = (MAX_FLATTEN_GLYPHS - budget.glyphs) as usize; + let points = MAX_FLATTEN_POINTS - budget.points; + let Some(flat) = flat? else { + return Ok((false, visits, points)); + }; + flat.emit(sink); + Ok((true, visits, points)) + } + /// The points [`Glyf::outline_at_coords`] draws `glyph_id` from, /// contour points on and off the curve, handed to `visit` without /// drawing the outline: what a box of the outline needs, since the diff --git a/src/tables/gvar.rs b/src/tables/gvar.rs index 51cc4cf..27fb698 100644 --- a/src/tables/gvar.rs +++ b/src/tables/gvar.rs @@ -576,12 +576,16 @@ const PHANTOM_COUNT: usize = 4; /// few hundred points over at most a few hundred tuples. pub(crate) const MAX_TUPLE_WORK: usize = 1 << 24; +/// The `context` of the error a walk fails with when its tuple work +/// runs out. +pub(crate) const OUT_OF_TUPLE_WORK: &str = "gvar variation work exceeds the cap"; + /// Takes `cost` units from `work`, or fails with the byte offset of the /// tuple data that would overspend it. fn charge(work: &mut usize, cost: usize, offset: usize) -> Result<()> { *work = work.checked_sub(cost).ok_or(Error::Malformed { offset, - context: "gvar variation work exceeds the cap", + context: OUT_OF_TUPLE_WORK, })?; Ok(()) } diff --git a/src/tables/varc.rs b/src/tables/varc.rs index 48e845d..7796e97 100644 --- a/src/tables/varc.rs +++ b/src/tables/varc.rs @@ -56,7 +56,7 @@ use alloc::vec::Vec; use crate::error::{Error, Result}; use crate::tables::layout::Coverage; use crate::tables::multi_var_store::{decode_tuple_values, read_cff2_index, MultiVarStore}; -use crate::tables::parse::{hb_roundf, Reader}; +use crate::tables::parse::{abs_f64, floor_f64, hb_roundf, Reader}; /// A parsed `VARC` table. #[derive(Debug, Clone)] @@ -306,12 +306,19 @@ impl<'a> Varc<'a> { /// 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`. + /// + /// Reading stops once the composite holds `max_components`: a walk + /// that may draw `n` more components passes `n + 1`, since a longer + /// list fails the walk anyway, so a record of a million components + /// costs no more than the walk can use. A composite cut short that + /// way is only kept for a walk that is about to fail. pub(crate) fn resolve( &self, gid: u16, coords: &[f32], font_coords: &[f32], memo: &mut VarcMemo, + max_components: usize, ) -> Option> { if !self.covers(gid) { return None; @@ -322,7 +329,8 @@ impl<'a> Varc<'a> { return Some(Rc::clone(known)); } } - let composite = Rc::new(self.composite_at(gid, coords, font_coords, id, memo)?); + let composite = + Rc::new(self.composite_at(gid, (coords, font_coords), id, memo, max_components)?); if let Some(id) = id { memo.per_coords[id] .composites @@ -345,19 +353,20 @@ impl<'a> Varc<'a> { return None; } let id = memo.coords_id(coords); - self.composite_at(gid, coords, font_coords, id, memo) + self.composite_at(gid, (coords, font_coords), id, memo, usize::MAX) } - /// [`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. + /// [`Self::composite_in`] at the glyph's coords and the font's, with + /// what the walk keeps for the glyph's coords at index `id`, or with + /// nothing kept when the walk had no work left to keep a copy of + /// them. Stops reading once the list holds `max_components`. fn composite_at( &self, gid: u16, - coords: &[f32], - font_coords: &[f32], + (coords, font_coords): (&[f32], &[f32]), id: Option, memo: &mut VarcMemo, + max_components: usize, ) -> Option { let idx = self.coverage.index_of(gid)? as usize; // HarfBuzz reads a record past the end of the INDEX as empty. @@ -374,7 +383,7 @@ impl<'a> Varc<'a> { }; let mut composite = VarcComposite::default(); let mut r = Reader::new(raw); - while !r.is_empty() { + while !r.is_empty() && composite.components.len() < max_components { // 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 @@ -910,24 +919,110 @@ impl Default for Decomposed { } impl Decomposed { - /// The affine the boring-expansion spec composes: + /// The affine HarfBuzz's `hb_transform_decomposed_t::to_transform` + /// builds, op for op in `f32`, with the angles in radians as + /// HarfBuzz takes them (half-turns times `f32` π): /// /// ```text - /// T(tx + tcx, ty + tcy) * R(rotation * π) * S(sx, sy) * - /// Skew(-skewX * π, skewY * π) * T(-tcx, -tcy) + /// translate(tx + tcx, ty + tcy), rotate(rotation), + /// scale(sx, sy), skew(-skewX, skewY), translate(-tcx, -tcy) /// ``` + /// + /// Returned in `[xx, xy, yx, yy, tx, ty]` order. fn to_affine(&self) -> [f32; 6] { - compose_affine( - self.tx, - self.ty, - self.rotation, - self.sx, - self.sy, - self.skew_x, - self.skew_y, - self.tcx, - self.tcy, - ) + let pi = core::f32::consts::PI; + let mut t = Affine::IDENTITY; + t.translate(self.tx + self.tcx, self.ty + self.tcy); + t.rotate(self.rotation * pi); + t.scale(self.sx, self.sy); + t.skew(-(self.skew_x * pi), self.skew_y * pi); + t.translate(-self.tcx, -self.tcy); + [t.xx, t.xy, t.yx, t.yy, t.x0, t.y0] + } +} + +/// HarfBuzz's `hb_transform_t`, with the operations +/// [`Decomposed::to_affine`] uses, each skipping the cases HarfBuzz +/// skips and summing in HarfBuzz's order. +#[derive(Clone, Copy)] +struct Affine { + xx: f32, + yx: f32, + xy: f32, + yy: f32, + x0: f32, + y0: f32, +} + +impl Affine { + const IDENTITY: Self = Self { + xx: 1.0, + yx: 0.0, + xy: 0.0, + yy: 1.0, + x0: 0.0, + y0: 0.0, + }; + + fn translate(&mut self, x: f32, y: f32) { + if x == 0.0 && y == 0.0 { + return; + } + self.x0 += self.xx * x + self.xy * y; + self.y0 += self.yx * x + self.yy * y; + } + + /// `self` times `b`: `b` applied first. + fn multiply(&mut self, b: Self) { + let a = *self; + *self = Self { + xx: a.xx * b.xx + a.xy * b.yx, + yx: a.yx * b.xx + a.yy * b.yx, + xy: a.xx * b.xy + a.xy * b.yy, + yy: a.yx * b.xy + a.yy * b.yy, + x0: a.xx * b.x0 + a.xy * b.y0 + a.x0, + y0: a.yx * b.x0 + a.yy * b.y0 + a.y0, + }; + } + + fn rotate(&mut self, radians: f32) { + if radians == 0.0 { + return; + } + let (s, c) = sincos(radians); + self.multiply(Self { + xx: c, + yx: s, + xy: -s, + yy: c, + x0: 0.0, + y0: 0.0, + }); + } + + fn scale(&mut self, sx: f32, sy: f32) { + if sx == 1.0 && sy == 1.0 { + return; + } + self.xx *= sx; + self.yx *= sx; + self.xy *= sy; + self.yy *= sy; + } + + fn skew(&mut self, skew_x: f32, skew_y: f32) { + if skew_x == 0.0 && skew_y == 0.0 { + return; + } + let tan_or_zero = |radians: f32| if radians == 0.0 { 0.0 } else { tan(radians) }; + self.multiply(Self { + xx: 1.0, + yx: tan_or_zero(skew_y), + xy: tan_or_zero(skew_x), + yy: 1.0, + x0: 0.0, + y0: 0.0, + }); } } @@ -1089,100 +1184,71 @@ fn decode_axis_indices(data: &[u8]) -> Vec { out } -/// Builds the affine matrix from VARC's transform fields. Matches the -/// boring-expansion-spec composition order: -/// -/// ```text -/// T(tx + tcx, ty + tcy) * R(rotation * π) * -/// S(sx, sy) * Skew(-skewX * π, skewY * π) * T(-tcx, -tcy) -/// ``` -/// -/// Returned in `[xx, xy, yx, yy, tx_eff, ty_eff]` row-major form. -#[allow(clippy::too_many_arguments)] -fn compose_affine( - tx: f32, - ty: f32, - rotation: f32, - sx: f32, - sy: f32, - skew_x: f32, - skew_y: f32, - tcx: f32, - tcy: f32, -) -> [f32; 6] { - // core has no sin or cos without std, so `sincos_pi` evaluates a - // Taylor polynomial instead of calling libm. - let (cos_r, sin_r) = sincos_pi(rotation); - let (cos_skx, sin_skx) = sincos_pi(-skew_x); - let (cos_sky, sin_sky) = sincos_pi(skew_y); - - // Skew matrix: - // [ 1, tan(skewY*π) ] - // [ tan(-skewX*π), 1 ] - // Implemented as cos/sin to avoid blowing up at ±π/2. - // Practically, skews in fonts stay well clear of ±π/2 so the - // tan form is fine; we compute via sin/cos for stability. - let tan_skx = sin_skx / cos_skx; - let tan_sky = sin_sky / cos_sky; - - // Build M = R * S * Skew. S applied to skew first: - // [ sx, 0 ] [ 1, tan_skx ] [ sx, sx*tan_skx ] - // [ 0, sy ] * [ tan_sky, 1 ] = [ sy*tan_sky, sy ] - let m_xx = sx; - let m_xy = sx * tan_skx; - let m_yx = sy * tan_sky; - let m_yy = sy; - - // Rotation * M: - // [ cos, -sin ] [ m_xx, m_xy ] - // [ sin, cos ] * [ m_yx, m_yy ] - let r_xx = cos_r * m_xx - sin_r * m_yx; - let r_xy = cos_r * m_xy - sin_r * m_yy; - let r_yx = sin_r * m_xx + cos_r * m_yx; - let r_yy = sin_r * m_xy + cos_r * m_yy; - - // Effective translation: - // p' = R*M*(p - tcenter) + (tcenter + translate) - // tx_eff = -(r_xx*tcx + r_xy*tcy) + tcx + tx - // ty_eff = -(r_yx*tcx + r_yy*tcy) + tcy + ty - let tx_eff = -(r_xx * tcx + r_xy * tcy) + tcx + tx; - let ty_eff = -(r_yx * tcx + r_yy * tcy) + tcy + ty; - - [r_xx, r_xy, r_yx, r_yy, tx_eff, ty_eff] +/// `sinf` and `cosf` of `radians`, as HarfBuzz's `hb_sincos` takes +/// them from libm: worked out in `f64` and rounded to `f32`, within an +/// `f32` rounding of the exact values at any angle a glyph uses. `core` +/// has no sine or cosine without `std`. An angle that is not finite +/// gives NaN. +fn sincos(radians: f32) -> (f32, f32) { + let (s, c) = sincos_f64(f64::from(radians)); + (s as f32, c as f32) } -/// `sin(x*π)` and `cos(x*π)`. Tight enough for glyph composites. -/// VARC's F4.12 fields stay within `[-8, 8)`, but variation deltas can -/// push an angle anywhere, including infinity. Implemented via a -/// Taylor series for `no_std`-friendliness. -fn sincos_pi(x: f32) -> (f32, f32) { - // Reduce to [-1, 1] (i.e. [-π, π]). - let mut t = x; - // For large or infinite angles the loops below would run for a - // long time or forever, so take the remainder first. `%` is exact - // and so is each loop step below 2^25, so both paths give the same - // angle wherever the loops finish. Clearing the sign of a zero - // remainder matches what the loops produce. - // A range check instead of `abs`, which core lacks before Rust 1.85. - if !(-16.0..=16.0).contains(&t) { - t %= 2.0; - if t == 0.0 { - t = 0.0; - } - } - while t > 1.0 { - t -= 2.0; +/// `tanf` of `radians`, from [`sincos_f64`] as [`sincos`] works it out. +fn tan(radians: f32) -> f32 { + let (s, c) = sincos_f64(f64::from(radians)); + (s / c) as f32 +} + +/// `sin(x)` and `cos(x)`: `x` brought into `[-π/4, π/4]` by the nearest +/// quarter turn (π/2 in two parts, so the remainder keeps its +/// precision), a Taylor polynomial there, accurate to about 1e-14, and +/// the quarter turn put back by swapping and negating. Deltas can push +/// an angle anywhere: past 2^30 radians it is first taken modulo 2π, +/// so the quarter-turn count stays exact and no loop can spin. +fn sincos_f64(x: f64) -> (f64, f64) { + const FRAC_2_PI: f64 = core::f64::consts::FRAC_2_PI; + // π/2 = PIO2_HI + PIO2_LO. + const PIO2_HI: f64 = core::f64::consts::FRAC_PI_2; + const PIO2_LO: f64 = 6.123_233_995_736_766e-17; + if !x.is_finite() { + return (f64::NAN, f64::NAN); } - while t < -1.0 { - t += 2.0; + let x = if abs_f64(x) > 1_073_741_824.0 { + x % core::f64::consts::TAU + } else { + x + }; + let quarter = floor_f64(x * FRAC_2_PI + 0.5); + let r = (x - quarter * PIO2_HI) - quarter * PIO2_LO; + let z = r * r; + // sin r = r - r^3/3! + ... - r^15/15!; cos r = 1 - r^2/2! + ... + r^16/16!. + let sin = r + * (1.0 + + z * (-1.0 / 6.0 + + z * (1.0 / 120.0 + + z * (-1.0 / 5040.0 + + z * (1.0 / 362_880.0 + + z * (-1.0 / 39_916_800.0 + + z * (1.0 / 6_227_020_800.0 + + z * (-1.0 / 1_307_674_368_000.0)))))))); + let cos = 1.0 + + z * (-0.5 + + z * (1.0 / 24.0 + + z * (-1.0 / 720.0 + + z * (1.0 / 40_320.0 + + z * (-1.0 / 3_628_800.0 + + z * (1.0 / 479_001_600.0 + + z * (-1.0 / 87_178_291_200.0 + + z * (1.0 / 20_922_789_888_000.0)))))))); + // `quarter` is a whole number below 2^30 in size, so the cast is + // exact, and its low two bits pick the quadrant. + match (quarter as i64) & 3 { + 0 => (sin, cos), + 1 => (cos, -sin), + 2 => (-sin, -cos), + _ => (-cos, sin), } - // y = t * π - let y = t * core::f32::consts::PI; - // 9-term Taylor expansion good to ~1e-6 over [-π, π]. - let y2 = y * y; - let sin = y * (1.0 - y2 / 6.0 + y2 * y2 / 120.0 - y2 * y2 * y2 / 5040.0); - let cos = 1.0 - y2 / 2.0 + y2 * y2 / 24.0 - y2 * y2 * y2 / 720.0 + y2 * y2 * y2 * y2 / 40320.0; - (cos, sin) } #[cfg(test)] diff --git a/src/tables/varc/tests.rs b/src/tables/varc/tests.rs index 54a4ea4..58f206a 100644 --- a/src/tables/varc/tests.rs +++ b/src/tables/varc/tests.rs @@ -270,17 +270,88 @@ fn rotation_rotates_unit_vector() { } #[test] -fn sincos_pi_huge_angle_terminates() { - // Reduction used to subtract 2 until the angle fell below 1, - // which never ends for infinity or for values where x - 2 == x. - let (c, s) = sincos_pi(1.0e30); - assert_eq!((c, s), (1.0, 0.0)); - assert!(sincos_pi(f32::INFINITY).0.is_nan()); - assert!(sincos_pi(f32::NEG_INFINITY).1.is_nan()); - // The remainder path agrees with the loop path, sign of zero - // included. - assert_eq!(sincos_pi(21.25), sincos_pi(1.25)); - assert_eq!(sincos_pi(-18.0).1.to_bits(), sincos_pi(0.0).1.to_bits()); +fn sincos_holds_over_every_quadrant() { + // Angles in every quadrant, negative, and many turns out, against + // their exact sines and cosines. + use core::f64::consts::PI; + let (half, root3) = (0.5, 0.866_025_403_784_438_6); + let root2 = core::f64::consts::FRAC_1_SQRT_2; + let cases = [ + (PI / 6.0, half, root3), + (PI / 4.0, root2, root2), + (PI / 2.0, 1.0, 0.0), + (2.0 * PI / 3.0, root3, -half), + (3.0 * PI / 4.0, root2, -root2), + (PI, 0.0, -1.0), + (7.0 * PI / 6.0, -half, -root3), + (3.0 * PI / 2.0, -1.0, 0.0), + (-3.0 * PI / 4.0, -root2, -root2), + (-PI / 3.0, -root3, half), + (100.0 * PI + PI / 3.0, root3, half), + (-1000.0 * PI - PI / 6.0, -half, root3), + ]; + for (x, sin, cos) in cases { + let (s, c) = sincos_f64(x); + assert!( + (s - sin).abs() < 1e-12 && (c - cos).abs() < 1e-12, + "{x}: {s} {c}" + ); + } +} + +#[test] +fn sincos_rounds_like_libm() { + // HarfBuzz takes 180 degrees as f32 π radians, a little past π, and + // libm's sinf of it is -8.742278e-8, which the f64 work rounds to. + let (s, c) = sincos(core::f32::consts::PI); + assert_eq!((s, c), (-8.742_278e-8, -1.0)); + assert_eq!(sincos(0.0), (0.0, 1.0)); + assert_eq!(tan(core::f32::consts::FRAC_PI_4), 1.0); +} + +#[test] +fn sincos_of_any_angle_terminates() { + // Deltas can push an angle anywhere: far out it is taken modulo 2π, + // and a value that is not finite gives NaN, with no loop to spin. + for x in [1.0e30, -3.0e38, 1.0e10, f32::MAX] { + let (s, c) = sincos(x); + assert!( + (-1.0..=1.0).contains(&s) && (-1.0..=1.0).contains(&c), + "{x}" + ); + } + for x in [f32::INFINITY, f32::NEG_INFINITY, f32::NAN] { + let (s, c) = sincos(x); + assert!(s.is_nan() && c.is_nan(), "{x}"); + } +} + +#[test] +fn half_and_whole_turns_compose_as_harfbuzz_does() { + // 180 degrees about a center far from the origin. HarfBuzz rotates + // by f32 π, whose sine is -8.742278e-8, not 0, so the image of the + // center's opposite point is off by about 1e-4 there; the old + // Taylor series left sine 0.075 off at this angle. + let flags = VC_HAVE_ROTATION | VC_HAVE_TCENTER_X | VC_HAVE_TCENTER_Y; + let mut record = vec![0x80 | (flags >> 8) as u8, flags as u8, 0x00, 0x05]; + record.extend_from_slice(&4096i16.to_be_bytes()); // rotation: one half-turn + record.extend_from_slice(&(-400i16).to_be_bytes()); + record.extend_from_slice(&(-300i16).to_be_bytes()); + let c = first(&record, None, None, &[]); + let t = c.components[0].transform; + let (s, k) = sincos(core::f32::consts::PI); + // translate(tcx, tcy), rotate, translate(-tcx, -tcy), in HarfBuzz's + // operations. + let mut want = Affine::IDENTITY; + want.translate(-400.0, -300.0); + want.rotate(core::f32::consts::PI); + want.translate(400.0, 300.0); + assert_eq!(t, [want.xx, want.xy, want.yx, want.yy, want.x0, want.y0]); + assert_eq!((t[0], t[2]), (k, s)); + assert!( + (t[4] - -800.0).abs() < 1e-3 && (t[5] - -600.0).abs() < 1e-3, + "{t:?}" + ); } /// A MultiItemVariationStore with one axis-free region (scalar 1 @@ -916,18 +987,20 @@ fn a_memo_keeps_composites_and_scalars_by_coords() { 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(); + let first = varc.resolve(1, &[1.0], &[], &mut memo, usize::MAX).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(); + let again = varc.resolve(1, &[1.0], &[], &mut memo, usize::MAX).unwrap(); assert!(Rc::ptr_eq(&first, &again)); assert_eq!(memo.work_done(), 2005); - let half = varc.resolve(1, &[0.5], &[], &mut memo).unwrap(); + let half = varc.resolve(1, &[0.5], &[], &mut memo, usize::MAX).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!(varc + .resolve(5, &[0.25], &[], &mut memo, usize::MAX) + .is_none()); assert_eq!(memo.work_done(), 2 * 2005); } @@ -975,3 +1048,20 @@ fn coords_past_harfbuzz_limit_start_from_the_font_coords() { .unwrap(); assert_eq!(c.components[0].coords.len(), 4096); } + +#[test] +fn a_walk_reads_no_more_components_than_it_can_draw() { + // 100,000 three-byte components of glyph 5. A walk that can draw + // two more components asks for three, the third failing it, so the + // other 99,997 records are never read or kept. + let record = [0x00, 0x00, 0x05].repeat(100_000); + let table = build_varc(&[1], &[&record], None, None); + let varc = Varc::parse(&table).unwrap(); + let mut memo = VarcMemo::new(); + let c = varc.resolve(1, &[], &[], &mut memo, 3).unwrap(); + assert_eq!(c.components.len(), 3); + // Keeping the empty coords (1) and three records (3). + assert_eq!(memo.work_done(), 4); + // Without a cap, the list holds every record. + assert_eq!(varc.composite(1, &[]).unwrap().components.len(), 100_000); +} diff --git a/tests/fixtures/varc_face.expected b/tests/fixtures/varc_face.expected index d99fccf..fc3061c 100644 --- a/tests/fixtures/varc_face.expected +++ b/tests/fixtures/varc_face.expected @@ -40,7 +40,14 @@ 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 default 43 rot_90 M -700 100 L -700 200 L -800 200 L -800 100 Z +outline default 44 rot_135 M -894.9747 -229.2893 L -965.6854 -158.5786 L -1036.3961 -229.2893 L -965.6854 -300 Z +outline default 45 rot_180 M -800 -600 L -900 -600 L -900 -700 L -800 -700 Z +outline default 46 rot_270 M -100 -700 L -100 -800 L 0 -800 L 0 -700 Z +outline default 47 rot_m135 M -470.7107 -794.9747 L -541.4214 -865.6854 L -470.7107 -936.3961 L -400 -865.6854 Z +outline default 48 rot_var M -894.9747 -229.2893 L -1036.3961 -87.868 L -1177.8175 -370.7107 Z +outline default 49 skew_big M -614.8947 413.0479 L -514.8947 275.3652 L -822.342 375.3652 L -922.342 513.0479 Z +outline default 50 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 @@ -81,7 +88,14 @@ outline AAAA=50,BBBB=-30,CCCC=70 39 x_user M 250 -14.0002 L 450 0 L 321.9995 300 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=50,BBBB=-30,CCCC=70 43 rot_90 M -700 100 L -700 225 L -800 225 L -842.0007 85.9997 Z +outline AAAA=50,BBBB=-30,CCCC=70 44 rot_135 M -894.9747 -229.2893 L -983.3631 -140.901 L -1054.0737 -211.6116 L -985.4847 -339.5987 Z +outline AAAA=50,BBBB=-30,CCCC=70 45 rot_180 M -800 -600 L -925 -600 L -925 -700 L -785.9998 -742.0007 Z +outline AAAA=50,BBBB=-30,CCCC=70 46 rot_270 M -100 -700 L -100 -825 L 0 -825 L 42.0007 -685.9998 Z +outline AAAA=50,BBBB=-30,CCCC=70 47 rot_m135 M -470.7107 -794.9747 L -559.0991 -883.3631 L -488.3884 -954.0738 L -360.4014 -885.4847 Z +outline AAAA=50,BBBB=-30,CCCC=70 48 rot_var M -1371.5125 -313.1891 L -1561.646 -249.587 L -1558.194 -575.7345 Z +outline AAAA=50,BBBB=-30,CCCC=70 49 skew_big M -614.8947 413.0479 L -489.8947 240.9446 L -797.342 340.9446 L -1065.4724 574.3245 Z +outline AAAA=50,BBBB=-30,CCCC=70 50 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 @@ -122,7 +136,14 @@ outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 39 x_user M 250 0 L 490.0195 0 L 350 353. 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=33.3,BBBB=66.7,CCCC=-12.5 43 rot_90 M -700 103.75 L -700 220.4004 L -832.2325 225.9532 L -826.6797 103.75 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 44 rot_135 M -897.6264 -226.6377 L -980.1107 -144.1534 L -1077.5397 -233.7294 L -987.2025 -316.2137 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 45 rot_180 M -803.75 -600 L -920.4004 -600 L -925.9532 -732.2325 L -803.75 -726.6797 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 46 rot_270 M -100 -703.75 L -100 -820.4004 L 32.2325 -825.9532 L 26.6797 -703.75 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 47 rot_m135 M -473.3623 -797.6264 L -555.8466 -880.1107 L -466.2706 -977.5396 L -383.7863 -887.2025 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 48 rot_var M -1214.5609 -274.393 L -1422.3932 -154.3301 L -1477.9086 -530.3439 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 49 skew_big M -611.1447 407.8848 L -494.4943 247.2775 L -895.4869 371.8647 L -1000.618 534.5645 Z +outline AAAA=33.3,BBBB=66.7,CCCC=-12.5 50 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 @@ -163,7 +184,14 @@ outline AAAA=-80,BBBB=90,CCCC=55 39 x_user M 250 -10.9998 L 504.0015 0 L 328.000 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=-80,BBBB=90,CCCC=55 43 rot_90 M -700 100 L -700 200 L -836.001 200 L -869.0002 89.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 44 rot_135 M -894.9747 -229.2893 L -965.6854 -158.5786 L -1061.8527 -254.7458 L -1006.6979 -356.5685 Z +outline AAAA=-80,BBBB=90,CCCC=55 45 rot_180 M -800 -600 L -900 -600 L -900 -736.001 L -789.0002 -769.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 46 rot_270 M -100 -700 L -100 -800 L 36.001 -800 L 69.0002 -689.0002 Z +outline AAAA=-80,BBBB=90,CCCC=55 47 rot_m135 M -470.7107 -794.9747 L -541.4214 -865.6854 L -445.2542 -961.8527 L -343.4315 -906.6979 Z +outline AAAA=-80,BBBB=90,CCCC=55 48 rot_var M -887.1967 -221.5113 L -1074.5809 -49.6832 L -1213.1746 -437.1797 Z +outline AAAA=-80,BBBB=90,CCCC=55 49 skew_big M -614.8947 413.0479 L -514.8947 275.3652 L -933.0261 411.3662 L -1145.4812 597.1929 Z +outline AAAA=-80,BBBB=90,CCCC=55 50 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 @@ -204,7 +232,14 @@ outline AAAA=70,BBBB=-100,CCCC=20 39 x_user M 250 -4.0002 L 450 0 L 341.9995 300 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=70,BBBB=-100,CCCC=20 43 rot_90 M -700 100 L -700 235.0006 L -800 235.0006 L -812.0007 95.9997 Z +outline AAAA=70,BBBB=-100,CCCC=20 44 rot_135 M -894.9747 -229.2893 L -990.4346 -133.8295 L -1061.1453 -204.5401 L -971.3426 -311.3144 Z +outline AAAA=70,BBBB=-100,CCCC=20 45 rot_180 M -800 -600 L -935.0006 -600 L -935.0006 -700 L -795.9998 -712.0007 Z +outline AAAA=70,BBBB=-100,CCCC=20 46 rot_270 M -100 -700 L -100 -835.0006 L 0 -835.0006 L 12.0007 -695.9998 Z +outline AAAA=70,BBBB=-100,CCCC=20 47 rot_m135 M -470.7107 -794.9747 L -566.1705 -890.4346 L -495.4598 -961.1453 L -388.6856 -871.3426 Z +outline AAAA=70,BBBB=-100,CCCC=20 48 rot_var M -1564.9563 -410.7827 L -1760.3646 -367.9853 L -1725.3787 -684.9081 Z +outline AAAA=70,BBBB=-100,CCCC=20 49 skew_big M -614.8947 413.0479 L -479.8941 227.1755 L -787.3414 327.1755 L -963.2383 530.5562 Z +outline AAAA=70,BBBB=-100,CCCC=20 50 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 @@ -245,7 +280,14 @@ 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=100,BBBB=100,CCCC=-100 43 rot_90 M -700 130 L -700 280 L -865 305 L -840 130 Z +outline AAAA=100,BBBB=100,CCCC=-100 44 rot_135 M -916.1879 -208.0761 L -1022.254 -102.0101 L -1156.6042 -201.005 L -1015.1829 -307.071 Z +outline AAAA=100,BBBB=100,CCCC=-100 45 rot_180 M -830 -600 L -980 -600 L -1005 -765 L -830 -740 Z +outline AAAA=100,BBBB=100,CCCC=-100 46 rot_270 M -100 -730 L -100 -880 L 65 -905 L 40 -730 Z +outline AAAA=100,BBBB=100,CCCC=-100 47 rot_m135 M -491.9239 -816.1879 L -597.9899 -922.254 L -498.995 -1056.6042 L -392.929 -915.1829 Z +outline AAAA=100,BBBB=100,CCCC=-100 48 rot_var M -1824 -600.0001 L -2084 -600.0001 L -1924 -980.0001 Z +outline AAAA=100,BBBB=100,CCCC=-100 49 skew_big M -584.8947 371.7431 L -434.8947 165.2191 L -917.1829 295.7985 L -1015.321 511.7431 Z +outline AAAA=100,BBBB=100,CCCC=-100 50 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 @@ -286,7 +328,14 @@ 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=25,BBBB=0,CCCC=0 43 rot_90 M -700 100 L -700 212.5 L -800 212.5 L -800 100 Z +outline AAAA=25,BBBB=0,CCCC=0 44 rot_135 M -894.9747 -229.2893 L -974.5242 -149.7398 L -1045.2349 -220.4505 L -965.6854 -300 Z +outline AAAA=25,BBBB=0,CCCC=0 45 rot_180 M -800 -600 L -912.5 -600 L -912.5 -700 L -800 -700 Z +outline AAAA=25,BBBB=0,CCCC=0 46 rot_270 M -100 -700 L -100 -812.5 L 0 -812.5 L 0 -700 Z +outline AAAA=25,BBBB=0,CCCC=0 47 rot_m135 M -470.7107 -794.9747 L -550.2602 -874.5242 L -479.5495 -945.2349 L -400 -865.6854 Z +outline AAAA=25,BBBB=0,CCCC=0 48 rot_var M -1133.687 -256.0999 L -1299.981 -144.9858 L -1383.505 -449.9837 Z +outline AAAA=25,BBBB=0,CCCC=0 49 skew_big M -614.8947 413.0479 L -502.3947 258.1549 L -809.842 358.1549 L -922.342 513.0479 Z +outline AAAA=25,BBBB=0,CCCC=0 50 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 @@ -327,4 +376,11 @@ outline AAAA=-50,BBBB=10,CCCC=60 39 x_user M 250 -12.0007 L 456.0022 0 L 325.998 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 +outline AAAA=-50,BBBB=10,CCCC=60 43 rot_90 M -700 100 L -700 200 L -804.0015 200 L -840.0037 87.9993 Z +outline AAAA=-50,BBBB=10,CCCC=60 44 rot_135 M -894.9747 -229.2893 L -965.6854 -158.5786 L -1039.2256 -232.1188 L -985.4865 -336.7726 Z +outline AAAA=-50,BBBB=10,CCCC=60 45 rot_180 M -800 -600 L -900 -600 L -900 -704.0015 L -787.9993 -740.0037 Z +outline AAAA=-50,BBBB=10,CCCC=60 46 rot_270 M -100 -700 L -100 -800 L 4.0015 -800 L 40.0037 -687.9993 Z +outline AAAA=-50,BBBB=10,CCCC=60 47 rot_m135 M -470.7107 -794.9747 L -541.4214 -865.6854 L -467.8813 -939.2256 L -363.2274 -885.4865 Z +outline AAAA=-50,BBBB=10,CCCC=60 48 rot_var M -886.489 -220.8035 L -1040.6403 -83.6238 L -1166.5048 -393.3412 Z +outline AAAA=-50,BBBB=10,CCCC=60 49 skew_big M -614.8947 413.0479 L -514.8947 275.3652 L -834.6445 379.3667 L -1057.333 569.5745 Z +outline AAAA=-50,BBBB=10,CCCC=60 50 x_norecord diff --git a/tests/fixtures/varc_face.ttf b/tests/fixtures/varc_face.ttf index d1f5344..e7dfa7a 100644 Binary files a/tests/fixtures/varc_face.ttf 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 index 608a13e..d88126a 100644 --- a/tests/tools/build_varc_face_fixtures.py +++ b/tests/tools/build_varc_face_fixtures.py @@ -21,7 +21,9 @@ - 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 default instance too, since its font holds a zero per axis there; +- rotations of 90, 135, 180, 270 and -135 degrees about a center 500 + units away, one varied past a half turn, and a skew of 72 degrees. The VARC table is written byte by byte with the encoders of build_varc_morx_parity_fixtures.py. tests/tools/varc_face_expected.py @@ -117,6 +119,7 @@ def vi(outer, inner): bytes([0x00, 5]), # 0: RA's value only b"", # 1: nothing tv([2, -3]), # 2: full + tv([1024, -512]), # 3: a rotation, F4.12 units ]), # outer 3: axis values, regions RB, RAN. ([RB, RAN], [ @@ -167,6 +170,8 @@ def vi(outer, inner): "x_user", # a region without axes, which holds at the default instance too "k_const", "k_cond", "k_outer", + # rotations past a quarter turn about a far center, and a large skew + "rot_90", "rot_135", "rot_180", "rot_270", "rot_m135", "rot_var", "skew_big", ] # Covered by the VARC coverage, past the glyph records: last in the # glyph order, so last in the coverage. @@ -258,6 +263,14 @@ def make(g): 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)) + # Rotations about a center 500 units away, where an angle off by a + # little moves the box a lot; one varies past a half turn. + for name, rot in [("rot_90", 2048), ("rot_135", 3072), ("rot_180", 4096), + ("rot_270", 6144), ("rot_m135", -3072)]: + rec[name] = C(box, fields=dict(ROT=rot, CX=-400, CY=-300)) + rec["rot_var"] = C(tri, transform_var=vi(2, 3), fields=dict(ROT=3072, CX=-400, CY=-300)) + rec["skew_big"] = C(box, fields=dict(SKX=1638, SKY=-1229, CX=300, CY=-200)) + # 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)) diff --git a/tests/varc_parity.rs b/tests/varc_parity.rs index f86e8aa..653c06d 100644 --- a/tests/varc_parity.rs +++ b/tests/varc_parity.rs @@ -9,14 +9,15 @@ //! RESET_UNSPECIFIED_AXES at the top and one level down, and an axis //! index past HarfBuzz's 4096-axis limit. //! -//! `tests/fixtures/varc_face.ttf` has the same axes and 41 glyphs the +//! `tests/fixtures/varc_face.ttf` has the same axes and 48 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. +//! composites, one glyph reached many times at the same coords, a region +//! without axes, which applies at the default instance too, and +//! rotations past a quarter turn about a far center. //! //! Each `.expected` file holds HarfBuzz 14.5.0's outline of each glyph //! at eight locations, which every glyph must match through @@ -249,10 +250,10 @@ fn varc_outlines_match_harfbuzz() { 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. + // axes, large rotations. `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 + 48 * 8 ); } diff --git a/tests/varc_unparsable.rs b/tests/varc_unparsable.rs new file mode 100644 index 0000000..0e46e21 --- /dev/null +++ b/tests/varc_unparsable.rs @@ -0,0 +1,216 @@ +//! A `VARC` table sigilbuzz cannot parse counts as absent. +//! +//! HarfBuzz 14.5.0 drops a `VARC` its sanitizer rejects and draws and +//! shapes the font from `glyf` or CFF. After 14.5.0, HarfBuzz and the +//! VARC spec moved the MultiItemVariationStore to a new layout (a 32-bit +//! region count, and an Offset32 to each MultiVarData's delta-set +//! INDEX), so fonts built to the updated spec carry a store this parser +//! rejects. Before, every glyph of such a font failed to draw, and +//! shaping failed (fallback mark positioning) or fell back to the +//! ascender (vertical origins), where HarfBuzz 14.5.0 ignores the table. +//! +//! The tests splice a `VARC` table with a new-layout store into a CFF +//! font and a `glyf` font, and check that every glyph draws, and every +//! run shapes, exactly as the font without `VARC`. + +use sigilbuzz::tables::{Outline, OutlineSink, PathOp}; +use sigilbuzz::{shape, Blob, Buffer, Direction, Face, Font}; + +const CFF_FONT: &[u8] = include_bytes!("fonts/SourceCodePro-Latin-Subset.otf"); +const GLYF_FONT: &[u8] = include_bytes!("fixtures/varc_parity.ttf"); + +/// A `VARC` table covering `covered`, each glyph one component naming +/// glyph 1, over a MultiItemVariationStore in the layout HarfBuzz +/// adopted after 14.5.0: `u32` region count, and an Offset32 from each +/// MultiVarData to its delta-set INDEX. +fn new_layout_varc(covered: &[u16]) -> Vec { + let mut coverage = vec![0, 1]; + coverage.extend_from_slice(&(covered.len() as u16).to_be_bytes()); + for g in covered { + coverage.extend_from_slice(&g.to_be_bytes()); + } + + let mut store = Vec::new(); + store.extend_from_slice(&1u16.to_be_bytes()); // format + store.extend_from_slice(&12u32.to_be_bytes()); // region list + store.extend_from_slice(&1u16.to_be_bytes()); // one MultiVarData + store.extend_from_slice(&30u32.to_be_bytes()); // at 30 + // SparseVarRegionList at 12: u32 count, Offset32 per region. + store.extend_from_slice(&1u32.to_be_bytes()); + store.extend_from_slice(&8u32.to_be_bytes()); + // The region at 20: axis 0, peak +1. + store.extend_from_slice(&1u16.to_be_bytes()); + store.extend_from_slice(&0u16.to_be_bytes()); + store.extend_from_slice(&0i16.to_be_bytes()); + store.extend_from_slice(&0x4000i16.to_be_bytes()); + store.extend_from_slice(&0x4000i16.to_be_bytes()); + assert_eq!(store.len(), 30); + // MultiVarData at 30: format, region indexes, Offset32 to the + // delta-set INDEX right after it. + store.push(1); + store.extend_from_slice(&1u16.to_be_bytes()); + store.extend_from_slice(&0u16.to_be_bytes()); + store.extend_from_slice(&9u32.to_be_bytes()); + // One delta set: one i8, 100. + store.extend_from_slice(&1u32.to_be_bytes()); + store.extend_from_slice(&[1, 1, 3, 0x00, 100]); + + // One record per covered glyph: TRANSFORM_HAS_VARIATION and + // HAVE_TRANSLATE_X, glyph 1, variation index 0, translate 0. + let record = [0x18, 0x00, 0x01, 0x00, 0x00, 0x00]; + let mut records = (covered.len() as u32).to_be_bytes().to_vec(); + records.push(1); // offSize + for i in 0..=covered.len() { + records.push((1 + i * record.len()) as u8); + } + for _ in covered { + records.extend_from_slice(&record); + } + + let mut out = Vec::new(); + out.extend_from_slice(&[0, 1, 0, 0]); // version 1.0 + let mut at = 24u32; + for part in [&coverage, &store] { + out.extend_from_slice(&at.to_be_bytes()); + at += part.len() as u32; + } + out.extend_from_slice(&0u32.to_be_bytes()); // no conditions + out.extend_from_slice(&0u32.to_be_bytes()); // no axis indices + out.extend_from_slice(&at.to_be_bytes()); // glyph records + out.extend_from_slice(&coverage); + out.extend_from_slice(&store); + out.extend_from_slice(&records); + out +} + +/// `font` with its tables rewritten into a new directory, `VARC` +/// replaced by `varc`, or dropped when `varc` is `None`. +fn with_varc(font: &[u8], varc: Option<&[u8]>) -> Vec { + let face = Face::parse_bytes(font, 0).unwrap(); + let mut tables: Vec<([u8; 4], &[u8])> = face + .records() + .iter() + .filter(|r| &r.tag != b"VARC") + .map(|r| (r.tag, face.table_bytes(r.tag).unwrap())) + .collect(); + if let Some(varc) = varc { + tables.push((*b"VARC", varc)); + } + tables.sort_by_key(|t| t.0); + let header = 12 + 16 * tables.len(); + let mut out = Vec::new(); + out.extend_from_slice(&font[..4]); + out.extend_from_slice(&(tables.len() as u16).to_be_bytes()); + out.extend_from_slice(&[0; 6]); + 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(&((header + body.len()) 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); + } + } + out.extend_from_slice(&body); + out +} + +#[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); + } +} + +/// Every glyph of `with` draws, through every outline path, exactly as +/// the same glyph of `without` does. +fn assert_outlines_match(with: &Face<'_>, without: &Face<'_>, coords: &[f32]) { + let n = without.maxp().unwrap().num_glyphs; + let (a, b) = (with.glyph_outlines(coords), without.glyph_outlines(coords)); + for gid in 0..n { + let want = without.glyph_outline_at_coords(gid, coords).unwrap(); + assert_eq!( + with.glyph_outline_at_coords(gid, coords).unwrap(), + want, + "glyph {gid}" + ); + assert_eq!( + a.outline(gid).unwrap(), + b.outline(gid).unwrap(), + "glyph {gid}" + ); + assert_eq!(a.outline(gid).unwrap(), want, "glyph {gid}"); + let mut sink = Recorder::default(); + let drew = a.draw(gid, &mut sink).unwrap(); + assert_eq!(drew.then_some(sink.0), want, "glyph {gid}"); + if coords.is_empty() { + assert_eq!(with.glyph_outline(gid).unwrap(), want, "glyph {gid}"); + } + } +} + +#[test] +fn a_cff_font_with_an_unparsable_varc_draws_and_shapes_without_it() { + let varc = new_layout_varc(&[1, 2, 3]); + let with = with_varc(CFF_FONT, Some(&varc)); + let without = with_varc(CFF_FONT, None); + let (with_blob, without_blob) = (Blob::new(&with), Blob::new(&without)); + let with = Face::parse(&with_blob, 0).unwrap(); + let without = Face::parse(&without_blob, 0).unwrap(); + // The table is there, and this parser rejects its store. + assert!(with.varc().is_err()); + assert_outlines_match(&with, &without, &[]); + + // "á" decomposes to a and U+0301, which the font lacks: fallback + // mark positioning reads glyph extents. A vertical run without + // VORG or vmtx centers the extents for the origins. + for (text, direction) in [("\u{e1}b", Direction::Ltr), ("abc", Direction::Ttb)] { + let run = |face: &Face<'_>| { + let font = Font::new(face.clone(), 1000.0); + let mut buffer = Buffer::new(); + buffer.set_direction(direction); + buffer.push_str(text); + shape(&font, &buffer, &[]).unwrap().glyphs + }; + assert_eq!(run(&with), run(&without), "{text:?} {direction:?}"); + } +} + +#[test] +fn a_glyf_font_with_an_unparsable_varc_draws_without_it() { + // varc_parity.ttf with its VARC table swapped for one in the new + // layout: glyphs 3 and up draw their (empty) glyf outlines. + let varc = new_layout_varc(&[3, 4, 5]); + let with = with_varc(GLYF_FONT, Some(&varc)); + let without = with_varc(GLYF_FONT, None); + let (with_blob, without_blob) = (Blob::new(&with), Blob::new(&without)); + let with = Face::parse(&with_blob, 0).unwrap(); + let without = Face::parse(&without_blob, 0).unwrap(); + assert!(with.varc().is_err()); + for coords in [&[][..], &[0.5, -0.25, 0.75][..]] { + assert_outlines_match(&with, &without, coords); + } +}