Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions crates/sigilbuzz-subset/tests/varc_round_trip/mvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ fn build_mvs_varc() -> Vec<u8> {
};

// 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<u16>, Vec<Vec<u8>>)> = vec![
(vec![0], (0..5).map(|i| vec![0x00_u8, i as u8]).collect()),
(
Expand Down Expand Up @@ -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],
);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod color;
mod glyph_outlines;
mod layout;
mod outlines;
mod varc_draw;
mod variations;

use alloc::vec::Vec;
Expand Down
37 changes: 6 additions & 31 deletions src/face/glyph_outlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -190,26 +185,6 @@ impl<'a> GlyphOutlines<'a> {
}
}

/// Sends the ops of `outline` to `sink`.
fn replay<S: OutlineSink>(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")
Expand Down
168 changes: 36 additions & 132 deletions src/face/outlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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<Option<GlyphBounds>> {
let loca = self.loca()?;
Expand Down Expand Up @@ -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<Option<Outline>> {
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<Option<Outline>> {
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<Option<Outline>> {
// 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
Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading