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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
*.otf binary
*.woff binary
*.woff2 binary
*.jpg binary
*.raw binary
82 changes: 73 additions & 9 deletions crates/sigilbuzz-paint/src/deltas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,41 @@
//! bits, `inner` in the low 16 bits.
//!
//! No other table is consulted; GDEF's variation store belongs to GDEF
//! and GPOS. The sentinel `0xFFFFFFFF` and an empty coordinate slice
//! both mean "no delta".
//! and GPOS. The sentinel `0xFFFFFFFF` means "no delta".
//!
//! The coordinates are rounded to F2DOT14 first, multiples of 1/16384
//! with halves rounded up, the precision HarfBuzz stores a font's
//! coordinates in. Shaping and `Face::glyph_outline_at_coords` round
//! them the same way, so a paint and the outlines it fills vary at the
//! same instance. Coordinates that all round to zero, or none at all,
//! are the default instance and mean "no delta".
//!
//! Both [`crate::evaluate_with`] and [`crate::walk`] read deltas through
//! the same [`Deltas`] value, so they never disagree about units or index
//! mapping.

use alloc::vec::Vec;

use sigilbuzz::tables::colr::{ClipBox, Colr, VarIndexBase};
use sigilbuzz::tables::variation_store::ItemVariationStore;

/// `varIndexBase` value meaning "this record does not vary".
const NO_VARIATION: VarIndexBase = VarIndexBase::MAX;

/// Variation deltas for one evaluation: COLR's item variation store,
/// its optional index map, and the normalized coordinates.
pub(crate) struct Deltas<'a, 'c> {
/// its optional index map, and the normalized coordinates rounded to
/// F2DOT14.
pub(crate) struct Deltas<'a> {
store: Option<ItemVariationStore<'a>>,
map: Option<DeltaSetIndexMap<'a>>,
coords: &'c [f32],
coords: Vec<f32>,
}

impl<'a, 'c> Deltas<'a, 'c> {
impl<'a> Deltas<'a> {
/// Reads the variation store and index map named by `colr`'s header.
/// An offset that does not lead to a well-formed structure is
/// treated as absent, which is what HarfBuzz's sanitizer does to it.
pub(crate) fn new(colr: &Colr<'a>, coords: &'c [f32]) -> Self {
pub(crate) fn new(colr: &Colr<'a>, coords: &[f32]) -> Self {
let data = colr.data();
let store = colr
.var_store_offset()
Expand All @@ -47,7 +56,11 @@ impl<'a, 'c> Deltas<'a, 'c> {
let map = colr
.var_index_map_offset()
.and_then(|off| DeltaSetIndexMap::parse(data, off as usize));
Self { store, map, coords }
Self {
store,
map,
coords: f2dot14_coords(coords),
}
}

/// The raw delta for field `field` of a record whose base index is
Expand All @@ -62,7 +75,7 @@ impl<'a, 'c> Deltas<'a, 'c> {
};
let index = base.wrapping_add(u32::from(field));
let index = self.map.as_ref().map_or(index, |map| map.map(index));
store.delta((index >> 16) as u16, index as u16, self.coords)
store.delta((index >> 16) as u16, index as u16, &self.coords)
}

/// Delta for an F2DOT14 field, as a fraction: 8192 ticks is 0.5.
Expand Down Expand Up @@ -96,6 +109,40 @@ impl<'a, 'c> Deltas<'a, 'c> {
}
}

/// `coords` rounded to F2DOT14 as HarfBuzz stores a font's coords: each
/// a multiple of 1/16384, rounded halves up (`floor(x * 16384 + 0.5)`),
/// with NaN read as zero. Empty when every coordinate rounds to zero,
/// the default instance. The same rule as the core crate's shaping and
/// outline paths.
fn f2dot14_coords(coords: &[f32]) -> Vec<f32> {
let rounded: Vec<f32> = coords.iter().map(|&c| round_f2dot14(c)).collect();
if rounded.iter().all(|&c| c == 0.0) {
return Vec::new();
}
rounded
}

/// One coordinate rounded to a multiple of 1/16384, halves up. Written
/// without `f32::floor`, which `core` lacks before Rust 1.85.
fn round_f2dot14(c: f32) -> f32 {
if c.is_nan() {
return 0.0;
}
let x = c * 16384.0 + 0.5;
// Every f32 of magnitude 2^23 or more is a whole number already.
let floor = if (-8_388_608.0..8_388_608.0).contains(&x) {
let t = x as i32 as f32;
if t > x {
t - 1.0
} else {
t
}
} else {
x
};
floor / 16384.0
}

/// A borrowed `DeltaSetIndexMap` (format 0 or 1):
///
/// ```text
Expand Down Expand Up @@ -181,6 +228,23 @@ mod tests {
out
}

#[test]
fn coords_round_to_f2dot14_halves_up() {
// A 16.16 coord `k / 65536` lands on `((k + 2) >> 2) / 16384`, as
// HarfBuzz rounds it.
for k in -70_000i32..=70_000 {
let got = f2dot14_coords(&[k as f32 / 65536.0, 1.0]);
assert_eq!(got[0], ((k + 2) >> 2) as f32 / 16384.0, "k = {k}");
}
assert!(f2dot14_coords(&[1.0 / 65536.0, -2.0 / 65536.0]).is_empty());
assert!(f2dot14_coords(&[f32::NAN, 0.0]).is_empty());
assert!(f2dot14_coords(&[]).is_empty());
assert_eq!(f2dot14_coords(&[f32::NAN, 0.5]), [0.0, 0.5]);
// Out-of-range and huge values keep their size.
assert_eq!(f2dot14_coords(&[3.0, -1e30]), [3.0, -1e30]);
assert_eq!(f2dot14_coords(&[f32::INFINITY]), [f32::INFINITY]);
}

#[test]
fn map_splits_entries_into_outer_and_inner() {
for format in [0, 1] {
Expand Down
3 changes: 2 additions & 1 deletion crates/sigilbuzz-paint/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ pub fn evaluate(face: &Face<'_>, gid: GlyphId) -> Vec<DrawCmd> {
/// every `PaintVar*` node visited. `coords` is the normalized axis
/// vector, the same shape sigilbuzz's
/// [`Face::glyph_outline_at_coords`](sigilbuzz::Face::glyph_outline_at_coords)
/// accepts. An empty slice is the static (no-deltas) path.
/// accepts. An empty slice is the static (no-deltas) path. The deltas
/// are read at the coords rounded to F2DOT14, as HarfBuzz reads them.
#[must_use]
pub fn evaluate_at_coords(face: &Face<'_>, gid: GlyphId, coords: &[f32]) -> Vec<DrawCmd> {
evaluate_with(face, gid, &EvalOptions::new().with_coords(coords))
Expand Down
5 changes: 4 additions & 1 deletion crates/sigilbuzz-paint/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ impl<'a> EvalOptions<'a> {
/// Evaluates `PaintVar*` deltas at `coords`, the normalized axis
/// vector (the same shape
/// [`Face::glyph_outline_at_coords`](sigilbuzz::Face::glyph_outline_at_coords)
/// accepts). An empty slice is the static path.
/// accepts). An empty slice is the static path. The deltas are read
/// at the coords rounded to F2DOT14 (multiples of 1/16384, halves
/// up), as HarfBuzz stores them and as the outlines are drawn;
/// [`EvalOptions::coords`] returns them as given.
///
/// ```
/// use sigilbuzz_paint::EvalOptions;
Expand Down
4 changes: 2 additions & 2 deletions crates/sigilbuzz-paint/src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ fn paint(
fn root_clip(
face: &Face<'_>,
colr: &Colr<'_>,
deltas: &Deltas<'_, '_>,
deltas: &Deltas<'_>,
glyph: GlyphId,
coords: &[f32],
) -> RootClip {
Expand All @@ -349,7 +349,7 @@ fn root_clip(

struct Walker<'a, 'b, 's> {
colr: &'b Colr<'a>,
deltas: Deltas<'a, 'b>,
deltas: Deltas<'a>,
/// COLR glyphs whose trees are on the walk stack.
glyphs: Vec<GlyphId>,
/// LayerList indices on the walk stack.
Expand Down
21 changes: 21 additions & 0 deletions crates/sigilbuzz-paint/tests/variable_units.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,24 @@ fn var_skew_angle_delta_is_f2dot14() {
assert!(close(t.xy, -1.0), "at +1: {t:?}");
assert!(close(t.yx, 0.0), "at +1: {t:?}");
}

#[test]
fn deltas_read_coords_rounded_to_f2dot14() {
// HarfBuzz stores a font's coords as F2DOT14 and resolves every
// PaintVar* delta there, so a coord between two F2DOT14 steps
// lands on the nearer one (halves up), as shaping and outlines do.
let bytes = font_bytes();
let face = Face::parse_bytes(&bytes, 0).expect("face parses");
let sx = |coord: f32| one_fill(&face, 2, &[coord]).0.xx;
// 0.25 + 1/65536, a 16.16 coord from `Fvar::normalize_coords`,
// rounds to 0.25: the scale is 1 + 0.5 * 0.25 exactly.
assert_eq!(sx(0.25 + 1.0 / 65536.0), 1.125);
assert_eq!(sx(0.25 - 1.0 / 65536.0), 1.125);
// Half a step rounds up.
assert_eq!(sx(1.0 / 32768.0), 1.0 + 0.5 / 16384.0);
assert_eq!(sx(3.0 / 32768.0), 1.0 + 0.5 * 2.0 / 16384.0);
// Less than half a step is the default instance.
assert_eq!(sx(1.0 / 65536.0), 1.0);
// NaN reads as zero.
assert_eq!(sx(f32::NAN), 1.0);
}
Loading
Loading