From d0a797278564d490a78be222c46d9de77fb8a7f4 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Sun, 20 Aug 2023 17:18:39 -0400 Subject: [PATCH 1/4] new CFF scaler --- Cargo.lock | 22 + Cargo.toml | 5 + src/scale/cff2/hint.rs | 1090 ++++++++++++++++++++++++++++++++++++++ src/scale/cff2/mod.rs | 132 +++++ src/scale/cff2/scaler.rs | 737 ++++++++++++++++++++++++++ src/scale/mod.rs | 41 +- 6 files changed, 2018 insertions(+), 9 deletions(-) create mode 100644 src/scale/cff2/hint.rs create mode 100644 src/scale/cff2/mod.rs create mode 100644 src/scale/cff2/scaler.rs diff --git a/Cargo.lock b/Cargo.lock index 926e1d8..afb1e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "font-test-data" +version = "0.1.0" +source = "git+https://github.com/googlefonts/fontations?rev=91ebdfd91bec9ae4ec34f6a7d5f01736b1b2eb6e#91ebdfd91bec9ae4ec34f6a7d5f01736b1b2eb6e" + +[[package]] +name = "font-types" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6978d65d61022aa249fefdd914dc8215757f617f1a697c496ef6b42013366567" + +[[package]] +name = "read-fonts" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d08214643b2df95b0b3955cd9f264bcfab22b73470b83df4992df523b4d6eb" +dependencies = [ + "font-types", +] + [[package]] name = "swash" version = "0.1.8" dependencies = [ + "font-test-data", + "read-fonts", "yazi", "zeno", ] diff --git a/Cargo.toml b/Cargo.toml index 765cd2d..93258bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,8 @@ render = ["scale", "zeno/eval"] [dependencies] yazi = { version = "0.1.6", optional = true } zeno = { version = "0.2.2", optional = true, default_features = false } +read-fonts = "0.10.0" + +[dev-dependencies] +font-test-data= { git = "https://github.com/googlefonts/fontations", rev = "91ebdfd91bec9ae4ec34f6a7d5f01736b1b2eb6e" } +read-fonts = { version = "0.10.0", features = ["scaler_test"] } diff --git a/src/scale/cff2/hint.rs b/src/scale/cff2/hint.rs new file mode 100644 index 0000000..99ccfc8 --- /dev/null +++ b/src/scale/cff2/hint.rs @@ -0,0 +1,1090 @@ +//! PostScript hinting. + +const TRACE: bool = false; + +use read_fonts::tables::postscript::{charstring::CommandSink, dict::Blues}; +use read_fonts::types::Fixed; + +// "Default values for OS/2 typoAscender/Descender.." +// See +const ICF_TOP: Fixed = Fixed::from_i32(880); +const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); + +// +const MAX_OTHER_BLUES: usize = 5; +const MAX_BLUE_ZONES: usize = 12; + +const MAX_STEM_HINTS: usize = 48; +const MAX_HINTS: usize = MAX_STEM_HINTS * 2; + +const HINT_MASK_SIZE: usize = (MAX_STEM_HINTS + 7) / 4; + +const EPSILON: Fixed = Fixed::from_bits(1); + +/// Parameters used to generate the stem and counter zones for the hinting +/// algorithm. +#[derive(Clone)] +pub(crate) struct HintParams { + pub blues: Blues, + pub family_blues: Blues, + pub other_blues: Blues, + pub family_other_blues: Blues, + pub blue_scale: Fixed, + pub blue_shift: Fixed, + pub blue_fuzz: Fixed, + pub language_group: i32, +} + +impl Default for HintParams { + fn default() -> Self { + Self { + blues: Blues::default(), + other_blues: Blues::default(), + family_blues: Blues::default(), + family_other_blues: Blues::default(), + // See + blue_scale: Fixed::from_f64(0.039625), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ONE, + language_group: 0, + } + } +} + +/// Hinting state for a PostScript subfont. +/// +/// Note that hinter states depend on the scale, subfont index and +/// variation coordinates of a glyph. They can be retained and reused +/// if those values remain the same. +#[derive(Copy, Clone)] +pub(crate) struct HintState { + scale: Fixed, + blue_scale: Fixed, + // These will be used in later code. + #[allow(dead_code)] + blue_shift: Fixed, + #[allow(dead_code)] + blue_fuzz: Fixed, + language_group: u8, + supress_overshoot: bool, + do_em_box_hints: bool, + boost: Fixed, + darken_y: Fixed, + zones: [BlueZone; MAX_BLUE_ZONES], + zone_count: usize, +} + +impl HintState { + pub fn new(params: &HintParams, scale: Fixed) -> Self { + let mut state = Self { + scale, + blue_scale: params.blue_scale, + blue_shift: params.blue_shift, + blue_fuzz: params.blue_fuzz, + language_group: params.language_group as u8, + supress_overshoot: false, + do_em_box_hints: false, + boost: Fixed::ZERO, + darken_y: Fixed::ZERO, + zones: [BlueZone::default(); MAX_BLUE_ZONES], + zone_count: 0, + }; + state.build_zones(params); + state + } + + #[cfg(test)] + fn zones(&self) -> &[BlueZone] { + &self.zones[..self.zone_count] + } + + /// Initialize zones from the set of blues values. + /// + /// See + fn build_zones(&mut self, params: &HintParams) { + self.do_em_box_hints = false; + // + match (self.language_group, params.blues.values().len()) { + (1, 2) => { + let blues = params.blues.values(); + if blues[0].0 < ICF_BOTTOM + && blues[0].1 < ICF_BOTTOM + && blues[1].0 > ICF_TOP + && blues[1].1 > ICF_TOP + { + // FreeType generates synthetic hints here. We'll do it + // later when building the hint map. + self.do_em_box_hints = true; + return; + } + } + (1, 0) => { + self.do_em_box_hints = true; + return; + } + _ => {} + } + let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; + let mut max_zone_height = Fixed::ZERO; + let mut zone_count = 0usize; + // Copy blues and other blues to a combined array of top and bottom zones. + for blue in params.blues.values() { + // FreeType loads blues as integers and then expands to 16.16 + // at initialization. We load them as 16.16 so floor them here + // to ensure we match. + // + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height + continue; + } + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_count]; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + if zone_count == 0 { + // First blue value is bottom zone + zone.is_bottom = true; + zone.cs_flat_edge = top; + } else { + // Adjust both edges of top zone upward by twice darkening amount + zone.cs_top_edge += twice(self.darken_y); + zone.cs_bottom_edge += twice(self.darken_y); + // Remaining blue values are top zones + zone.is_bottom = false; + zone.cs_flat_edge = zone.cs_bottom_edge; + } + zone_count += 1; + } + for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height + continue; + } + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_count]; + // All "other" blues are bottom zone + zone.is_bottom = true; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + zone.cs_flat_edge = top; + zone_count += 1; + } + // Adjust for family blues + let units_per_pixel = Fixed::ONE / self.scale; + for zone in &mut zones[..zone_count] { + let flat = zone.cs_flat_edge; + let mut min_diff = Fixed::MAX; + if zone.is_bottom { + // In a bottom zone, the top edge is the flat edge. + // Search family other blues for bottom zones. Look for the + // closest edge that is within the one pixel threshold. + for blue in params.family_other_blues.values() { + let family_flat = blue.1; + let diff = (flat - family_flat).abs(); + if diff < min_diff && diff < units_per_pixel { + zone.cs_flat_edge = family_flat; + min_diff = diff; + if diff == Fixed::ZERO { + break; + } + } + } + // Check the first member of family blues, which is a bottom + // zone + if !params.family_blues.values().is_empty() { + let family_flat = params.family_blues.values()[0].1; + let diff = (flat - family_flat).abs(); + if diff < min_diff && diff < units_per_pixel { + zone.cs_flat_edge = family_flat; + } + } + } else { + // In a top zone, the bottom edge is the flat edge. + // Search family blues for top zones, skipping the first, which + // is a bottom zone. Look for closest family edge that is + // within the one pixel threshold. + for blue in params.family_blues.values().iter().skip(1) { + let family_flat = blue.0 + twice(self.darken_y); + let diff = (flat - family_flat).abs(); + if diff < min_diff && diff < units_per_pixel { + zone.cs_flat_edge = family_flat; + min_diff = diff; + if diff == Fixed::ZERO { + break; + } + } + } + } + } + if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { + // Clamp at maximum scale + self.blue_scale = Fixed::ONE / max_zone_height; + } + // Suppress overshoot and boost blue zones at small sizes + if self.scale < self.blue_scale { + self.supress_overshoot = true; + self.boost = + Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); + // boost must remain less than 0.5, or baseline could go negative + self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); + } + if self.darken_y != Fixed::ZERO { + self.boost = Fixed::ZERO; + } + // Set device space alignment for each zone; apply boost amount before + // rounding flat edge + let scale = self.scale; + let boost = self.boost; + for zone in &mut zones[..zone_count] { + let boost = if zone.is_bottom { -boost } else { boost }; + zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); + } + self.zones = zones; + self.zone_count = zone_count; + } + + fn capture(&self, bottom: &mut Hint, top: &mut Hint) -> bool { + let fuzz = self.blue_fuzz; + let mut captured = false; + let mut adjustment = Fixed::ZERO; + for zone in &self.zones[..self.zone_count] { + if zone.is_bottom + && bottom.is_bottom() + && (zone.cs_bottom_edge - fuzz) <= bottom.coord + && bottom.coord <= (zone.cs_top_edge + fuzz) + { + adjustment = if self.supress_overshoot { + zone.ds_flat_edge + } else if zone.cs_top_edge - bottom.coord >= self.blue_shift { + bottom.ds_coord.round().min(zone.ds_flat_edge - Fixed::ONE) + } else { + bottom.ds_coord.round() + } - bottom.ds_coord; + captured = true; + break; + } + if !zone.is_bottom + && top.is_top() + && (zone.cs_bottom_edge - fuzz) <= top.coord + && top.coord <= (zone.cs_top_edge + fuzz) + { + adjustment = if self.supress_overshoot { + zone.ds_flat_edge + } else if top.coord - zone.cs_bottom_edge >= self.blue_shift { + top.ds_coord.round().max(zone.ds_flat_edge + Fixed::ONE) + } else { + top.ds_coord.round() + } - top.ds_coord; + captured = true; + break; + } + } + if captured { + if bottom.is_valid() { + bottom.ds_coord += adjustment; + bottom.lock(); + } + if top.is_valid() { + top.ds_coord += adjustment; + top.lock(); + } + } + captured + } +} + +pub(crate) struct Hinter<'a, S> { + state: &'a HintState, + sink: &'a mut S, + stem_hints: [StemHint; MAX_STEM_HINTS], + stem_count: u8, + mask: HintMask, + initial_map: HintMap, + map: HintMap, +} + +impl<'a, S: CommandSink> Hinter<'a, S> { + pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { + Self { + state, + sink, + stem_hints: [StemHint::default(); MAX_STEM_HINTS], + stem_count: 0, + mask: HintMask::all(), + initial_map: HintMap::new(), + map: HintMap::new(), + } + } + + fn hint(&mut self, coord: Fixed) -> Fixed { + if !self.map.valid { + self.build_hint_map(Some(self.mask), Fixed::ZERO); + } + Fixed::from_bits(self.map.apply(self.state.scale, coord).to_bits() >> 6) + } + + #[inline(always)] + fn scale(&self, coord: Fixed) -> Fixed { + Fixed::from_bits((coord * self.state.scale).to_bits() >> 6) + } + + fn add_stem(&mut self, min: Fixed, max: Fixed) { + let index = self.stem_count as usize; + if index >= MAX_STEM_HINTS || self.map.valid { + return; + } + let stem = &mut self.stem_hints[index]; + stem.min = min; + stem.max = max; + stem.used = false; + stem.ds_min = Fixed::ZERO; + stem.ds_max = Fixed::ZERO; + self.stem_count = index as u8 + 1; + } + + fn build_hint_map(&mut self, mask: Option, origin: Fixed) { + self.map.build( + self.state, + mask, + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + origin, + false, + ); + } +} + +impl<'a, S: CommandSink> CommandSink for Hinter<'a, S> { + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.add_stem(y, dy); + } + + fn hint_mask(&mut self, mask: &[u8]) { + let mut hint_mask = HintMask::new(); + hint_mask.set_mask(mask); + if TRACE { + //println!("Got hintmask: {:?}", mask); + } + if hint_mask != self.mask { + self.mask = hint_mask; + self.map.valid = false; + } + } + + fn counter_mask(&mut self, mask: &[u8]) { + let mut hint_mask = HintMask::new(); + hint_mask.set_mask(mask); + let mut map = HintMap::new(); + map.build( + self.state, + Some(hint_mask), + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + Fixed::ZERO, + false, + ); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + let x = self.scale(x); + let y = self.hint(y); + self.sink.move_to(x, y); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + let x = self.scale(x); + let y = self.hint(y); + self.sink.line_to(x, y); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + let cx1 = self.scale(cx1); + let cy1 = self.hint(cy1); + let cx2 = self.scale(cx2); + let cy2 = self.hint(cy2); + let x = self.scale(x); + let y = self.hint(y); + self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + self.sink.close(); + } +} + +/// See +#[derive(Copy, Clone, PartialEq, Default, Debug)] +struct BlueZone { + is_bottom: bool, + cs_bottom_edge: Fixed, + cs_top_edge: Fixed, + cs_flat_edge: Fixed, + ds_flat_edge: Fixed, +} + +#[derive(Copy, Clone, Default)] +struct StemHint { + used: bool, + min: Fixed, + max: Fixed, + ds_min: Fixed, + ds_max: Fixed, +} + +// Hint flags +const GHOST_BOTTOM: u8 = 0x1; +const GHOST_TOP: u8 = 0x2; +const PAIR_BOTTOM: u8 = 0x4; +const PAIR_TOP: u8 = 0x8; +const LOCKED: u8 = 0x10; +const SYNTHETIC: u8 = 0x20; + +/// See +#[derive(Copy, Clone, Default)] +struct Hint { + flags: u8, + index: u8, + coord: Fixed, + ds_coord: Fixed, + scale: Fixed, +} + +impl Hint { + fn is_valid(&self) -> bool { + self.flags != 0 + } + + fn is_bottom(&self) -> bool { + self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0 + } + + fn is_top(&self) -> bool { + self.flags & (GHOST_TOP | PAIR_TOP) != 0 + } + + fn is_pair(&self) -> bool { + self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0 + } + + fn is_pair_top(&self) -> bool { + self.flags & PAIR_TOP != 0 + } + + fn is_locked(&self) -> bool { + self.flags & LOCKED != 0 + } + + fn is_synthetic(&self) -> bool { + self.flags & SYNTHETIC != 0 + } + + fn lock(&mut self) { + self.flags |= LOCKED + } + + fn setup( + &mut self, + stem: &StemHint, + index: u8, + origin: Fixed, + scale: Fixed, + darken_y: Fixed, + is_bottom: bool, + ) { + let width = stem.max - stem.min; + if width == Fixed::from_i32(-21) { + if is_bottom { + self.coord = stem.max; + self.flags = GHOST_BOTTOM; + } else { + self.flags = 0; + } + } else if width == Fixed::from_i32(-20) { + if !is_bottom { + self.coord = stem.min; + self.flags = GHOST_TOP; + } else { + self.flags = 0; + } + } else if width < Fixed::ZERO { + if is_bottom { + self.coord = stem.max; + self.flags = PAIR_BOTTOM; + } else { + self.coord = stem.min; + self.flags = PAIR_TOP; + } + } else if is_bottom { + self.coord = stem.min; + self.flags = PAIR_BOTTOM; + } else { + self.coord = stem.max; + self.flags = PAIR_TOP; + } + if self.is_top() { + self.coord += twice(darken_y); + } + self.coord += origin; + self.scale = scale; + self.index = index; + if self.flags != 0 && stem.used { + if self.is_top() { + self.ds_coord = stem.ds_max; + } else { + self.ds_coord = stem.ds_min; + } + self.lock(); + } else { + self.ds_coord = self.coord * scale; + } + } +} + +#[derive(Copy, Clone)] +struct HintMap { + hints: [Hint; MAX_HINTS], + len: usize, + valid: bool, +} + +impl HintMap { + fn new() -> Self { + Self { + hints: [Hint::default(); MAX_HINTS], + len: 0, + valid: false, + } + } + + fn clear(&mut self) { + self.len = 0; + self.valid = false; + } + + fn apply(&self, scale: Fixed, coord: Fixed) -> Fixed { + if self.len == 0 { + return coord * scale; + } + let limit = self.len - 1; + let mut i = 0; + while i < limit && coord >= self.hints[i + 1].coord { + i += 1; + } + while i > 0 && coord < self.hints[i].coord { + i -= 1; + } + let hint = &self.hints[i]; + if i == 0 && coord < self.hints[0].coord { + ((coord - self.hints[0].coord) * scale) + self.hints[0].ds_coord + } else { + ((coord - hint.coord) * hint.scale) + hint.ds_coord + } + } + + fn insert(&mut self, bottom: &Hint, top: &Hint, scale: Fixed, initial: Option<&HintMap>) { + let (is_pair, mut first) = if !bottom.is_valid() { + (false, *top) + } else if !top.is_valid() { + (false, *bottom) + } else { + (true, *bottom) + }; + let mut second = *top; + if is_pair && top.coord < bottom.coord { + return; + } + let count = if is_pair { 2 } else { 1 }; + if self.len + count > MAX_HINTS { + return; + } + if TRACE { + // println!( + // " Got hint at {} ({})", + // first.coord.to_f64(), + // first.ds_coord.to_f64() + // ); + // if is_pair { + // println!( + // " Got hint at {} ({})", + // second.coord.to_f64(), + // second.ds_coord.to_f64() + // ); + // } + } + let mut insertion_index = 0; + while insertion_index < self.len { + if self.hints[insertion_index].coord >= first.coord { + break; + } + insertion_index += 1; + } + if insertion_index < self.len { + let current = &self.hints[insertion_index]; + if (current.coord == first.coord) + || (is_pair && current.coord <= second.coord) + || current.is_pair_top() + { + return; + } + } + if !first.is_locked() { + if let Some(initial) = initial { + if is_pair { + let mid = initial.apply(scale, half(second.coord + first.coord)); + let half = half(second.coord - first.coord) * scale; + first.ds_coord = mid - half; + second.ds_coord = mid + half; + } else { + first.ds_coord = initial.apply(scale, first.coord); + } + } + } + if insertion_index > 0 && first.ds_coord < self.hints[insertion_index - 1].ds_coord { + return; + } + if insertion_index < self.len + && ((is_pair && second.ds_coord > self.hints[insertion_index].ds_coord) + || first.ds_coord > self.hints[insertion_index].ds_coord) + { + return; + } + if insertion_index != self.len { + let mut src_index = self.len - 1; + let mut dst_index = self.len + count - 1; + loop { + self.hints[dst_index] = self.hints[src_index]; + if src_index == insertion_index { + break; + } + src_index -= 1; + dst_index -= 1; + } + } + self.hints[insertion_index] = first; + if is_pair { + self.hints[insertion_index + 1] = second; + } + if TRACE { + // println!( + // " Inserting hint at {} ({})", + // first.coord.to_f64(), + // first.ds_coord.to_f64() + // ); + // if is_pair { + // println!( + // " Inserting hint at {} ({})", + // second.coord.to_f64(), + // second.ds_coord.to_f64() + // ); + // } + } + self.len += count; + } + + fn adjust(&mut self) { + let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; + let mut saved_count = 0usize; + let mut i = 0; + let limit = self.len; + while i < limit { + let is_pair = self.hints[i].is_pair(); + let j = if is_pair { i + 1 } else { i }; + if !self.hints[i].is_locked() { + let frac_down = self.hints[i].ds_coord.fract(); + let frac_up = self.hints[j].ds_coord.fract(); + let down_move_down = Fixed::ZERO - frac_down; + let up_move_down = Fixed::ZERO - frac_up; + let down_move_up = if frac_down == Fixed::ZERO { + Fixed::ZERO + } else { + Fixed::ONE - frac_down + }; + let up_move_up = if frac_up == Fixed::ZERO { + Fixed::ZERO + } else { + Fixed::ONE - frac_up + }; + let move_up = down_move_up.min(up_move_up); + let move_down = down_move_down.max(up_move_down); + const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); + let mut save_edge = false; + let adjustment; + if j >= self.len - 1 + || self.hints[j + 1].ds_coord + >= (self.hints[j].ds_coord + move_up + MIN_COUNTER) + { + if i == 0 + || self.hints[i - 1].ds_coord + <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) + { + adjustment = if -move_down < move_up { + move_down + } else { + move_up + }; + } else { + adjustment = move_up; + } + } else if i == 0 + || self.hints[i - 1].ds_coord + <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) + { + adjustment = move_down; + save_edge = move_up < -move_down; + } else { + adjustment = Fixed::ZERO; + save_edge = true; + } + if save_edge && j < self.len - 1 && !self.hints[j + 1].is_locked() { + saved[saved_count] = (j, move_up - adjustment); + saved_count += 1; + } + self.hints[i].ds_coord += adjustment; + if is_pair { + self.hints[j].ds_coord += adjustment; + } + } + if i > 0 && self.hints[i].coord != self.hints[i - 1].coord { + let a = self.hints[i]; + let b = self.hints[i - 1]; + self.hints[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); + } + if is_pair { + if self.hints[j].coord != self.hints[j - 1].coord { + let a = self.hints[j]; + let b = self.hints[j - 1]; + self.hints[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); + } + i += 1; + } + i += 1; + } + for i in (0..saved_count).rev() { + let (j, adjustment) = saved[i]; + if self.hints[j + 1].ds_coord + >= (self.hints[j].ds_coord + adjustment + Fixed::from_f64(0.5)) + { + self.hints[j].ds_coord += adjustment; + if self.hints[j].is_pair() { + self.hints[j - 1].ds_coord += adjustment; + } + } + } + } + + fn build( + &mut self, + state: &HintState, + mask: Option, + mut initial_map: Option<&mut HintMap>, + stems: &mut [StemHint], + origin: Fixed, + initial: bool, + ) { + let scale = state.scale; + let darken_y = Fixed::ZERO; + if !initial { + if let Some(ref mut initial_map) = initial_map { + if !initial_map.valid { + initial_map.build(state, None, None, stems, origin, true); + } + } + } + let initial_map = initial_map.map(|x| x as &HintMap); + self.clear(); + let mut mask = mask.unwrap_or_else(HintMask::all); + if !mask.valid { + mask = HintMask::all(); + } + if state.do_em_box_hints { + let mut bottom = Hint::default(); + bottom.coord = ICF_BOTTOM - EPSILON; + bottom.ds_coord = (bottom.coord * scale).round() - Fixed::from_f64(0.5); + bottom.scale = scale; + bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; + let mut top = Hint::default(); + top.coord = ICF_TOP + EPSILON + twice(state.darken_y); + top.ds_coord = (top.coord * scale).round() + Fixed::from_f64(0.5); + top.scale = scale; + top.flags = GHOST_TOP | LOCKED | SYNTHETIC; + let invalid = Hint::default(); + self.insert(&bottom, &invalid, scale, initial_map); + self.insert(&invalid, &top, scale, initial_map); + } + let mut tmp_mask = mask; + for (i, stem) in stems.iter().enumerate() { + if !tmp_mask.get(i) { + continue; + } + let mut bottom = Hint::default(); + let mut top = Hint::default(); + bottom.setup(stem, i as u8, origin, scale, darken_y, true); + top.setup(stem, i as u8, origin, scale, darken_y, false); + if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { + if initial { + self.insert(&bottom, &top, scale, None); + } else { + self.insert(&bottom, &top, scale, initial_map); + } + tmp_mask.clear(i); + } + } + if initial { + if self.len == 0 + || self.hints[0].coord > Fixed::ZERO + || self.hints[self.len - 1].coord < Fixed::ZERO + { + let edge = Hint { + flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, + scale, + ..Default::default() + }; + let invalid = Hint::default(); + self.insert(&edge, &invalid, scale, None); + } + } else { + for (i, stem) in stems.iter().enumerate() { + if !tmp_mask.get(i) { + continue; + } + let mut bottom = Hint::default(); + let mut top = Hint::default(); + bottom.setup(stem, i as u8, origin, scale, darken_y, true); + top.setup(stem, i as u8, origin, scale, darken_y, false); + self.insert(&bottom, &top, scale, initial_map); + } + } + self.dump(); + self.adjust(); + self.dump(); + if !initial { + for i in 0..self.len { + let hint = &self.hints[i]; + if hint.is_synthetic() { + continue; + } + let stem = &mut stems[hint.index as usize]; + if hint.is_top() { + stem.ds_max = hint.ds_coord; + } else { + stem.ds_min = hint.ds_coord; + } + stem.used = true; + } + self.valid = true; + } + self.valid = true; + } + + fn dump(&self) { + // if !TRACE { + // return; + // } + // for i in 0..self.len { + // let hint = self.hints[i]; + // println!( + // "[{}] {} {} {} {}{}{}{}{}{}", + // hint.index, + // hint.coord.to_f64(), + // hint.ds_coord.to_f64() / hint.scale.to_f64(), + // hint.scale.to_f64() * 65536., + // if hint.is_pair() { "p" } else { "" }, + // if hint.flags & (GHOST_BOTTOM | GHOST_TOP) != 0 { + // "g" + // } else { + // "" + // }, + // if hint.is_top() { "t" } else { "" }, + // if hint.is_bottom() { "b" } else { "" }, + // if hint.is_locked() { "L" } else { "" }, + // if hint.is_synthetic() { "S" } else { "" }, + // ); + // } + // println!("-------------------------------"); + } +} + +#[derive(Copy, Clone, PartialEq, Default)] +pub struct HintMask { + mask: [u8; HINT_MASK_SIZE], + valid: bool, +} + +impl HintMask { + pub fn new() -> Self { + Self { + mask: [0u8; HINT_MASK_SIZE], + valid: false, + } + } + + pub fn all() -> Self { + Self { + mask: [0xFF; HINT_MASK_SIZE], + valid: true, + } + } + + fn clear_all(&mut self) { + self.mask = [0u8; HINT_MASK_SIZE]; + self.valid = true; + } + + pub fn set_mask(&mut self, mask: &[u8]) { + self.clear_all(); + if mask.len() > HINT_MASK_SIZE { + return; + } + for (i, b) in mask.iter().enumerate() { + self.mask[i] = *b; + } + self.valid = true; + } + + #[inline] + #[allow(dead_code)] + pub fn set(&mut self, bit: usize) { + self.mask[bit >> 3] |= 1 << (7 - (bit & 0x7)); + } + + #[inline] + pub fn clear(&mut self, bit: usize) { + self.mask[bit >> 3] &= !(1 << (7 - (bit & 0x7))); + } + + #[inline] + pub fn get(&self, bit: usize) -> bool { + self.mask[bit >> 3] & (1 << (7 - (bit & 0x7))) != 0 + } + + // pub fn dump(&self) { + // for i in 0..MAX_STEM_HINTS { + // print!("{}", self.get(i) as u8); + // } + // println!(); + // for b in &self.mask { + // print!("{:#8b}", *b) + // } + // println!(); + // } +} + +fn half(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() / 2) +} + +fn twice(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits().wrapping_mul(2)) +} + +#[cfg(test)] +mod tests { + use super::{BlueZone, Blues, Fixed, HintParams, HintState}; + + // #[test] + // fn noto_serif_display_blue_zones() { + // fn make_blues(values: &[f64]) -> Blues { + // Blues::new(values.iter().copied().map(Fixed::from_f64)) + // } + // // + // // + // // + // // + // // + // let params = HintParams { + // blues: make_blues(&[ + // -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, + // ]), + // other_blues: make_blues(&[-255.0, -240.0]), + // blue_scale: Fixed::from_f64(0.05), + // blue_shift: Fixed::from_i32(7), + // blue_fuzz: Fixed::ZERO, + // ..Default::default() + // }; + // let state = HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)); + // assert!(!state.do_em_box_hints); + // assert_eq!(state.zone_count, 6); + // assert_eq!(state.boost, Fixed::from_bits(27035)); + // assert!(state.supress_overshoot); + // // FreeType generates the following zones: + // let expected_zones = &[ + // // csBottomEdge -983040 int + // // csTopEdge 0 int + // // csFlatEdge 0 int + // // dsFlatEdge 0 int + // // bottomZone 1 '\x1' unsigned char + // BlueZone { + // cs_bottom_edge: Fixed::from_bits(-983040), + // is_bottom: true, + // ..Default::default() + // }, + // // csBottomEdge 35127296 int + // // csTopEdge 35848192 int + // // csFlatEdge 35127296 int + // // dsFlatEdge 589824 int + // // bottomZone 0 '\0' unsigned char + // BlueZone { + // cs_bottom_edge: Fixed::from_bits(35127296), + // cs_top_edge: Fixed::from_bits(35848192), + // cs_flat_edge: Fixed::from_bits(35127296), + // ds_flat_edge: Fixed::from_bits(589824), + // is_bottom: false, + // }, + // // csBottomEdge 37421056 int + // // csTopEdge 38141952 int + // // csFlatEdge 37421056 int + // // dsFlatEdge 589824 int + // // bottomZone 0 '\0' unsigned char + // BlueZone { + // cs_bottom_edge: Fixed::from_bits(37421056), + // cs_top_edge: Fixed::from_bits(38141952), + // cs_flat_edge: Fixed::from_bits(37421056), + // ds_flat_edge: Fixed::from_bits(589824), + // is_bottom: false, + // }, + // // csBottomEdge 46792704 int + // // csTopEdge 47579136 int + // // csFlatEdge 46792704 int + // // dsFlatEdge 786432 int + // // bottomZone 0 '\0' unsigned char + // BlueZone { + // cs_bottom_edge: Fixed::from_bits(46792704), + // cs_top_edge: Fixed::from_bits(47579136), + // cs_flat_edge: Fixed::from_bits(46792704), + // ds_flat_edge: Fixed::from_bits(786432), + // is_bottom: false, + // }, + // // csBottomEdge 49807360 int + // // csTopEdge 50593792 int + // // csFlatEdge 49807360 int + // // dsFlatEdge 786432 int + // // bottomZone 0 '\0' unsigned char + // BlueZone { + // cs_bottom_edge: Fixed::from_bits(49807360), + // cs_top_edge: Fixed::from_bits(50593792), + // cs_flat_edge: Fixed::from_bits(49807360), + // ds_flat_edge: Fixed::from_bits(786432), + // is_bottom: false, + // }, + // // csBottomEdge -16711680 int + // // csTopEdge -15728640 int + // // csFlatEdge -15728640 int + // // dsFlatEdge -262144 int + // // bottomZone 1 '\x1' unsigned char + // BlueZone { + // cs_bottom_edge: Fixed::from_bits(-16711680), + // cs_top_edge: Fixed::from_bits(-15728640), + // cs_flat_edge: Fixed::from_bits(-15728640), + // ds_flat_edge: Fixed::from_bits(-262144), + // is_bottom: true, + // }, + // ]; + // assert_eq!(state.zones(), expected_zones); + // } +} diff --git a/src/scale/cff2/mod.rs b/src/scale/cff2/mod.rs new file mode 100644 index 0000000..11baef0 --- /dev/null +++ b/src/scale/cff2/mod.rs @@ -0,0 +1,132 @@ +mod hint; +mod scaler; + +pub use scaler::{Scaler, ScalerSubfont}; + +use read_fonts::types::{F2Dot14, GlyphId, Pen}; + +use super::Outline; + +pub struct SubfontCache { + entries: Vec, + max_entries: usize, + epoch: u64, +} + +impl SubfontCache { + pub fn new(max_entries: usize) -> Self { + Self { + entries: Vec::new(), + max_entries, + epoch: 0, + } + } + + pub fn scale( + &mut self, + scaler: &Scaler, + id: u64, + glyph_id: u16, + size: f32, + coords: &[i16], + hint: bool, + outline: &mut Outline, + ) -> Option<()> { + let epoch = self.epoch; + let gid = GlyphId::new(glyph_id); + let subfont_index = scaler.subfont_index(gid); + let (found, entry_index) = self.find_entry(id, subfont_index, coords, size); + let (subfont, coords) = if found { + let entry = &mut self.entries[entry_index]; + entry.epoch = epoch; + (&entry.subfont, &entry.coords) + } else { + self.epoch += 1; + let epoch = self.epoch; + if entry_index == self.entries.len() { + let coords: Vec = coords.iter().map(|x| F2Dot14::from_bits(*x)).collect(); + let subfont = scaler.subfont(subfont_index, size, &coords).ok()?; + self.entries.push(Entry { + id, + epoch, + subfont, + coords, + }); + let entry = &self.entries[entry_index]; + (&entry.subfont, &entry.coords) + } else { + let entry = &mut self.entries[entry_index]; + entry.id = u64::MAX; + entry.epoch = epoch; + entry.coords.clear(); + entry + .coords + .extend(coords.iter().map(|x| F2Dot14::from_bits(*x))); + entry.subfont = scaler.subfont(subfont_index, size, &entry.coords).ok()?; + entry.id = id; + (&entry.subfont, &entry.coords) + } + }; + scaler + .outline(subfont, gid, coords, hint, &mut OutlineBuilder(outline)) + .ok()?; + Some(()) + } + + fn find_entry(&self, id: u64, index: u32, coords: &[i16], size: f32) -> (bool, usize) { + let mut lowest_epoch = self.epoch; + let mut lowest_index = 0; + for (i, entry) in self.entries.iter().enumerate() { + if entry.id == id + && entry.subfont.index() == index + && entry.subfont.size() == size + && coords + .iter() + .map(|x| F2Dot14::from_bits(*x)) + .eq(entry.coords.iter().copied()) + { + return (true, i); + } + if entry.epoch < lowest_epoch { + lowest_epoch = entry.epoch; + lowest_index = i; + } + } + if self.entries.len() < self.max_entries { + lowest_index = self.entries.len(); + } + (false, lowest_index) + } +} + +struct Entry { + epoch: u64, + id: u64, + subfont: ScalerSubfont, + coords: Vec, +} + +struct OutlineBuilder<'a>(&'a mut Outline); + +impl Pen for OutlineBuilder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, y).into()); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x, y).into()); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.0.quad_to((cx0, cy0).into(), (x, y).into()); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0 + .curve_to((cx0, cy0).into(), (cx1, cy1).into(), (x, y).into()); + } + + fn close(&mut self) { + self.0.close(); + } +} diff --git a/src/scale/cff2/scaler.rs b/src/scale/cff2/scaler.rs new file mode 100644 index 0000000..a690767 --- /dev/null +++ b/src/scale/cff2/scaler.rs @@ -0,0 +1,737 @@ +//! Scaler for CFF outlines. + +use std::ops::Range; + +use read_fonts::{ + tables::{ + cff::Cff, + cff2::Cff2, + postscript::{ + charstring::{self, CommandSink}, + dict, BlendState, Error, FdSelect, Index, + }, + variations::ItemVariationStore, + }, + types::{F2Dot14, Fixed, GlyphId, Pen}, + FontData, FontRead, ReadError, TableProvider, +}; + +use super::hint::{HintParams, HintState}; + +/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. +/// +/// The skrifa crate provides a higher level interface for this that handles +/// caching and abstracting over the different outline formats. Consider using +/// that if detailed control over resources is not required. +/// +/// # Subfonts +/// +/// CFF tables can contain multiple logical "subfonts" which determine the +/// state required for processing some subset of glyphs. This state is +/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28) +/// operators to select an appropriate subfont for any given glyph identifier. +/// This process is exposed on this type with the +/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont +/// index for the requested glyph followed by using the +/// [`subfont`](Self::subfont) method to create an appropriately configured +/// subfont for that glyph. +/// +/// # Example +/// +/// ``` +/// # use read_fonts::{tables::postscript::*, types::*, FontRef}; +/// # fn example(font: FontRef, coords: &[F2Dot14], pen: &mut impl Pen) -> Result<(), Error> { +/// let scaler = Scaler::new(&font)?; +/// let glyph_id = GlyphId::new(24); +/// // Retrieve the subfont index for the requested glyph. +/// let subfont_index = scaler.subfont_index(glyph_id); +/// // Construct a subfont with the given configuration. +/// let size = 16.0; +/// let coords = &[]; +/// let subfont = scaler.subfont(subfont_index, size, coords)?; +/// // Scale the outline using our configured subfont and emit the +/// // result to the given pen. +/// let hint = false; +/// scaler.outline(&subfont, glyph_id, coords, hint, pen)?; +/// # Ok(()) +/// # } +/// ``` +pub struct Scaler<'a> { + version: Version<'a>, + top_dict: ScalerTopDict<'a>, + units_per_em: u16, +} + +impl<'a> Scaler<'a> { + /// Creates a new scaler for the given font. + /// + /// This will choose an underyling CFF2 or CFF table from the font, in that + /// order. + pub fn new(font: &impl TableProvider<'a>) -> Result { + let units_per_em = font.head()?.units_per_em(); + if let Ok(cff2) = font.cff2() { + Self::from_cff2(cff2, units_per_em) + } else { + // "The Name INDEX in the CFF data must contain only one entry; + // that is, there must be only one font in the CFF FontSet" + // So we always pass 0 for Top DICT index when reading from an + // OpenType font. + // + Self::from_cff(font.cff()?, 0, units_per_em) + } + } + + pub fn from_cff( + cff1: Cff<'a>, + top_dict_index: usize, + units_per_em: u16, + ) -> Result { + let top_dict_data = cff1.top_dicts().get(top_dict_index)?; + let top_dict = ScalerTopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; + Ok(Self { + version: Version::Version1(cff1), + top_dict, + units_per_em, + }) + } + + pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result { + let table_data = cff2.offset_data().as_bytes(); + let top_dict = ScalerTopDict::new(table_data, cff2.top_dict_data(), true)?; + Ok(Self { + version: Version::Version2(cff2), + top_dict, + units_per_em, + }) + } + + pub fn is_cff2(&self) -> bool { + matches!(self.version, Version::Version2(_)) + } + + /// Returns the number of available subfonts. + pub fn subfont_count(&self) -> u32 { + self.top_dict + .font_dicts + .as_ref() + .map(|font_dicts| font_dicts.count()) + // All CFF fonts have at least one logical subfont. + .unwrap_or(1) + } + + /// Returns the subfont (or Font DICT) index for the given glyph + /// identifier. + pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 { + // For CFF tables, an FDSelect index will be present for CID-keyed + // fonts. Otherwise, the Top DICT will contain an entry for the + // "global" Private DICT. + // See + // + // CFF2 tables always contain a Font DICT and an FDSelect is only + // present if the size of the DICT is greater than 1. + // See + // + // In both cases, we return a subfont index of 0 when FDSelect is missing. + self.top_dict + .fd_select + .as_ref() + .and_then(|select| select.font_index(glyph_id)) + .unwrap_or(0) as u32 + } + + /// Creates a new subfont for the given index, size, normalized + /// variation coordinates and hinting state. + /// + /// The index of a subfont for a particular glyph can be retrieved with + /// the [`subfont_index`](Self::subfont_index) method. + pub fn subfont( + &self, + index: u32, + size: f32, + coords: &[F2Dot14], + ) -> Result { + let private_dict_range = self.private_dict_range(index)?; + let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?; + let mut hint_params = HintParams::default(); + let mut subrs_offset = None; + let mut store_index = 0; + let blend_state = self + .top_dict + .var_store + .clone() + .map(|store| BlendState::new(store, coords, store_index)) + .transpose()?; + for entry in dict::entries(private_dict_data, blend_state) { + use dict::Entry::*; + match entry? { + BlueValues(values) => hint_params.blues = values, + FamilyBlues(values) => hint_params.family_blues = values, + OtherBlues(values) => hint_params.other_blues = values, + FamilyOtherBlues(values) => hint_params.family_blues = values, + BlueScale(value) => hint_params.blue_scale = value, + BlueShift(value) => hint_params.blue_shift = value, + BlueFuzz(value) => hint_params.blue_fuzz = value, + LanguageGroup(group) => hint_params.language_group = group, + // Subrs offset is relative to the private DICT + SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset), + VariationStoreIndex(index) => store_index = index, + _ => {} + } + } + let scale = if size <= 0.0 { + Fixed::ONE + } else { + // Note: we do an intermediate scale to 26.6 to ensure we + // match FreeType + Fixed::from_bits((size * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32) + }; + let hint_state = HintState::new(&hint_params, scale); + Ok(ScalerSubfont { + is_cff2: self.is_cff2(), + index, + size, + scale, + subrs_offset, + hint_state, + store_index, + }) + } + + /// Loads and scales an outline for the given subfont instance, glyph + /// identifier and normalized variation coordinates. + /// + /// Before calling this method, use [`subfont_index`](Self::subfont_index) + /// to retrieve the subfont index for the desired glyph and then + /// [`subfont`](Self::subfont) to create an instance of the subfont for a + /// particular size and location in variation space. + /// Creating subfont instances is not free, so this process is exposed in + /// discrete steps to allow for caching. + /// + /// The result is emitted to the specified pen. + pub fn outline( + &self, + subfont: &ScalerSubfont, + glyph_id: GlyphId, + coords: &[F2Dot14], + hint: bool, + pen: &mut impl Pen, + ) -> Result<(), Error> { + let charstring_data = self + .top_dict + .charstrings + .as_ref() + .ok_or(Error::Read(ReadError::MalformedData( + "missing charstrings INDEX in CFF table", + )))? + .get(glyph_id.to_u16() as usize)?; + let subrs = subfont.subrs(self)?; + let blend_state = subfont.blend_state(self, coords)?; + let mut pen_sink = charstring::PenSink::new(pen); + let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink); + if hint { + let mut scaling_adapter = ScalingSink26Dot6::new(&mut simplifying_adapter, Fixed::ONE); + let mut hinting_adapter = + super::hint::Hinter::new(&subfont.hint_state, &mut scaling_adapter); + charstring::evaluate( + charstring_data, + self.global_subrs(), + subrs, + blend_state, + &mut hinting_adapter, + )?; + } else { + let mut scaling_adapter = + ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale); + charstring::evaluate( + charstring_data, + self.global_subrs(), + subrs, + blend_state, + &mut scaling_adapter, + )?; + } + simplifying_adapter.finish(); + Ok(()) + } + + fn offset_data(&self) -> FontData<'a> { + match &self.version { + Version::Version1(cff1) => cff1.offset_data(), + Version::Version2(cff2) => cff2.offset_data(), + } + } + + fn global_subrs(&self) -> Index<'a> { + match &self.version { + Version::Version1(cff1) => cff1.global_subrs().into(), + Version::Version2(cff2) => cff2.global_subrs().into(), + } + } + + fn private_dict_range(&self, subfont_index: u32) -> Result, Error> { + if let Some(font_dicts) = &self.top_dict.font_dicts { + // If we have a font dict array, extract the private dict range + // from the font dict at the given index. + let font_dict_data = font_dicts.get(subfont_index as usize)?; + let mut range = None; + for entry in dict::entries(font_dict_data, None) { + if let dict::Entry::PrivateDictRange(r) = entry? { + range = Some(r); + break; + } + } + range + } else { + // Last chance, use the private dict range from the top dict if + // available. + self.top_dict.private_dict_range.clone() + } + .ok_or(Error::Read(ReadError::MalformedData( + "missing Private DICT in CFF table", + ))) + } +} + +enum Version<'a> { + /// + Version1(Cff<'a>), + /// + Version2(Cff2<'a>), +} + +/// Specifies local subroutines and hinting parameters for some subset of +/// glyphs in a CFF or CFF2 table. +/// +/// This type is designed to be cacheable to avoid re-evaluating the private +/// dict every time a charstring is processed. +/// +/// For variable fonts, this is dependent on a location in variation space. +#[derive(Clone)] +pub struct ScalerSubfont { + is_cff2: bool, + index: u32, + size: f32, + scale: Fixed, + subrs_offset: Option, + hint_state: HintState, + store_index: u16, +} + +impl ScalerSubfont { + pub fn index(&self) -> u32 { + self.index + } + + pub fn size(&self) -> f32 { + self.size + } + + /// Returns the local subroutine index. + pub fn subrs<'a>(&self, scaler: &Scaler<'a>) -> Result>, Error> { + if let Some(subrs_offset) = self.subrs_offset { + let offset_data = scaler.offset_data().as_bytes(); + let index_data = offset_data.get(subrs_offset..).unwrap_or_default(); + Ok(Some(Index::new(index_data, self.is_cff2)?)) + } else { + Ok(None) + } + } + + /// Creates a new blend state for the given normalized variation + /// coordinates. + pub fn blend_state<'a>( + &self, + scaler: &Scaler<'a>, + coords: &'a [F2Dot14], + ) -> Result>, Error> { + if let Some(var_store) = scaler.top_dict.var_store.clone() { + Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) + } else { + Ok(None) + } + } +} + +/// Entries that we parse from the Top DICT that are required to support +/// charstring evaluation. +#[derive(Default)] +struct ScalerTopDict<'a> { + charstrings: Option>, + font_dicts: Option>, + fd_select: Option>, + private_dict_range: Option>, + var_store: Option>, +} + +impl<'a> ScalerTopDict<'a> { + fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result { + let mut items = ScalerTopDict::default(); + for entry in dict::entries(top_dict_data, None) { + match entry? { + dict::Entry::CharstringsOffset(offset) => { + items.charstrings = Some(Index::new( + table_data.get(offset..).unwrap_or_default(), + is_cff2, + )?); + } + dict::Entry::FdArrayOffset(offset) => { + items.font_dicts = Some(Index::new( + table_data.get(offset..).unwrap_or_default(), + is_cff2, + )?); + } + dict::Entry::FdSelectOffset(offset) => { + items.fd_select = Some(FdSelect::read(FontData::new( + table_data.get(offset..).unwrap_or_default(), + ))?); + } + dict::Entry::PrivateDictRange(range) => { + items.private_dict_range = Some(range); + } + dict::Entry::VariationStoreOffset(offset) if is_cff2 => { + items.var_store = Some(ItemVariationStore::read(FontData::new( + // IVS is preceded by a 2 byte length + table_data.get(offset + 2..).unwrap_or_default(), + ))?); + } + _ => {} + } + } + Ok(items) + } +} + +/// Command sink adapter that applies a scaling factor. +/// +/// This assumes a 26.6 scaling factor packed into a Fixed and thus, +/// this is not public and exists only to match FreeType's exact +/// scaling process. +struct ScalingSink26Dot6<'a, S> { + inner: &'a mut S, + scale: Fixed, +} + +impl<'a, S> ScalingSink26Dot6<'a, S> { + fn new(sink: &'a mut S, scale: Fixed) -> Self { + Self { scale, inner: sink } + } + + fn scale(&self, coord: Fixed) -> Fixed { + if self.scale != Fixed::ONE { + // The following dance is necessary to exactly match FreeType's + // application of scaling factors. This seems to be the result + // of merging the contributed Adobe code while not breaking the + // FreeType public API. + // 1. Multiply by 1/64 + // + let a = coord * Fixed::from_bits(0x0400); + // 2. Convert to 26.6 by truncation + // + let b = Fixed::from_bits(a.to_bits() >> 10); + // 3. Multiply by the original scale factor + // + let c = b * self.scale; + // Finally, we convert back to 16.16 + Fixed::from_bits(c.to_bits() << 10) + } else { + // Otherwise, simply zero the low 10 bits + Fixed::from_bits(coord.to_bits() & !0x3FF) + } + } +} + +impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> { + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.inner.hstem(y, dy); + } + + fn vstem(&mut self, x: Fixed, dx: Fixed) { + self.inner.vstem(x, dx); + } + + fn hint_mask(&mut self, mask: &[u8]) { + self.inner.hint_mask(mask); + } + + fn counter_mask(&mut self, mask: &[u8]) { + self.inner.counter_mask(mask); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.inner.move_to(self.scale(x), self.scale(y)); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.inner.line_to(self.scale(x), self.scale(y)); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.inner.curve_to( + self.scale(cx1), + self.scale(cy1), + self.scale(cx2), + self.scale(cy2), + self.scale(x), + self.scale(y), + ); + } + + fn close(&mut self) { + self.inner.close(); + } +} + +/// Command sink adapter that supresses degenerate move and line commands. +/// +/// FreeType avoids emitting empty contours and zero length lines to prevent +/// artifacts when stem darkening is enabled. We don't support stem darkening +/// because it's not enabled by any of our clients but we remove the degenerate +/// elements regardless to match the output. +/// +/// See +struct NopFilteringSink<'a, S> { + start: Option<(Fixed, Fixed)>, + last: Option<(Fixed, Fixed)>, + pending_move: Option<(Fixed, Fixed)>, + inner: &'a mut S, +} + +impl<'a, S> NopFilteringSink<'a, S> +where + S: CommandSink, +{ + fn new(inner: &'a mut S) -> Self { + Self { + start: None, + last: None, + pending_move: None, + inner, + } + } + + fn flush_pending_move(&mut self) { + if let Some((x, y)) = self.pending_move.take() { + if let Some((last_x, last_y)) = self.start { + if self.last != self.start { + self.inner.line_to(last_x, last_y); + } + } + self.start = Some((x, y)); + self.last = None; + self.inner.move_to(x, y); + } + } + + pub fn finish(&mut self) { + match self.start { + Some((x, y)) if self.last != self.start => { + self.inner.line_to(x, y); + } + _ => {} + } + } +} + +impl<'a, S> CommandSink for NopFilteringSink<'a, S> +where + S: CommandSink, +{ + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.inner.hstem(y, dy); + } + + fn vstem(&mut self, x: Fixed, dx: Fixed) { + self.inner.vstem(x, dx); + } + + fn hint_mask(&mut self, mask: &[u8]) { + self.inner.hint_mask(mask); + } + + fn counter_mask(&mut self, mask: &[u8]) { + self.inner.counter_mask(mask); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.pending_move = Some((x, y)); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + if self.pending_move == Some((x, y)) { + return; + } + self.flush_pending_move(); + if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) { + return; + } + self.inner.line_to(x, y); + self.last = Some((x, y)); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.flush_pending_move(); + self.last = Some((x, y)); + self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + if self.pending_move.is_none() { + if let Some((start_x, start_y)) = self.start { + if self.start != self.last { + self.inner.line_to(start_x, start_y); + } + } + self.start = None; + self.last = None; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use read_fonts::FontRef; + + // fn check_blues(blues: &Blues, expected_values: &[(f64, f64)]) { + // for (i, blue) in blues.values().iter().enumerate() { + // let expected = expected_values[i]; + // assert_eq!(blue.0, Fixed::from_f64(expected.0)); + // assert_eq!(blue.1, Fixed::from_f64(expected.1)); + // } + // } + + #[test] + fn read_cff_static() { + let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); + let cff = Scaler::new(&font).unwrap(); + assert!(!cff.is_cff2()); + assert!(cff.top_dict.var_store.is_none()); + assert!(cff.top_dict.font_dicts.is_none()); + assert!(cff.top_dict.private_dict_range.is_some()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 17); + // let subfont = cff.subfont(0, 0.0, Default::default(), false).unwrap(); + // let hinting_params = subfont.hint_params; + // check_blues( + // &hinting_params.blues, + // &[ + // (-15.0, 0.0), + // (536.0, 547.0), + // (571.0, 582.0), + // (714.0, 726.0), + // (760.0, 772.0), + // ], + // ); + // check_blues(&hinting_params.other_blues, &[(-255.0, -240.0)]); + // assert_eq!(hinting_params.blue_scale, Fixed::from_f64(0.05)); + // assert_eq!(hinting_params.blue_fuzz, Fixed::ZERO); + // assert_eq!(hinting_params.language_group, 0); + } + + #[test] + fn read_cff2_static() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff = Scaler::new(&font).unwrap(); + assert!(cff.is_cff2()); + assert!(cff.top_dict.var_store.is_some()); + assert!(cff.top_dict.font_dicts.is_some()); + assert!(cff.top_dict.private_dict_range.is_none()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 0); + // let subfont = cff.subfont(0, 0.0, Default::default(), false).unwrap(); + // let hinting_params = subfont.hint_params; + // check_blues( + // &hinting_params.blues, + // &[(-10.0, 0.0), (482.0, 492.0), (694.0, 704.0), (739.0, 749.0)], + // ); + // check_blues(&hinting_params.other_blues, &[(-227.0, -217.0)]); + // assert_eq!(hinting_params.blue_scale, Fixed::from_f64(0.0625)); + // assert_eq!(hinting_params.blue_fuzz, Fixed::ONE); + // assert_eq!(hinting_params.language_group, 0); + } + + #[test] + fn read_example_cff2_table() { + let cff = Scaler::from_cff2( + Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap(), + 1000, + ) + .unwrap(); + assert!(cff.is_cff2()); + assert!(cff.top_dict.var_store.is_some()); + assert!(cff.top_dict.font_dicts.is_some()); + assert!(cff.top_dict.private_dict_range.is_none()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 0); + } + + #[test] + fn cff2_variable_outlines_match_freetype() { + compare_glyphs( + font_test_data::CANTARELL_VF_TRIMMED, + font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, + ); + } + + #[test] + fn cff_static_outlines_match_freetype() { + compare_glyphs( + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, + ); + } + + /// For the given font data and extracted outlines, parse the extracted + /// outline data into a set of expected values and compare these with the + /// results generated by the scaler. + /// + /// This will compare all outlines at various sizes and (for variable + /// fonts), locations in variation space. + fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { + let font = FontRef::new(font_data).unwrap(); + let outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines); + let scaler = super::Scaler::new(&font).unwrap(); + let mut path = read_fonts::scaler_test::Path { + elements: vec![], + is_cff: true, + }; + for expected_outline in &outlines { + if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() { + continue; + } + path.elements.clear(); + let subfont = scaler + .subfont( + scaler.subfont_index(expected_outline.glyph_id), + expected_outline.size, + &expected_outline.coords, + ) + .unwrap(); + scaler + .outline( + &subfont, + expected_outline.glyph_id, + &expected_outline.coords, + false, + &mut path, + ) + .unwrap(); + if path.elements != expected_outline.path { + panic!( + "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}", + expected_outline.glyph_id, + expected_outline.size, + expected_outline.coords, + &path.elements, + &expected_outline.path + ); + } + } + } +} diff --git a/src/scale/mod.rs b/src/scale/mod.rs index 4e05f89..a46a50b 100644 --- a/src/scale/mod.rs +++ b/src/scale/mod.rs @@ -220,6 +220,7 @@ pub mod outline; mod bitmap; mod cff; +mod cff2; mod color; mod glyf; mod proxy; @@ -287,6 +288,7 @@ pub struct ScaleContext { struct State { glyf_scaler: glyf::Scaler, cff_scaler: cff::Scaler, + cff_cache: cff2::SubfontCache, scratch0: Vec, scratch1: Vec, outline: Outline, @@ -309,6 +311,7 @@ impl ScaleContext { state: State { glyf_scaler: glyf::Scaler::new(max_entries), cff_scaler: cff::Scaler::new(max_entries), + cff_cache: cff2::SubfontCache::new(max_entries), scratch0: Vec::new(), scratch1: Vec::new(), outline: Outline::new(), @@ -417,10 +420,30 @@ impl<'a> ScalerBuilder<'a> { } else { 1. }; + // Handle read-fonts conversion for CFF + let cff = if matches!(&self.proxy.outlines, OutlinesProxy::Cff(_)) { + let font = if self.font.offset == 0 { + read_fonts::FontRef::new(self.font.data).ok() + } else { + // TODO: make this faster + let index = crate::FontDataRef::new(self.font.data).and_then(|font_data| { + font_data + .fonts() + .position(|font| font.offset == self.font.offset) + }); + index.and_then(|index| { + read_fonts::FontRef::from_index(self.font.data, index as u32).ok() + }) + }; + font.and_then(|font| cff2::Scaler::new(&font).ok()) + } else { + None + }; Scaler { state: self.state, font: self.font, proxy: self.proxy, + cff, id: self.id, coords: &self.coords[..], size: self.size, @@ -438,6 +461,7 @@ pub struct Scaler<'a> { state: &'a mut State, font: FontRef<'a>, proxy: &'a ScalerProxy, + cff: Option>, id: u64, coords: &'a [i16], size: f32, @@ -517,22 +541,20 @@ impl<'a> Scaler<'a> { _ => &mut self.state.outline, }; match &self.proxy.outlines { - OutlinesProxy::None => false, - OutlinesProxy::Cff(proxy) => { + OutlinesProxy::Cff(_) if self.cff.is_some() => { + let cff_scaler = self.cff.as_ref().unwrap(); outline.begin_layer(color_index); - let mut builder = CffBuilder { outline }; if self .state - .cff_scaler + .cff_cache .scale( - &self.font, + cff_scaler, self.id, + glyph_id, + self.size, &self.coords, - proxy, - self.scale, self.hint, - glyph_id, - &mut builder, + outline, ) .is_some() { @@ -573,6 +595,7 @@ impl<'a> Scaler<'a> { false } } + _ => false, } } From 275ce853eb35a16c11a8895330b03bc39e4a7ae3 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 30 Jan 2024 11:32:08 -0500 Subject: [PATCH 2/4] fix cff hinting midpoint calculation --- src/scale/cff2/hint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scale/cff2/hint.rs b/src/scale/cff2/hint.rs index 99ccfc8..14340f8 100644 --- a/src/scale/cff2/hint.rs +++ b/src/scale/cff2/hint.rs @@ -636,7 +636,7 @@ impl HintMap { if !first.is_locked() { if let Some(initial) = initial { if is_pair { - let mid = initial.apply(scale, half(second.coord + first.coord)); + let mid = initial.apply(scale, first.coord + half(second.coord - first.coord)); let half = half(second.coord - first.coord) * scale; first.ds_coord = mid - half; second.ds_coord = mid + half; From 3190c4c676a0e9fec99a1e0c52b2940c0668db94 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 30 Jan 2024 12:01:34 -0500 Subject: [PATCH 3/4] update CFF impl --- Cargo.lock | 10 +- Cargo.toml | 8 +- src/scale/cff/hint.rs | 2 +- src/scale/cff2/hint.rs | 111 --- src/scale/cff2/scaler.rs | 149 ---- src/scale/cff3/hint.rs | 1479 ++++++++++++++++++++++++++++++++++++ src/scale/cff3/mod.rs | 137 ++++ src/scale/cff3/outlines.rs | 709 +++++++++++++++++ src/scale/mod.rs | 40 +- src/scale/proxy.rs | 9 +- 10 files changed, 2347 insertions(+), 307 deletions(-) create mode 100644 src/scale/cff3/hint.rs create mode 100644 src/scale/cff3/mod.rs create mode 100644 src/scale/cff3/outlines.rs diff --git a/Cargo.lock b/Cargo.lock index afb1e2c..0bf7081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,19 +5,19 @@ version = 3 [[package]] name = "font-test-data" version = "0.1.0" -source = "git+https://github.com/googlefonts/fontations?rev=91ebdfd91bec9ae4ec34f6a7d5f01736b1b2eb6e#91ebdfd91bec9ae4ec34f6a7d5f01736b1b2eb6e" +source = "git+https://github.com/googlefonts/fontations?rev=10c27ef7bba1549fa37a3f41cd4870b2a24b1073#10c27ef7bba1549fa37a3f41cd4870b2a24b1073" [[package]] name = "font-types" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6978d65d61022aa249fefdd914dc8215757f617f1a697c496ef6b42013366567" +checksum = "0bd7f3ea17572640b606b35df42cfb6ecdf003704b062580e59918692190b73d" [[package]] name = "read-fonts" -version = "0.10.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d08214643b2df95b0b3955cd9f264bcfab22b73470b83df4992df523b4d6eb" +checksum = "7555e052e772f964a1c99f1434f6a2c3a47a5f8e4292236921f121a7753cb2b5" dependencies = [ "font-types", ] diff --git a/Cargo.toml b/Cargo.toml index 93258bd..49e05d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.8" authors = ["Chad Brokaw "] edition = "2018" description = "Font introspection, complex text shaping and glyph rendering." -license = "MIT OR Apache-2.0" +license = "MIT/Apache-2.0" keywords = ["font", "shape", "glyph", "text"] categories = ["graphics", "text-processing"] repository = "https://github.com/dfrg/swash" @@ -20,8 +20,8 @@ render = ["scale", "zeno/eval"] [dependencies] yazi = { version = "0.1.6", optional = true } zeno = { version = "0.2.2", optional = true, default_features = false } -read-fonts = "0.10.0" +read-fonts = "0.15.2" [dev-dependencies] -font-test-data= { git = "https://github.com/googlefonts/fontations", rev = "91ebdfd91bec9ae4ec34f6a7d5f01736b1b2eb6e" } -read-fonts = { version = "0.10.0", features = ["scaler_test"] } +font-test-data= { git = "https://github.com/googlefonts/fontations", rev = "10c27ef7bba1549fa37a3f41cd4870b2a24b1073" } +read-fonts = { version = "0.15.2", features = ["scaler_test"] } diff --git a/src/scale/cff/hint.rs b/src/scale/cff/hint.rs index 5c555e7..41defb4 100644 --- a/src/scale/cff/hint.rs +++ b/src/scale/cff/hint.rs @@ -593,7 +593,7 @@ impl HintMap { if !first.is_locked() { if let Some(ref initial) = initial { if is_pair { - let mid = initial.map(scale, (second.coord + first.coord) / 2); + let mid = initial.map(scale, first.coord + (second.coord + first.coord) / 2); let half = (second.coord - first.coord) / 2 * scale; first.ds_coord = mid - half; second.ds_coord = mid + half; diff --git a/src/scale/cff2/hint.rs b/src/scale/cff2/hint.rs index 14340f8..291f39e 100644 --- a/src/scale/cff2/hint.rs +++ b/src/scale/cff2/hint.rs @@ -93,11 +93,6 @@ impl HintState { state } - #[cfg(test)] - fn zones(&self) -> &[BlueZone] { - &self.zones[..self.zone_count] - } - /// Initialize zones from the set of blues values. /// /// See @@ -982,109 +977,3 @@ fn half(value: Fixed) -> Fixed { fn twice(value: Fixed) -> Fixed { Fixed::from_bits(value.to_bits().wrapping_mul(2)) } - -#[cfg(test)] -mod tests { - use super::{BlueZone, Blues, Fixed, HintParams, HintState}; - - // #[test] - // fn noto_serif_display_blue_zones() { - // fn make_blues(values: &[f64]) -> Blues { - // Blues::new(values.iter().copied().map(Fixed::from_f64)) - // } - // // - // // - // // - // // - // // - // let params = HintParams { - // blues: make_blues(&[ - // -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, - // ]), - // other_blues: make_blues(&[-255.0, -240.0]), - // blue_scale: Fixed::from_f64(0.05), - // blue_shift: Fixed::from_i32(7), - // blue_fuzz: Fixed::ZERO, - // ..Default::default() - // }; - // let state = HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)); - // assert!(!state.do_em_box_hints); - // assert_eq!(state.zone_count, 6); - // assert_eq!(state.boost, Fixed::from_bits(27035)); - // assert!(state.supress_overshoot); - // // FreeType generates the following zones: - // let expected_zones = &[ - // // csBottomEdge -983040 int - // // csTopEdge 0 int - // // csFlatEdge 0 int - // // dsFlatEdge 0 int - // // bottomZone 1 '\x1' unsigned char - // BlueZone { - // cs_bottom_edge: Fixed::from_bits(-983040), - // is_bottom: true, - // ..Default::default() - // }, - // // csBottomEdge 35127296 int - // // csTopEdge 35848192 int - // // csFlatEdge 35127296 int - // // dsFlatEdge 589824 int - // // bottomZone 0 '\0' unsigned char - // BlueZone { - // cs_bottom_edge: Fixed::from_bits(35127296), - // cs_top_edge: Fixed::from_bits(35848192), - // cs_flat_edge: Fixed::from_bits(35127296), - // ds_flat_edge: Fixed::from_bits(589824), - // is_bottom: false, - // }, - // // csBottomEdge 37421056 int - // // csTopEdge 38141952 int - // // csFlatEdge 37421056 int - // // dsFlatEdge 589824 int - // // bottomZone 0 '\0' unsigned char - // BlueZone { - // cs_bottom_edge: Fixed::from_bits(37421056), - // cs_top_edge: Fixed::from_bits(38141952), - // cs_flat_edge: Fixed::from_bits(37421056), - // ds_flat_edge: Fixed::from_bits(589824), - // is_bottom: false, - // }, - // // csBottomEdge 46792704 int - // // csTopEdge 47579136 int - // // csFlatEdge 46792704 int - // // dsFlatEdge 786432 int - // // bottomZone 0 '\0' unsigned char - // BlueZone { - // cs_bottom_edge: Fixed::from_bits(46792704), - // cs_top_edge: Fixed::from_bits(47579136), - // cs_flat_edge: Fixed::from_bits(46792704), - // ds_flat_edge: Fixed::from_bits(786432), - // is_bottom: false, - // }, - // // csBottomEdge 49807360 int - // // csTopEdge 50593792 int - // // csFlatEdge 49807360 int - // // dsFlatEdge 786432 int - // // bottomZone 0 '\0' unsigned char - // BlueZone { - // cs_bottom_edge: Fixed::from_bits(49807360), - // cs_top_edge: Fixed::from_bits(50593792), - // cs_flat_edge: Fixed::from_bits(49807360), - // ds_flat_edge: Fixed::from_bits(786432), - // is_bottom: false, - // }, - // // csBottomEdge -16711680 int - // // csTopEdge -15728640 int - // // csFlatEdge -15728640 int - // // dsFlatEdge -262144 int - // // bottomZone 1 '\x1' unsigned char - // BlueZone { - // cs_bottom_edge: Fixed::from_bits(-16711680), - // cs_top_edge: Fixed::from_bits(-15728640), - // cs_flat_edge: Fixed::from_bits(-15728640), - // ds_flat_edge: Fixed::from_bits(-262144), - // is_bottom: true, - // }, - // ]; - // assert_eq!(state.zones(), expected_zones); - // } -} diff --git a/src/scale/cff2/scaler.rs b/src/scale/cff2/scaler.rs index a690767..099be3d 100644 --- a/src/scale/cff2/scaler.rs +++ b/src/scale/cff2/scaler.rs @@ -586,152 +586,3 @@ where } } } - -#[cfg(test)] -mod tests { - use super::*; - use read_fonts::FontRef; - - // fn check_blues(blues: &Blues, expected_values: &[(f64, f64)]) { - // for (i, blue) in blues.values().iter().enumerate() { - // let expected = expected_values[i]; - // assert_eq!(blue.0, Fixed::from_f64(expected.0)); - // assert_eq!(blue.1, Fixed::from_f64(expected.1)); - // } - // } - - #[test] - fn read_cff_static() { - let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); - let cff = Scaler::new(&font).unwrap(); - assert!(!cff.is_cff2()); - assert!(cff.top_dict.var_store.is_none()); - assert!(cff.top_dict.font_dicts.is_none()); - assert!(cff.top_dict.private_dict_range.is_some()); - assert!(cff.top_dict.fd_select.is_none()); - assert_eq!(cff.subfont_count(), 1); - assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); - assert_eq!(cff.global_subrs().count(), 17); - // let subfont = cff.subfont(0, 0.0, Default::default(), false).unwrap(); - // let hinting_params = subfont.hint_params; - // check_blues( - // &hinting_params.blues, - // &[ - // (-15.0, 0.0), - // (536.0, 547.0), - // (571.0, 582.0), - // (714.0, 726.0), - // (760.0, 772.0), - // ], - // ); - // check_blues(&hinting_params.other_blues, &[(-255.0, -240.0)]); - // assert_eq!(hinting_params.blue_scale, Fixed::from_f64(0.05)); - // assert_eq!(hinting_params.blue_fuzz, Fixed::ZERO); - // assert_eq!(hinting_params.language_group, 0); - } - - #[test] - fn read_cff2_static() { - let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); - let cff = Scaler::new(&font).unwrap(); - assert!(cff.is_cff2()); - assert!(cff.top_dict.var_store.is_some()); - assert!(cff.top_dict.font_dicts.is_some()); - assert!(cff.top_dict.private_dict_range.is_none()); - assert!(cff.top_dict.fd_select.is_none()); - assert_eq!(cff.subfont_count(), 1); - assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); - assert_eq!(cff.global_subrs().count(), 0); - // let subfont = cff.subfont(0, 0.0, Default::default(), false).unwrap(); - // let hinting_params = subfont.hint_params; - // check_blues( - // &hinting_params.blues, - // &[(-10.0, 0.0), (482.0, 492.0), (694.0, 704.0), (739.0, 749.0)], - // ); - // check_blues(&hinting_params.other_blues, &[(-227.0, -217.0)]); - // assert_eq!(hinting_params.blue_scale, Fixed::from_f64(0.0625)); - // assert_eq!(hinting_params.blue_fuzz, Fixed::ONE); - // assert_eq!(hinting_params.language_group, 0); - } - - #[test] - fn read_example_cff2_table() { - let cff = Scaler::from_cff2( - Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap(), - 1000, - ) - .unwrap(); - assert!(cff.is_cff2()); - assert!(cff.top_dict.var_store.is_some()); - assert!(cff.top_dict.font_dicts.is_some()); - assert!(cff.top_dict.private_dict_range.is_none()); - assert!(cff.top_dict.fd_select.is_none()); - assert_eq!(cff.subfont_count(), 1); - assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); - assert_eq!(cff.global_subrs().count(), 0); - } - - #[test] - fn cff2_variable_outlines_match_freetype() { - compare_glyphs( - font_test_data::CANTARELL_VF_TRIMMED, - font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, - ); - } - - #[test] - fn cff_static_outlines_match_freetype() { - compare_glyphs( - font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, - font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, - ); - } - - /// For the given font data and extracted outlines, parse the extracted - /// outline data into a set of expected values and compare these with the - /// results generated by the scaler. - /// - /// This will compare all outlines at various sizes and (for variable - /// fonts), locations in variation space. - fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { - let font = FontRef::new(font_data).unwrap(); - let outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines); - let scaler = super::Scaler::new(&font).unwrap(); - let mut path = read_fonts::scaler_test::Path { - elements: vec![], - is_cff: true, - }; - for expected_outline in &outlines { - if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() { - continue; - } - path.elements.clear(); - let subfont = scaler - .subfont( - scaler.subfont_index(expected_outline.glyph_id), - expected_outline.size, - &expected_outline.coords, - ) - .unwrap(); - scaler - .outline( - &subfont, - expected_outline.glyph_id, - &expected_outline.coords, - false, - &mut path, - ) - .unwrap(); - if path.elements != expected_outline.path { - panic!( - "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}", - expected_outline.glyph_id, - expected_outline.size, - expected_outline.coords, - &path.elements, - &expected_outline.path - ); - } - } - } -} diff --git a/src/scale/cff3/hint.rs b/src/scale/cff3/hint.rs new file mode 100644 index 0000000..84aeae9 --- /dev/null +++ b/src/scale/cff3/hint.rs @@ -0,0 +1,1479 @@ +//! CFF hinting. + +use read_fonts::{ + tables::postscript::{charstring::CommandSink, dict::Blues}, + types::Fixed, +}; + +// "Default values for OS/2 typoAscender/Descender.." +// See +const ICF_TOP: Fixed = Fixed::from_i32(880); +const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); + +// +const MAX_BLUES: usize = 7; +const MAX_OTHER_BLUES: usize = 5; +const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES; + +// +const MAX_HINTS: usize = 96; + +// One bit per stem hint +// +const HINT_MASK_SIZE: usize = (MAX_HINTS + 7) / 8; + +// Constant for hint adjustment and em box hint placement. +// +const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); + +// +const EPSILON: Fixed = Fixed::from_bits(1); + +/// Parameters used to generate the stem and counter zones for the hinting +/// algorithm. +#[derive(Clone)] +pub(crate) struct HintParams { + pub blues: Blues, + pub family_blues: Blues, + pub other_blues: Blues, + pub family_other_blues: Blues, + pub blue_scale: Fixed, + pub blue_shift: Fixed, + pub blue_fuzz: Fixed, + pub language_group: i32, +} + +impl Default for HintParams { + fn default() -> Self { + Self { + blues: Blues::default(), + other_blues: Blues::default(), + family_blues: Blues::default(), + family_other_blues: Blues::default(), + // See + blue_scale: Fixed::from_f64(0.039625), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ONE, + language_group: 0, + } + } +} + +/// See +#[derive(Copy, Clone, PartialEq, Default, Debug)] +struct BlueZone { + is_bottom: bool, + cs_bottom_edge: Fixed, + cs_top_edge: Fixed, + cs_flat_edge: Fixed, + ds_flat_edge: Fixed, +} + +/// Hinting state for a PostScript subfont. +/// +/// Note that hinter states depend on the scale, subfont index and +/// variation coordinates of a glyph. They can be retained and reused +/// if those values remain the same. +#[derive(Copy, Clone, PartialEq, Default)] +pub(crate) struct HintState { + scale: Fixed, + blue_scale: Fixed, + blue_shift: Fixed, + blue_fuzz: Fixed, + language_group: i32, + supress_overshoot: bool, + do_em_box_hints: bool, + boost: Fixed, + darken_y: Fixed, + zones: [BlueZone; MAX_BLUE_ZONES], + zone_count: usize, +} + +impl HintState { + pub fn new(params: &HintParams, scale: Fixed) -> Self { + let mut state = Self { + scale, + blue_scale: params.blue_scale, + blue_shift: params.blue_shift, + blue_fuzz: params.blue_fuzz, + language_group: params.language_group, + supress_overshoot: false, + do_em_box_hints: false, + boost: Fixed::ZERO, + darken_y: Fixed::ZERO, + zones: [BlueZone::default(); MAX_BLUE_ZONES], + zone_count: 0, + }; + state.build_zones(params); + state + } + + fn zones(&self) -> &[BlueZone] { + &self.zones[..self.zone_count] + } + + /// Initialize zones from the set of blues values. + /// + /// See + fn build_zones(&mut self, params: &HintParams) { + self.do_em_box_hints = false; + // + match (self.language_group, params.blues.values().len()) { + (1, 2) => { + let blues = params.blues.values(); + if blues[0].0 < ICF_BOTTOM + && blues[0].1 < ICF_BOTTOM + && blues[1].0 > ICF_TOP + && blues[1].1 > ICF_TOP + { + // FreeType generates synthetic hints here. We'll do it + // later when building the hint map. + self.do_em_box_hints = true; + return; + } + } + (1, 0) => { + self.do_em_box_hints = true; + return; + } + _ => {} + } + let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; + let mut max_zone_height = Fixed::ZERO; + let mut zone_ix = 0usize; + // Copy blues and other blues to a combined array of top and bottom zones. + for blue in params.blues.values().iter().take(MAX_BLUES) { + // FreeType loads blues as integers and then expands to 16.16 + // at initialization. We load them as 16.16 so floor them here + // to ensure we match. + // + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height + continue; + } + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_ix]; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + if zone_ix == 0 { + // First blue value is bottom zone + zone.is_bottom = true; + zone.cs_flat_edge = top; + } else { + // Remaining blue values are top zones + zone.is_bottom = false; + // Adjust both edges of top zone upward by twice darkening amount + zone.cs_top_edge += twice(self.darken_y); + zone.cs_bottom_edge += twice(self.darken_y); + zone.cs_flat_edge = zone.cs_bottom_edge; + } + zone_ix += 1; + } + for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height + continue; + } + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_ix]; + // All "other" blues are bottom zone + zone.is_bottom = true; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + zone.cs_flat_edge = top; + zone_ix += 1; + } + // Adjust for family blues + let units_per_pixel = Fixed::ONE / self.scale; + for zone in &mut zones[..zone_ix] { + let flat = zone.cs_flat_edge; + let mut min_diff = Fixed::MAX; + if zone.is_bottom { + // In a bottom zone, the top edge is the flat edge. + // Search family other blues for bottom zones. Look for the + // closest edge that is within the one pixel threshold. + for blue in params.family_other_blues.values() { + let family_flat = blue.1; + let diff = (flat - family_flat).abs(); + if diff < min_diff && diff < units_per_pixel { + zone.cs_flat_edge = family_flat; + min_diff = diff; + if diff == Fixed::ZERO { + break; + } + } + } + // Check the first member of family blues, which is a bottom + // zone + if !params.family_blues.values().is_empty() { + let family_flat = params.family_blues.values()[0].1; + let diff = (flat - family_flat).abs(); + if diff < min_diff && diff < units_per_pixel { + zone.cs_flat_edge = family_flat; + } + } + } else { + // In a top zone, the bottom edge is the flat edge. + // Search family blues for top zones, skipping the first, which + // is a bottom zone. Look for closest family edge that is + // within the one pixel threshold. + for blue in params.family_blues.values().iter().skip(1) { + let family_flat = blue.0 + twice(self.darken_y); + let diff = (flat - family_flat).abs(); + if diff < min_diff && diff < units_per_pixel { + zone.cs_flat_edge = family_flat; + min_diff = diff; + if diff == Fixed::ZERO { + break; + } + } + } + } + } + if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { + // Clamp at maximum scale + self.blue_scale = Fixed::ONE / max_zone_height; + } + // Suppress overshoot and boost blue zones at small sizes + if self.scale < self.blue_scale { + self.supress_overshoot = true; + self.boost = + Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); + // boost must remain less than 0.5, or baseline could go negative + self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); + } + if self.darken_y != Fixed::ZERO { + self.boost = Fixed::ZERO; + } + // Set device space alignment for each zone; apply boost amount before + // rounding flat edge + let scale = self.scale; + let boost = self.boost; + for zone in &mut zones[..zone_ix] { + let boost = if zone.is_bottom { -boost } else { boost }; + zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); + } + self.zones = zones; + self.zone_count = zone_ix; + } + + /// Check whether a hint is captured by one of the blue zones. + /// + /// See + fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool { + let fuzz = self.blue_fuzz; + let mut captured = false; + let mut adjustment = Fixed::ZERO; + for zone in self.zones() { + if zone.is_bottom + && bottom_edge.is_bottom() + && (zone.cs_bottom_edge - fuzz) <= bottom_edge.cs_coord + && bottom_edge.cs_coord <= (zone.cs_top_edge + fuzz) + { + // Bottom edge captured by bottom zone. + adjustment = if self.supress_overshoot { + zone.ds_flat_edge + } else if zone.cs_top_edge - bottom_edge.cs_coord >= self.blue_shift { + // Guarantee minimum of 1 pixel overshoot + bottom_edge + .ds_coord + .round() + .min(zone.ds_flat_edge - Fixed::ONE) + } else { + bottom_edge.ds_coord.round() + }; + adjustment -= bottom_edge.ds_coord; + captured = true; + break; + } + if !zone.is_bottom + && top_edge.is_top() + && (zone.cs_bottom_edge - fuzz) <= top_edge.cs_coord + && top_edge.cs_coord <= (zone.cs_top_edge + fuzz) + { + // Top edge captured by top zone. + adjustment = if self.supress_overshoot { + zone.ds_flat_edge + } else if top_edge.cs_coord - zone.cs_bottom_edge >= self.blue_shift { + // Guarantee minimum of 1 pixel overshoot + top_edge + .ds_coord + .round() + .max(zone.ds_flat_edge + Fixed::ONE) + } else { + top_edge.ds_coord.round() + }; + adjustment -= top_edge.ds_coord; + captured = true; + break; + } + } + if captured { + // Move both edges and mark them as "locked" + if bottom_edge.is_valid() { + bottom_edge.ds_coord += adjustment; + bottom_edge.lock(); + } + if top_edge.is_valid() { + top_edge.ds_coord += adjustment; + top_edge.lock(); + } + } + captured + } +} + +/// +#[derive(Copy, Clone, Default)] +struct StemHint { + /// If true, device space position is valid + is_used: bool, + // Character space position + min: Fixed, + max: Fixed, + // Device space position after first use + ds_min: Fixed, + ds_max: Fixed, +} + +// Hint flags +const GHOST_BOTTOM: u8 = 0x1; +const GHOST_TOP: u8 = 0x2; +const PAIR_BOTTOM: u8 = 0x4; +const PAIR_TOP: u8 = 0x8; +const LOCKED: u8 = 0x10; +const SYNTHETIC: u8 = 0x20; + +/// +#[derive(Copy, Clone, PartialEq, Default, Debug)] +struct Hint { + flags: u8, + /// Index in original stem hint array (if not synthetic) + index: u8, + cs_coord: Fixed, + ds_coord: Fixed, + scale: Fixed, +} + +impl Hint { + fn is_valid(&self) -> bool { + self.flags != 0 + } + + fn is_bottom(&self) -> bool { + self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0 + } + + fn is_top(&self) -> bool { + self.flags & (GHOST_TOP | PAIR_TOP) != 0 + } + + fn is_pair(&self) -> bool { + self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0 + } + + fn is_pair_top(&self) -> bool { + self.flags & PAIR_TOP != 0 + } + + fn is_locked(&self) -> bool { + self.flags & LOCKED != 0 + } + + fn is_synthetic(&self) -> bool { + self.flags & SYNTHETIC != 0 + } + + fn lock(&mut self) { + self.flags |= LOCKED + } + + /// Hint initialization from an incoming stem hint. + /// + /// See + fn setup( + &mut self, + stem: &StemHint, + index: u8, + origin: Fixed, + scale: Fixed, + darken_y: Fixed, + is_bottom: bool, + ) { + // "Ghost hints" are used to align a single edge rather than a + // stem-- think the top and bottom edges of an uppercase + // sans-serif I. + // These are encoded internally with stem hints of width -21 + // and -20 for bottom and top hints, respectively. + const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21); + const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20); + let width = stem.max - stem.min; + if width == GHOST_BOTTOM_WIDTH { + if is_bottom { + self.cs_coord = stem.max; + self.flags = GHOST_BOTTOM; + } else { + self.flags = 0; + } + } else if width == GHOST_TOP_WIDTH { + if !is_bottom { + self.cs_coord = stem.min; + self.flags = GHOST_TOP; + } else { + self.flags = 0; + } + } else if width < Fixed::ZERO { + // If width < 0, this is an inverted pair. We follow FreeType and + // swap the coordinates + if is_bottom { + self.cs_coord = stem.max; + self.flags = PAIR_BOTTOM; + } else { + self.cs_coord = stem.min; + self.flags = PAIR_TOP; + } + } else { + // This is a normal pair + if is_bottom { + self.cs_coord = stem.min; + self.flags = PAIR_BOTTOM; + } else { + self.cs_coord = stem.max; + self.flags = PAIR_TOP; + } + } + if self.is_top() { + // For top hints, adjust character space position up by twice the + // darkening amount + self.cs_coord += twice(darken_y); + } + self.cs_coord += origin; + self.scale = scale; + self.index = index; + // If original stem hint was used, copy the position + if self.flags != 0 && stem.is_used { + if self.is_top() { + self.ds_coord = stem.ds_max; + } else { + self.ds_coord = stem.ds_min; + } + self.lock(); + } else { + self.ds_coord = self.cs_coord * scale; + } + } +} + +/// Collection of adjusted hint edges. +/// +/// +#[derive(Copy, Clone)] +struct HintMap { + edges: [Hint; MAX_HINTS], + len: usize, + is_valid: bool, + scale: Fixed, +} + +impl HintMap { + fn new(scale: Fixed) -> Self { + Self { + edges: [Hint::default(); MAX_HINTS], + len: 0, + is_valid: false, + scale, + } + } + + fn clear(&mut self) { + self.len = 0; + self.is_valid = false; + } + + /// Transform character space coordinate to device space. + /// + /// Based on + fn transform(&self, coord: Fixed) -> Fixed { + if self.len == 0 { + return coord * self.scale; + } + let limit = self.len - 1; + let mut i = 0; + while i < limit && coord >= self.edges[i + 1].cs_coord { + i += 1; + } + while i > 0 && coord < self.edges[i].cs_coord { + i -= 1; + } + let first_edge = &self.edges[0]; + if i == 0 && coord < first_edge.cs_coord { + // Special case for points below first edge: use uniform scale + ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord + } else { + // Use highest edge where cs_coord >= edge.cs_coord + let edge = &self.edges[i]; + ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord + } + } + + /// Insert hint edges into map, sorted by character space coordinate. + /// + /// Based on + fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) { + let (is_pair, mut first_edge) = if !bottom.is_valid() { + // Bottom is invalid: insert only top edge + (false, *top) + } else if !top.is_valid() { + // Top is invalid: insert only bottom edge + (false, *bottom) + } else { + // We have a valid pair! + (true, *bottom) + }; + let mut second_edge = *top; + if is_pair && top.cs_coord < bottom.cs_coord { + // Paired edges must be in proper order. FT just ignores the hint. + return; + } + let edge_count = if is_pair { 2 } else { 1 }; + if self.len + edge_count > MAX_HINTS { + // Won't fit. Again, ignore. + return; + } + // Find insertion index that keeps the edge list sorted + let mut insert_ix = 0; + while insert_ix < self.len { + if self.edges[insert_ix].cs_coord >= first_edge.cs_coord { + break; + } + insert_ix += 1; + } + // Discard hints that overlap in character space + if insert_ix < self.len { + let current = &self.edges[insert_ix]; + // Existing edge is the same + if (current.cs_coord == first_edge.cs_coord) + // Pair straddles the next edge + || (is_pair && current.cs_coord <= second_edge.cs_coord) + // Inserting between paired edges + || current.is_pair_top() + { + return; + } + } + // Recompute device space locations using initial hint map + if !first_edge.is_locked() { + if let Some(initial) = initial { + if is_pair { + // Preserve stem width: position center of stem with + // initial hint map and two edges with nominal scale + let mid = initial.transform( + first_edge.cs_coord + half(second_edge.cs_coord - first_edge.cs_coord), + ); + let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale; + first_edge.ds_coord = mid - half_width; + second_edge.ds_coord = mid + half_width; + } else { + first_edge.ds_coord = initial.transform(first_edge.cs_coord); + } + } + } + // Now discard hints that overlap in device space: + if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord { + // Inserting after an existing edge + return; + } + if insert_ix < self.len + && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord) + || first_edge.ds_coord > self.edges[insert_ix].ds_coord) + { + // Inserting before an existing edge + return; + } + // If we're inserting in the middle, make room in the edge array + if insert_ix != self.len { + let mut src_index = self.len - 1; + let mut dst_index = self.len + edge_count - 1; + loop { + self.edges[dst_index] = self.edges[src_index]; + if src_index == insert_ix { + break; + } + src_index -= 1; + dst_index -= 1; + } + } + self.edges[insert_ix] = first_edge; + if is_pair { + self.edges[insert_ix + 1] = second_edge; + } + self.len += edge_count; + } + + /// Adjust hint pairs so that one of the two edges is on a pixel boundary. + /// + /// Based on + fn adjust(&mut self) { + let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; + let mut saved_count = 0usize; + let mut i = 0; + // From FT with adjustments for variable names: + // "First pass is bottom-up (font hint order) without look-ahead. + // Locked edges are already adjusted. + // Unlocked edges begin with ds_coord from `initial_map'. + // Save edges that are not optimally adjusted in `saved' array, + // and process them in second pass." + let limit = self.len; + while i < limit { + let is_pair = self.edges[i].is_pair(); + let j = if is_pair { i + 1 } else { i }; + if !self.edges[i].is_locked() { + // We can adjust hint edges that are not locked + let frac_down = self.edges[i].ds_coord.fract(); + let frac_up = self.edges[j].ds_coord.fract(); + // There are four possibilities. We compute them all. + // (moves down are negative) + let down_move_down = Fixed::ZERO - frac_down; + let up_move_down = Fixed::ZERO - frac_up; + let down_move_up = if frac_down == Fixed::ZERO { + Fixed::ZERO + } else { + Fixed::ONE - frac_down + }; + let up_move_up = if frac_up == Fixed::ZERO { + Fixed::ZERO + } else { + Fixed::ONE - frac_up + }; + // Smallest move up + let move_up = down_move_up.min(up_move_up); + // Smallest move down + let move_down = down_move_down.max(up_move_down); + let mut save_edge = false; + let adjustment; + // Check for room to move up: + // 1. We're at the top of the array, or + // 2. The next edge is at or above the proposed move up + if j >= self.len - 1 + || self.edges[j + 1].ds_coord + >= (self.edges[j].ds_coord + move_up + MIN_COUNTER) + { + // Also check for room to move down... + if i == 0 + || self.edges[i - 1].ds_coord + <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) + { + // .. and move the smallest distance + adjustment = if -move_down < move_up { + move_down + } else { + move_up + }; + } else { + adjustment = move_up; + } + } else if i == 0 + || self.edges[i - 1].ds_coord + <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) + { + // We can move down + adjustment = move_down; + // True if the move is not optimum + save_edge = move_up < -move_down; + } else { + // We can't move either way without overlapping + adjustment = Fixed::ZERO; + save_edge = true; + } + // Capture non-optimal adjustments and save them for a second + // pass. This is only possible if the edge above is unlocked + // and can be moved. + if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() { + // (index, desired adjustment) + saved[saved_count] = (j, move_up - adjustment); + saved_count += 1; + } + // Apply the adjustment + self.edges[i].ds_coord += adjustment; + if is_pair { + self.edges[j].ds_coord += adjustment; + } + } + // Compute the new edge scale + if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord { + let a = self.edges[i]; + let b = self.edges[i - 1]; + self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); + } + if is_pair { + if self.edges[j].cs_coord != self.edges[j - 1].cs_coord { + let a = self.edges[j]; + let b = self.edges[j - 1]; + self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); + } + i += 1; + } + i += 1; + } + // Second pass tries to move non-optimal edges up if the first + // pass created room + for (j, adjustment) in saved[..saved_count].iter().copied().rev() { + if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) { + self.edges[j].ds_coord += adjustment; + if self.edges[j].is_pair() { + self.edges[j - 1].ds_coord += adjustment; + } + } + } + } + + /// Builds a hintmap from hints and mask. + /// + /// If `initial_map` is invalid, this recurses one level to initialize + /// it. If `is_initial` is true, simply build the initial map. + /// + /// Based on + fn build( + &mut self, + state: &HintState, + mask: Option, + mut initial_map: Option<&mut HintMap>, + stems: &mut [StemHint], + origin: Fixed, + is_initial: bool, + ) { + let scale = state.scale; + let darken_y = Fixed::ZERO; + if !is_initial { + if let Some(initial_map) = &mut initial_map { + if !initial_map.is_valid { + // Note: recursive call here to build the initial map if it + // is provided and invalid + initial_map.build(state, Some(HintMask::all()), None, stems, origin, true); + } + } + } + let initial_map = initial_map.map(|x| x as &HintMap); + self.clear(); + // If the mask is missing or invalid, assume all hints are active + let mut mask = mask.unwrap_or_else(HintMask::all); + if !mask.is_valid { + mask = HintMask::all(); + } + if state.do_em_box_hints { + // FreeType generates these during blues initialization. Do + // it here just to avoid carrying the extra state in the + // already large HintState struct. + // + let mut bottom = Hint::default(); + bottom.cs_coord = ICF_BOTTOM - EPSILON; + bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER; + bottom.scale = scale; + bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; + let mut top = Hint::default(); + top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y); + top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER; + top.scale = scale; + top.flags = GHOST_TOP | LOCKED | SYNTHETIC; + let invalid = Hint::default(); + self.insert(&bottom, &invalid, initial_map); + self.insert(&invalid, &top, initial_map); + } + let mut tmp_mask = mask; + // FreeType iterates over the hint mask with some fancy bit logic. We + // do the simpler thing and loop over the stems. + // + for (i, stem) in stems.iter().enumerate() { + if !tmp_mask.get(i) { + continue; + } + let hint_ix = i as u8; + let mut bottom = Hint::default(); + let mut top = Hint::default(); + bottom.setup(stem, hint_ix, origin, scale, darken_y, true); + top.setup(stem, hint_ix, origin, scale, darken_y, false); + // Insert hints that are locked or captured by a blue zone + if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { + if is_initial { + self.insert(&bottom, &top, None); + } else { + self.insert(&bottom, &top, initial_map); + } + // Avoid processing this hint in the second pass + tmp_mask.clear(i); + } + } + if is_initial { + // Heuristic: insert a point at (0, 0) if it's not covered by a + // mapping. Ensures a lock at baseline for glyphs missing a + // baseline hint. + if self.len == 0 + || self.edges[0].cs_coord > Fixed::ZERO + || self.edges[self.len - 1].cs_coord < Fixed::ZERO + { + let edge = Hint { + flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, + scale, + ..Default::default() + }; + let invalid = Hint::default(); + self.insert(&edge, &invalid, None); + } + } else { + // Insert hints that were skipped in the first pass + for (i, stem) in stems.iter().enumerate() { + if !tmp_mask.get(i) { + continue; + } + let hint_ix = i as u8; + let mut bottom = Hint::default(); + let mut top = Hint::default(); + bottom.setup(stem, hint_ix, origin, scale, darken_y, true); + top.setup(stem, hint_ix, origin, scale, darken_y, false); + self.insert(&bottom, &top, initial_map); + } + } + // Adjust edges that are not locked to blue zones + self.adjust(); + if !is_initial { + // Save position of edges that were used by the hint map. + for edge in &self.edges[..self.len] { + if edge.is_synthetic() { + continue; + } + let stem = &mut stems[edge.index as usize]; + if edge.is_top() { + stem.ds_max = edge.ds_coord; + } else { + stem.ds_min = edge.ds_coord; + } + stem.is_used = true; + } + } + self.is_valid = true; + } +} + +/// Bitmask that specifies which hints are currently active. +/// +/// "Each bit of the mask, starting with the most-significant bit of +/// the first byte, represents the corresponding hint zone in the +/// order in which the hints were declared at the beginning of +/// the charstring." +/// +/// See +/// Also +#[derive(Copy, Clone, PartialEq, Default)] +struct HintMask { + mask: [u8; HINT_MASK_SIZE], + is_valid: bool, +} + +impl HintMask { + fn new(bytes: &[u8]) -> Option { + let len = bytes.len(); + if len > HINT_MASK_SIZE { + return None; + } + let mut mask = Self::default(); + mask.mask[..len].copy_from_slice(&bytes[..len]); + mask.is_valid = true; + Some(mask) + } + + fn all() -> Self { + Self { + mask: [0xFF; HINT_MASK_SIZE], + is_valid: true, + } + } + + fn clear(&mut self, bit: usize) { + self.mask[bit >> 3] &= !msb_mask(bit); + } + + fn get(&self, bit: usize) -> bool { + self.mask[bit >> 3] & msb_mask(bit) != 0 + } +} + +/// Returns a bit mask for the selected bit with the +/// most significant bit at index 0. +fn msb_mask(bit: usize) -> u8 { + 1 << (7 - (bit & 0x7)) +} + +pub(super) struct HintingSink<'a, S> { + state: &'a HintState, + sink: &'a mut S, + stem_hints: [StemHint; MAX_HINTS], + stem_count: u8, + mask: HintMask, + initial_map: HintMap, + map: HintMap, + /// Most recent move_to in character space. + start_point: Option<[Fixed; 2]>, + /// Most recent line_to. First two elements are coords in character + /// space and the last two are in device space. + pending_line: Option<[Fixed; 4]>, +} + +impl<'a, S: CommandSink> HintingSink<'a, S> { + pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { + let scale = state.scale; + Self { + state, + sink, + stem_hints: [StemHint::default(); MAX_HINTS], + stem_count: 0, + mask: HintMask::all(), + initial_map: HintMap::new(scale), + map: HintMap::new(scale), + start_point: None, + pending_line: None, + } + } + + pub fn finish(&mut self) { + self.maybe_close_subpath(); + } + + fn maybe_close_subpath(&mut self) { + // This requires some explanation. The hint mask can be modified + // during charstring evaluation which changes the set of hints that + // are applied. FreeType ensures that the closing line for any subpath + // is transformed with the same hint map as the starting point for the + // subpath. This is done by stashing a copy of the hint map that is + // active when a new subpath is started. Unlike FreeType, we make use + // of close elements, so we can cheat a bit here and avoid the + // extra hintmap. If we're closing an open subpath and have a pending + // line and the line is not equal to the start point in character + // space, then we emit the saved device space coordinates for the + // line. If the coordinates do match in character space, we omit + // that line. The unconditional close command ensures that the + // start and end points coincide. + // Note: this doesn't apply to subpaths that end in cubics. + match (self.start_point.take(), self.pending_line.take()) { + (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => { + if start != [cs_x, cs_y] { + self.sink.line_to(ds_x, ds_y); + } + self.sink.close(); + } + (Some(_), _) => self.sink.close(), + _ => {} + } + } + + fn flush_pending_line(&mut self) { + if let Some([_, _, x, y]) = self.pending_line.take() { + self.sink.line_to(x, y); + } + } + + fn hint(&mut self, coord: Fixed) -> Fixed { + if !self.map.is_valid { + self.build_hint_map(Some(self.mask), Fixed::ZERO); + } + trunc(self.map.transform(coord)) + } + + fn scale(&self, coord: Fixed) -> Fixed { + trunc(coord * self.state.scale) + } + + fn add_stem(&mut self, min: Fixed, max: Fixed) { + let index = self.stem_count as usize; + if index >= MAX_HINTS || self.map.is_valid { + return; + } + let stem = &mut self.stem_hints[index]; + stem.min = min; + stem.max = max; + stem.is_used = false; + stem.ds_min = Fixed::ZERO; + stem.ds_max = Fixed::ZERO; + self.stem_count = index as u8 + 1; + } + + fn build_hint_map(&mut self, mask: Option, origin: Fixed) { + self.map.build( + self.state, + mask, + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + origin, + false, + ); + } +} + +impl<'a, S: CommandSink> CommandSink for HintingSink<'a, S> { + fn hstem(&mut self, min: Fixed, max: Fixed) { + self.add_stem(min, max); + } + + fn hint_mask(&mut self, mask: &[u8]) { + // For invalid hint masks, FreeType assumes all hints are active. + // See + let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); + if mask != self.mask { + self.mask = mask; + self.map.is_valid = false; + } + } + + fn counter_mask(&mut self, mask: &[u8]) { + // For counter masks, we build a temporary hint map "just to + // place and lock those stems participating in the counter + // mask." Building the map modifies the stem hint array as a + // side effect. + // See + let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); + let mut map = HintMap::new(self.state.scale); + map.build( + self.state, + Some(mask), + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + Fixed::ZERO, + false, + ); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.maybe_close_subpath(); + self.start_point = Some([x, y]); + let x = self.scale(x); + let y = self.hint(y); + self.sink.move_to(x, y); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.flush_pending_line(); + let ds_x = self.scale(x); + let ds_y = self.hint(y); + self.pending_line = Some([x, y, ds_x, ds_y]); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.flush_pending_line(); + let cx1 = self.scale(cx1); + let cy1 = self.hint(cy1); + let cx2 = self.scale(cx2); + let cy2 = self.hint(cy2); + let x = self.scale(x); + let y = self.hint(y); + self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + // We emit close commands based on the sequence of moves. + // See `maybe_close_subpath` + } +} + +/// FreeType converts from 16.16 to 26.6 by truncation. We keep our +/// values in 16.16 so simply zero the low 10 bits to match the +/// precision when converting to f32. +fn trunc(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() & !0x3FF) +} + +fn half(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() / 2) +} + +fn twice(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits().wrapping_mul(2)) +} + +#[cfg(test)] +mod tests { + use read_fonts::{tables::postscript::charstring::CommandSink, types::F2Dot14, FontRef}; + + use super::{ + BlueZone, Blues, Fixed, Hint, HintMap, HintMask, HintParams, HintState, HintingSink, + StemHint, GHOST_BOTTOM, GHOST_TOP, HINT_MASK_SIZE, LOCKED, PAIR_BOTTOM, PAIR_TOP, + }; + + fn make_hint_state() -> HintState { + fn make_blues(values: &[f64]) -> Blues { + Blues::new(values.iter().copied().map(Fixed::from_f64)) + } + // + // + // + // + // + let params = HintParams { + blues: make_blues(&[ + -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, + ]), + other_blues: make_blues(&[-255.0, -240.0]), + blue_scale: Fixed::from_f64(0.05), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ZERO, + ..Default::default() + }; + HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)) + } + + #[test] + fn scaled_blue_zones() { + let state = make_hint_state(); + assert!(!state.do_em_box_hints); + assert_eq!(state.zone_count, 6); + assert_eq!(state.boost, Fixed::from_bits(27035)); + assert!(state.supress_overshoot); + // FreeType generates the following zones: + let expected_zones = &[ + // csBottomEdge -983040 int + // csTopEdge 0 int + // csFlatEdge 0 int + // dsFlatEdge 0 int + // bottomZone 1 '\x1' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(-983040), + is_bottom: true, + ..Default::default() + }, + // csBottomEdge 35127296 int + // csTopEdge 35848192 int + // csFlatEdge 35127296 int + // dsFlatEdge 589824 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(35127296), + cs_top_edge: Fixed::from_bits(35848192), + cs_flat_edge: Fixed::from_bits(35127296), + ds_flat_edge: Fixed::from_bits(589824), + is_bottom: false, + }, + // csBottomEdge 37421056 int + // csTopEdge 38141952 int + // csFlatEdge 37421056 int + // dsFlatEdge 589824 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(37421056), + cs_top_edge: Fixed::from_bits(38141952), + cs_flat_edge: Fixed::from_bits(37421056), + ds_flat_edge: Fixed::from_bits(589824), + is_bottom: false, + }, + // csBottomEdge 46792704 int + // csTopEdge 47579136 int + // csFlatEdge 46792704 int + // dsFlatEdge 786432 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(46792704), + cs_top_edge: Fixed::from_bits(47579136), + cs_flat_edge: Fixed::from_bits(46792704), + ds_flat_edge: Fixed::from_bits(786432), + is_bottom: false, + }, + // csBottomEdge 49807360 int + // csTopEdge 50593792 int + // csFlatEdge 49807360 int + // dsFlatEdge 786432 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(49807360), + cs_top_edge: Fixed::from_bits(50593792), + cs_flat_edge: Fixed::from_bits(49807360), + ds_flat_edge: Fixed::from_bits(786432), + is_bottom: false, + }, + // csBottomEdge -16711680 int + // csTopEdge -15728640 int + // csFlatEdge -15728640 int + // dsFlatEdge -262144 int + // bottomZone 1 '\x1' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(-16711680), + cs_top_edge: Fixed::from_bits(-15728640), + cs_flat_edge: Fixed::from_bits(-15728640), + ds_flat_edge: Fixed::from_bits(-262144), + is_bottom: true, + }, + ]; + assert_eq!(state.zones(), expected_zones); + } + + #[test] + fn blue_zone_capture() { + let state = make_hint_state(); + let bottom_edge = Hint { + flags: PAIR_BOTTOM, + ds_coord: Fixed::from_f64(2.3), + ..Default::default() + }; + let top_edge = Hint { + flags: PAIR_TOP, + // This value chosen to fit within the first "top" blue zone + cs_coord: Fixed::from_bits(35127297), + ds_coord: Fixed::from_f64(2.3), + ..Default::default() + }; + // Capture both + { + let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge); + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(bottom_edge.is_locked()); + assert!(top_edge.is_locked()); + } + // Capture none + { + // Used to guarantee the edges are below all blue zones and will + // not be captured + let min_cs_coord = Fixed::MIN; + let mut bottom_edge = Hint { + cs_coord: min_cs_coord, + ..bottom_edge + }; + let mut top_edge = Hint { + cs_coord: min_cs_coord, + ..top_edge + }; + assert!(!state.capture(&mut bottom_edge, &mut top_edge)); + assert!(!bottom_edge.is_locked()); + assert!(!top_edge.is_locked()); + } + // Capture bottom, ignore invalid top + { + let mut bottom_edge = bottom_edge; + let mut top_edge = Hint { + // Empty flags == invalid hint + flags: 0, + ..top_edge + }; + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(bottom_edge.is_locked()); + assert!(!top_edge.is_locked()); + } + // Capture top, ignore invalid bottom + { + let mut bottom_edge = Hint { + // Empty flags == invalid hint + flags: 0, + ..bottom_edge + }; + let mut top_edge = top_edge; + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(!bottom_edge.is_locked()); + assert!(top_edge.is_locked()); + } + } + + #[test] + fn hint_mask_ops() { + const MAX_BITS: usize = HINT_MASK_SIZE * 8; + let all_bits = HintMask::all(); + for i in 0..MAX_BITS { + assert!(all_bits.get(i)); + } + let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap(); + for i in 0..MAX_BITS { + assert_eq!(i & 1 != 0, odd_bits.get(i)); + } + let mut cleared_bits = odd_bits; + for i in 0..MAX_BITS { + if i & 1 != 0 { + cleared_bits.clear(i); + } + } + assert_eq!(cleared_bits.mask, HintMask::default().mask); + } + + #[test] + fn hint_mapping() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff_font = super::super::outlines::Outlines::new(&font).unwrap(); + let state = cff_font + .subfont(0, 8.0, &[F2Dot14::from_f32(-1.0); 2]) + .unwrap() + .hint_state; + let mut initial_map = HintMap::new(state.scale); + let mut map = HintMap::new(state.scale); + // Stem hints from Cantarell-VF.otf glyph id 2 + let mut stems = [ + StemHint { + min: Fixed::from_bits(1376256), + max: Fixed::ZERO, + ..Default::default() + }, + StemHint { + min: Fixed::from_bits(16318464), + max: Fixed::from_bits(17563648), + ..Default::default() + }, + StemHint { + min: Fixed::from_bits(45481984), + max: Fixed::from_bits(44171264), + ..Default::default() + }, + ]; + map.build( + &state, + Some(HintMask::all()), + Some(&mut initial_map), + &mut stems, + Fixed::ZERO, + false, + ); + // FT generates the following hint map: + // + // index csCoord dsCoord scale flags + // 0 0.00 0.00 526 gbL + // 1 249.00 250.14 524 pb + // 1 268.00 238.22 592 pt + // 2 694.00 750.41 524 gtL + let expected_edges = [ + Hint { + index: 0, + cs_coord: Fixed::from_f64(0.0), + ds_coord: Fixed::from_f64(0.0), + scale: Fixed::from_bits(526), + flags: GHOST_BOTTOM | LOCKED, + }, + Hint { + index: 1, + cs_coord: Fixed::from_bits(16318464), + ds_coord: Fixed::from_bits(131072), + scale: Fixed::from_bits(524), + flags: PAIR_BOTTOM, + }, + Hint { + index: 1, + cs_coord: Fixed::from_bits(17563648), + ds_coord: Fixed::from_bits(141028), + scale: Fixed::from_bits(592), + flags: PAIR_TOP, + }, + Hint { + index: 2, + cs_coord: Fixed::from_bits(45481984), + ds_coord: Fixed::from_bits(393216), + scale: Fixed::from_bits(524), + flags: GHOST_TOP | LOCKED, + }, + ]; + assert_eq!(expected_edges, &map.edges[..map.len]); + // And FT generates the following mappings + let mappings = [ + // (coord in font units, expected hinted coord in device space) in 16.16 + (0, 0), // 0 -> 0 + (44302336, 382564), // 676 -> 5.828125 + (45481984, 393216), // 694 -> 6 + (16318464, 131072), // 249 -> 2 + (17563648, 141028), // 268 -> 2.140625 + (49676288, 426752), // 758 -> 6.5 + (56754176, 483344), // 866 -> 7.375 + (57868288, 492252), // 883 -> 7.5 + (50069504, 429896), // 764 -> 6.546875 + ]; + for (coord, expected) in mappings { + assert_eq!( + map.transform(Fixed::from_bits(coord)), + Fixed::from_bits(expected) + ); + } + } + + /// HintingSink is mostly pass-through. This test captures the logic + /// around omission of pending lines that match subpath start. + /// See HintingSink::maybe_close_subpath for details. + #[test] + fn hinting_sink_omits_closing_line_that_matches_start() { + let state = HintState { + scale: Fixed::ONE, + ..Default::default() + }; + let mut path = Path::default(); + let mut sink = HintingSink::new(&state, &mut path); + let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; + let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)]; + let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; + let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)]; + let curve = [ + Fixed::from_f64(3.0), + Fixed::from_f64(4.0), + Fixed::from_f64(5.0), + Fixed::from_f64(6.0), + Fixed::from_f64(1.0), + Fixed::from_f64(2.0), + ]; + // First subpath, closing line matches start + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.line_to(line1_2[0], line1_2[1]); + // Second subpath, closing line does not match start + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.line_to(line3_4[0], line3_4[1]); + // Third subpath, ends with cubic. Still emits a close command + // even though end point matches start. + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]); + sink.finish(); + // Subpaths always end with a close command. If a final line coincides + // with the start of a subpath, it is omitted. + assert_eq!( + &path.0, + &[ + // First subpath + MoveTo(move1_2), + LineTo(line2_3), + // line1_2 is omitted + Close, + // Second subpath + MoveTo(move1_2), + LineTo(line2_3), + LineTo(line3_4), + Close, + // Third subpath + MoveTo(move1_2), + LineTo(line2_3), + CurveTo(curve), + Close, + ] + ); + } + + #[derive(Copy, Clone, PartialEq, Debug)] + enum Command { + MoveTo([Fixed; 2]), + LineTo([Fixed; 2]), + CurveTo([Fixed; 6]), + Close, + } + + use Command::*; + + #[derive(Default)] + struct Path(Vec); + + impl CommandSink for Path { + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.0.push(MoveTo([x, y])); + } + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.0.push(LineTo([x, y])); + } + fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) { + self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y])); + } + fn close(&mut self) { + self.0.push(Close); + } + } +} diff --git a/src/scale/cff3/mod.rs b/src/scale/cff3/mod.rs new file mode 100644 index 0000000..b2f6ae8 --- /dev/null +++ b/src/scale/cff3/mod.rs @@ -0,0 +1,137 @@ +mod hint; +mod outlines; + +pub(crate) use outlines::Outlines; + +use super::Outline; +use read_fonts::types::{F2Dot14, GlyphId}; + +pub struct SubfontCache { + entries: Vec, + max_entries: usize, + epoch: u64, +} + +impl SubfontCache { + pub fn new(max_entries: usize) -> Self { + Self { + entries: Vec::new(), + max_entries, + epoch: 0, + } + } + + pub fn scale( + &mut self, + outlines: &outlines::Outlines, + id: u64, + glyph_id: u16, + size: f32, + coords: &[i16], + hint: bool, + outline: &mut Outline, + ) -> Option<()> { + let epoch = self.epoch; + let gid = GlyphId::new(glyph_id); + let subfont_index = outlines.subfont_index(gid); + let (found, entry_index) = self.find_entry(id, subfont_index, coords, size); + let (subfont, coords) = if found { + let entry = &mut self.entries[entry_index]; + entry.epoch = epoch; + (&entry.subfont, &entry.coords) + } else { + self.epoch += 1; + let epoch = self.epoch; + if entry_index == self.entries.len() { + let coords: Vec = coords.iter().map(|x| F2Dot14::from_bits(*x)).collect(); + let subfont = outlines.subfont(subfont_index, size, &coords).ok()?; + self.entries.push(Entry { + id, + epoch, + subfont, + subfont_index, + size, + coords, + }); + let entry = &self.entries[entry_index]; + (&entry.subfont, &entry.coords) + } else { + let entry = &mut self.entries[entry_index]; + entry.id = u64::MAX; + entry.epoch = epoch; + entry.coords.clear(); + entry + .coords + .extend(coords.iter().map(|x| F2Dot14::from_bits(*x))); + entry.subfont = outlines.subfont(subfont_index, size, &entry.coords).ok()?; + entry.id = id; + entry.subfont_index = subfont_index; + entry.size = size; + (&entry.subfont, &entry.coords) + } + }; + outlines + .draw(subfont, gid, coords, hint, &mut OutlineBuilder(outline)) + .ok()?; + Some(()) + } + + fn find_entry(&self, id: u64, index: u32, coords: &[i16], size: f32) -> (bool, usize) { + let mut lowest_epoch = self.epoch; + let mut lowest_index = 0; + for (i, entry) in self.entries.iter().enumerate() { + if entry.id == id + && entry.subfont_index == index + && entry.size == size + && coords + .iter() + .map(|x| F2Dot14::from_bits(*x)) + .eq(entry.coords.iter().copied()) + { + return (true, i); + } + if entry.epoch < lowest_epoch { + lowest_epoch = entry.epoch; + lowest_index = i; + } + } + if self.entries.len() < self.max_entries { + lowest_index = self.entries.len(); + } + (false, lowest_index) + } +} + +struct Entry { + epoch: u64, + id: u64, + subfont: outlines::Subfont, + subfont_index: u32, + size: f32, + coords: Vec, +} + +struct OutlineBuilder<'a>(&'a mut Outline); + +impl read_fonts::types::Pen for OutlineBuilder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, y).into()); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x, y).into()); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.0.quad_to((cx0, cy0).into(), (x, y).into()); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0 + .curve_to((cx0, cy0).into(), (cx1, cy1).into(), (x, y).into()); + } + + fn close(&mut self) { + self.0.close(); + } +} diff --git a/src/scale/cff3/outlines.rs b/src/scale/cff3/outlines.rs new file mode 100644 index 0000000..5c0df21 --- /dev/null +++ b/src/scale/cff3/outlines.rs @@ -0,0 +1,709 @@ +//! Support for scaling CFF outlines. + +use std::ops::Range; + +use read_fonts::{ + tables::{ + cff::Cff, + cff2::Cff2, + postscript::{ + charstring::{self, CommandSink}, + dict, BlendState, Error, FdSelect, Index, + }, + variations::ItemVariationStore, + }, + types::{F2Dot14, Fixed, GlyphId, Pen}, + FontData, FontRead, TableProvider, +}; + +use super::hint::{HintParams, HintState, HintingSink}; + +/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. +/// +/// The skrifa crate provides a higher level interface for this that handles +/// caching and abstracting over the different outline formats. Consider using +/// that if detailed control over resources is not required. +/// +/// # Subfonts +/// +/// CFF tables can contain multiple logical "subfonts" which determine the +/// state required for processing some subset of glyphs. This state is +/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28) +/// operators to select an appropriate subfont for any given glyph identifier. +/// This process is exposed on this type with the +/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont +/// index for the requested glyph followed by using the +/// [`subfont`](Self::subfont) method to create an appropriately configured +/// subfont for that glyph. +#[derive(Clone)] +pub(crate) struct Outlines<'a> { + version: Version<'a>, + top_dict: TopDict<'a>, + units_per_em: u16, +} + +impl<'a> Outlines<'a> { + /// Creates a new scaler for the given font. + /// + /// This will choose an underyling CFF2 or CFF table from the font, in that + /// order. + pub fn new(font: &impl TableProvider<'a>) -> Result { + let units_per_em = font.head()?.units_per_em(); + if let Ok(cff2) = font.cff2() { + Self::from_cff2(cff2, units_per_em) + } else { + // "The Name INDEX in the CFF data must contain only one entry; + // that is, there must be only one font in the CFF FontSet" + // So we always pass 0 for Top DICT index when reading from an + // OpenType font. + // + Self::from_cff(font.cff()?, 0, units_per_em) + } + } + + pub fn from_cff( + cff1: Cff<'a>, + top_dict_index: usize, + units_per_em: u16, + ) -> Result { + let top_dict_data = cff1.top_dicts().get(top_dict_index)?; + let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; + Ok(Self { + version: Version::Version1(cff1), + top_dict, + units_per_em, + }) + } + + pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result { + let table_data = cff2.offset_data().as_bytes(); + let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true)?; + Ok(Self { + version: Version::Version2(cff2), + top_dict, + units_per_em, + }) + } + + pub fn is_cff2(&self) -> bool { + matches!(self.version, Version::Version2(_)) + } + + /// Returns the number of available glyphs. + pub fn glyph_count(&self) -> usize { + self.top_dict + .charstrings + .as_ref() + .map(|cs| cs.count() as usize) + .unwrap_or_default() + } + + /// Returns the number of available subfonts. + pub fn subfont_count(&self) -> u32 { + self.top_dict + .font_dicts + .as_ref() + .map(|font_dicts| font_dicts.count()) + // All CFF fonts have at least one logical subfont. + .unwrap_or(1) + } + + /// Returns the subfont (or Font DICT) index for the given glyph + /// identifier. + pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 { + // For CFF tables, an FDSelect index will be present for CID-keyed + // fonts. Otherwise, the Top DICT will contain an entry for the + // "global" Private DICT. + // See + // + // CFF2 tables always contain a Font DICT and an FDSelect is only + // present if the size of the DICT is greater than 1. + // See + // + // In both cases, we return a subfont index of 0 when FDSelect is missing. + self.top_dict + .fd_select + .as_ref() + .and_then(|select| select.font_index(glyph_id)) + .unwrap_or(0) as u32 + } + + /// Creates a new subfont for the given index, size, normalized + /// variation coordinates and hinting state. + /// + /// The index of a subfont for a particular glyph can be retrieved with + /// the [`subfont_index`](Self::subfont_index) method. + pub fn subfont(&self, index: u32, size: f32, coords: &[F2Dot14]) -> Result { + let private_dict_range = self.private_dict_range(index)?; + let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?; + let mut hint_params = HintParams::default(); + let mut subrs_offset = None; + let mut store_index = 0; + let blend_state = self + .top_dict + .var_store + .clone() + .map(|store| BlendState::new(store, coords, store_index)) + .transpose()?; + for entry in dict::entries(private_dict_data, blend_state) { + use dict::Entry::*; + match entry? { + BlueValues(values) => hint_params.blues = values, + FamilyBlues(values) => hint_params.family_blues = values, + OtherBlues(values) => hint_params.other_blues = values, + FamilyOtherBlues(values) => hint_params.family_blues = values, + BlueScale(value) => hint_params.blue_scale = value, + BlueShift(value) => hint_params.blue_shift = value, + BlueFuzz(value) => hint_params.blue_fuzz = value, + LanguageGroup(group) => hint_params.language_group = group, + // Subrs offset is relative to the private DICT + SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset), + VariationStoreIndex(index) => store_index = index, + _ => {} + } + } + let scale = if size <= 0.0 { + Fixed::ONE + } else { + // Note: we do an intermediate scale to 26.6 to ensure we + // match FreeType + Fixed::from_bits((size * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32) + }; + // When hinting, use a modified scale factor + // + let hint_scale = Fixed::from_bits((scale.to_bits() + 32) / 64); + let hint_state = HintState::new(&hint_params, hint_scale); + Ok(Subfont { + is_cff2: self.is_cff2(), + scale, + subrs_offset, + hint_state, + store_index, + }) + } + + /// Loads and scales an outline for the given subfont instance, glyph + /// identifier and normalized variation coordinates. + /// + /// Before calling this method, use [`subfont_index`](Self::subfont_index) + /// to retrieve the subfont index for the desired glyph and then + /// [`subfont`](Self::subfont) to create an instance of the subfont for a + /// particular size and location in variation space. + /// Creating subfont instances is not free, so this process is exposed in + /// discrete steps to allow for caching. + /// + /// The result is emitted to the specified pen. + pub fn draw( + &self, + subfont: &Subfont, + glyph_id: GlyphId, + coords: &[F2Dot14], + hint: bool, + pen: &mut impl Pen, + ) -> Result<(), Error> { + let charstring_data = self + .top_dict + .charstrings + .as_ref() + .ok_or(Error::MissingCharstrings)? + .get(glyph_id.to_u16() as usize)?; + let subrs = subfont.subrs(self)?; + let blend_state = subfont.blend_state(self, coords)?; + let mut pen_sink = charstring::PenSink::new(pen); + let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink); + if hint { + let mut hinting_adapter = + HintingSink::new(&subfont.hint_state, &mut simplifying_adapter); + charstring::evaluate( + charstring_data, + self.global_subrs(), + subrs, + blend_state, + &mut hinting_adapter, + )?; + hinting_adapter.finish(); + } else { + let mut scaling_adapter = + ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale); + charstring::evaluate( + charstring_data, + self.global_subrs(), + subrs, + blend_state, + &mut scaling_adapter, + )?; + } + simplifying_adapter.finish(); + Ok(()) + } + + fn offset_data(&self) -> FontData<'a> { + match &self.version { + Version::Version1(cff1) => cff1.offset_data(), + Version::Version2(cff2) => cff2.offset_data(), + } + } + + fn global_subrs(&self) -> Index<'a> { + match &self.version { + Version::Version1(cff1) => cff1.global_subrs().into(), + Version::Version2(cff2) => cff2.global_subrs().into(), + } + } + + fn private_dict_range(&self, subfont_index: u32) -> Result, Error> { + if let Some(font_dicts) = &self.top_dict.font_dicts { + // If we have a font dict array, extract the private dict range + // from the font dict at the given index. + let font_dict_data = font_dicts.get(subfont_index as usize)?; + let mut range = None; + for entry in dict::entries(font_dict_data, None) { + if let dict::Entry::PrivateDictRange(r) = entry? { + range = Some(r); + break; + } + } + range + } else { + // Last chance, use the private dict range from the top dict if + // available. + self.top_dict.private_dict_range.clone() + } + .ok_or(Error::MissingPrivateDict) + } +} + +#[derive(Clone)] +enum Version<'a> { + /// + Version1(Cff<'a>), + /// + Version2(Cff2<'a>), +} + +/// Specifies local subroutines and hinting parameters for some subset of +/// glyphs in a CFF or CFF2 table. +/// +/// This type is designed to be cacheable to avoid re-evaluating the private +/// dict every time a charstring is processed. +/// +/// For variable fonts, this is dependent on a location in variation space. +#[derive(Clone)] +pub(crate) struct Subfont { + is_cff2: bool, + scale: Fixed, + subrs_offset: Option, + pub(crate) hint_state: HintState, + store_index: u16, +} + +impl Subfont { + /// Returns the local subroutine index. + pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result>, Error> { + if let Some(subrs_offset) = self.subrs_offset { + let offset_data = scaler.offset_data().as_bytes(); + let index_data = offset_data.get(subrs_offset..).unwrap_or_default(); + Ok(Some(Index::new(index_data, self.is_cff2)?)) + } else { + Ok(None) + } + } + + /// Creates a new blend state for the given normalized variation + /// coordinates. + pub fn blend_state<'a>( + &self, + scaler: &Outlines<'a>, + coords: &'a [F2Dot14], + ) -> Result>, Error> { + if let Some(var_store) = scaler.top_dict.var_store.clone() { + Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) + } else { + Ok(None) + } + } +} + +/// Entries that we parse from the Top DICT that are required to support +/// charstring evaluation. +#[derive(Clone, Default)] +struct TopDict<'a> { + charstrings: Option>, + font_dicts: Option>, + fd_select: Option>, + private_dict_range: Option>, + var_store: Option>, +} + +impl<'a> TopDict<'a> { + fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result { + let mut items = TopDict::default(); + for entry in dict::entries(top_dict_data, None) { + match entry? { + dict::Entry::CharstringsOffset(offset) => { + items.charstrings = Some(Index::new( + table_data.get(offset..).unwrap_or_default(), + is_cff2, + )?); + } + dict::Entry::FdArrayOffset(offset) => { + items.font_dicts = Some(Index::new( + table_data.get(offset..).unwrap_or_default(), + is_cff2, + )?); + } + dict::Entry::FdSelectOffset(offset) => { + items.fd_select = Some(FdSelect::read(FontData::new( + table_data.get(offset..).unwrap_or_default(), + ))?); + } + dict::Entry::PrivateDictRange(range) => { + items.private_dict_range = Some(range); + } + dict::Entry::VariationStoreOffset(offset) if is_cff2 => { + items.var_store = Some(ItemVariationStore::read(FontData::new( + // IVS is preceded by a 2 byte length + table_data.get(offset + 2..).unwrap_or_default(), + ))?); + } + _ => {} + } + } + Ok(items) + } +} + +/// Command sink adapter that applies a scaling factor. +/// +/// This assumes a 26.6 scaling factor packed into a Fixed and thus, +/// this is not public and exists only to match FreeType's exact +/// scaling process. +struct ScalingSink26Dot6<'a, S> { + inner: &'a mut S, + scale: Fixed, +} + +impl<'a, S> ScalingSink26Dot6<'a, S> { + fn new(sink: &'a mut S, scale: Fixed) -> Self { + Self { scale, inner: sink } + } + + fn scale(&self, coord: Fixed) -> Fixed { + // The following dance is necessary to exactly match FreeType's + // application of scaling factors. This seems to be the result + // of merging the contributed Adobe code while not breaking the + // FreeType public API. + // + // The first two steps apply to both scaled and unscaled outlines: + // + // 1. Multiply by 1/64 + // + let a = coord * Fixed::from_bits(0x0400); + // 2. Truncate the bottom 10 bits. Combined with the division by 64, + // converts to font units. + // + let b = Fixed::from_bits(a.to_bits() >> 10); + if self.scale != Fixed::ONE { + // Scaled case: + // 3. Multiply by the original scale factor (to 26.6) + // + let c = b * self.scale; + // 4. Convert from 26.6 to 16.16 + Fixed::from_bits(c.to_bits() << 10) + } else { + // Unscaled case: + // 3. Convert from integer to 16.16 + Fixed::from_bits(b.to_bits() << 16) + } + } +} + +impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> { + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.inner.hstem(y, dy); + } + + fn vstem(&mut self, x: Fixed, dx: Fixed) { + self.inner.vstem(x, dx); + } + + fn hint_mask(&mut self, mask: &[u8]) { + self.inner.hint_mask(mask); + } + + fn counter_mask(&mut self, mask: &[u8]) { + self.inner.counter_mask(mask); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.inner.move_to(self.scale(x), self.scale(y)); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.inner.line_to(self.scale(x), self.scale(y)); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.inner.curve_to( + self.scale(cx1), + self.scale(cy1), + self.scale(cx2), + self.scale(cy2), + self.scale(x), + self.scale(y), + ); + } + + fn close(&mut self) { + self.inner.close(); + } +} + +/// Command sink adapter that supresses degenerate move and line commands. +/// +/// FreeType avoids emitting empty contours and zero length lines to prevent +/// artifacts when stem darkening is enabled. We don't support stem darkening +/// because it's not enabled by any of our clients but we remove the degenerate +/// elements regardless to match the output. +/// +/// See +struct NopFilteringSink<'a, S> { + start: Option<(Fixed, Fixed)>, + last: Option<(Fixed, Fixed)>, + pending_move: Option<(Fixed, Fixed)>, + inner: &'a mut S, +} + +impl<'a, S> NopFilteringSink<'a, S> +where + S: CommandSink, +{ + fn new(inner: &'a mut S) -> Self { + Self { + start: None, + last: None, + pending_move: None, + inner, + } + } + + fn flush_pending_move(&mut self) { + if let Some((x, y)) = self.pending_move.take() { + if let Some((last_x, last_y)) = self.start { + if self.last != self.start { + self.inner.line_to(last_x, last_y); + } + } + self.start = Some((x, y)); + self.last = None; + self.inner.move_to(x, y); + } + } + + pub fn finish(&mut self) { + match self.start { + Some((x, y)) if self.last != self.start => { + self.inner.line_to(x, y); + } + _ => {} + } + } +} + +impl<'a, S> CommandSink for NopFilteringSink<'a, S> +where + S: CommandSink, +{ + fn hstem(&mut self, y: Fixed, dy: Fixed) { + self.inner.hstem(y, dy); + } + + fn vstem(&mut self, x: Fixed, dx: Fixed) { + self.inner.vstem(x, dx); + } + + fn hint_mask(&mut self, mask: &[u8]) { + self.inner.hint_mask(mask); + } + + fn counter_mask(&mut self, mask: &[u8]) { + self.inner.counter_mask(mask); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.pending_move = Some((x, y)); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + if self.pending_move == Some((x, y)) { + return; + } + self.flush_pending_move(); + if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) { + return; + } + self.inner.line_to(x, y); + self.last = Some((x, y)); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.flush_pending_move(); + self.last = Some((x, y)); + self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + if self.pending_move.is_none() { + self.inner.close(); + self.start = None; + self.last = None; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use read_fonts::FontRef; + + #[test] + fn unscaled_scaling_sink_produces_integers() { + let nothing = &mut (); + let sink = ScalingSink26Dot6::new(nothing, Fixed::ONE); + for coord in [50.0, 50.1, 50.125, 50.5, 50.9] { + assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0); + } + } + + #[test] + fn scaled_scaling_sink() { + let ppem = 20.0; + let upem = 1000.0; + // match FreeType scaling with intermediate conversion to 26.6 + let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32); + let nothing = &mut (); + let sink = ScalingSink26Dot6::new(nothing, scale); + let inputs = [ + // input coord, expected scaled output + (0.0, 0.0), + (8.0, 0.15625), + (16.0, 0.3125), + (32.0, 0.640625), + (72.0, 1.4375), + (128.0, 2.5625), + ]; + for (coord, expected) in inputs { + assert_eq!( + sink.scale(Fixed::from_f64(coord)).to_f32(), + expected, + "scaling coord {coord}" + ); + } + } + + #[test] + fn read_cff_static() { + let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); + let cff = Outlines::new(&font).unwrap(); + assert!(!cff.is_cff2()); + assert!(cff.top_dict.var_store.is_none()); + assert!(cff.top_dict.font_dicts.is_none()); + assert!(cff.top_dict.private_dict_range.is_some()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 17); + } + + #[test] + fn read_cff2_static() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff = Outlines::new(&font).unwrap(); + assert!(cff.is_cff2()); + assert!(cff.top_dict.var_store.is_some()); + assert!(cff.top_dict.font_dicts.is_some()); + assert!(cff.top_dict.private_dict_range.is_none()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 0); + } + + #[test] + fn read_example_cff2_table() { + let cff = Outlines::from_cff2( + Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap(), + 1000, + ) + .unwrap(); + assert!(cff.is_cff2()); + assert!(cff.top_dict.var_store.is_some()); + assert!(cff.top_dict.font_dicts.is_some()); + assert!(cff.top_dict.private_dict_range.is_none()); + assert!(cff.top_dict.fd_select.is_none()); + assert_eq!(cff.subfont_count(), 1); + assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); + assert_eq!(cff.global_subrs().count(), 0); + } + + #[test] + fn cff2_variable_outlines_match_freetype() { + compare_glyphs( + font_test_data::CANTARELL_VF_TRIMMED, + font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, + ); + } + + #[test] + fn cff_static_outlines_match_freetype() { + compare_glyphs( + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, + ); + } + + /// For the given font data and extracted outlines, parse the extracted + /// outline data into a set of expected values and compare these with the + /// results generated by the scaler. + /// + /// This will compare all outlines at various sizes and (for variable + /// fonts), locations in variation space. + fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { + let font = FontRef::new(font_data).unwrap(); + let outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines); + let scaler = super::Outlines::new(&font).unwrap(); + let mut path = read_fonts::scaler_test::Path::default(); + for expected_outline in &outlines { + if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() { + continue; + } + path.elements.clear(); + let subfont = scaler + .subfont( + scaler.subfont_index(expected_outline.glyph_id), + expected_outline.size, + &expected_outline.coords, + ) + .unwrap(); + scaler + .draw( + &subfont, + expected_outline.glyph_id, + &expected_outline.coords, + false, + &mut path, + ) + .unwrap(); + if path.elements != expected_outline.path { + panic!( + "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}", + expected_outline.glyph_id, + expected_outline.size, + expected_outline.coords, + &path.elements, + &expected_outline.path + ); + } + } + } +} diff --git a/src/scale/mod.rs b/src/scale/mod.rs index a46a50b..0bf5599 100644 --- a/src/scale/mod.rs +++ b/src/scale/mod.rs @@ -219,8 +219,7 @@ pub mod image; pub mod outline; mod bitmap; -mod cff; -mod cff2; +mod cff3; mod color; mod glyf; mod proxy; @@ -287,8 +286,7 @@ pub struct ScaleContext { struct State { glyf_scaler: glyf::Scaler, - cff_scaler: cff::Scaler, - cff_cache: cff2::SubfontCache, + cff_cache: cff3::SubfontCache, scratch0: Vec, scratch1: Vec, outline: Outline, @@ -310,8 +308,7 @@ impl ScaleContext { fonts: FontCache::new(max_entries), state: State { glyf_scaler: glyf::Scaler::new(max_entries), - cff_scaler: cff::Scaler::new(max_entries), - cff_cache: cff2::SubfontCache::new(max_entries), + cff_cache: cff3::SubfontCache::new(max_entries), scratch0: Vec::new(), scratch1: Vec::new(), outline: Outline::new(), @@ -421,7 +418,7 @@ impl<'a> ScalerBuilder<'a> { 1. }; // Handle read-fonts conversion for CFF - let cff = if matches!(&self.proxy.outlines, OutlinesProxy::Cff(_)) { + let cff = if matches!(&self.proxy.outlines, OutlinesProxy::Cff) { let font = if self.font.offset == 0 { read_fonts::FontRef::new(self.font.data).ok() } else { @@ -435,7 +432,7 @@ impl<'a> ScalerBuilder<'a> { read_fonts::FontRef::from_index(self.font.data, index as u32).ok() }) }; - font.and_then(|font| cff2::Scaler::new(&font).ok()) + font.and_then(|font| cff3::Outlines::new(&font).ok()) } else { None }; @@ -461,7 +458,7 @@ pub struct Scaler<'a> { state: &'a mut State, font: FontRef<'a>, proxy: &'a ScalerProxy, - cff: Option>, + cff: Option>, id: u64, coords: &'a [i16], size: f32, @@ -541,7 +538,7 @@ impl<'a> Scaler<'a> { _ => &mut self.state.outline, }; match &self.proxy.outlines { - OutlinesProxy::Cff(_) if self.cff.is_some() => { + OutlinesProxy::Cff if self.cff.is_some() => { let cff_scaler = self.cff.as_ref().unwrap(); outline.begin_layer(color_index); if self @@ -1109,26 +1106,3 @@ fn fill_outline( } Some(()) } - -struct CffBuilder<'a> { - outline: &'a mut Outline, -} - -impl cff::GlyphSink for CffBuilder<'_> { - fn move_to(&mut self, x: f32, y: f32) { - self.outline.move_to(Point::new(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.outline.line_to(Point::new(x, y)); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - self.outline - .curve_to(Point::new(cx1, cy1), Point::new(cx2, cy2), Point::new(x, y)); - } - - fn close(&mut self) { - self.outline.close(); - } -} diff --git a/src/scale/proxy.rs b/src/scale/proxy.rs index 83aa5e2..c17d8ad 100644 --- a/src/scale/proxy.rs +++ b/src/scale/proxy.rs @@ -1,6 +1,7 @@ +use crate::internal::raw_tag; + use super::{ super::{metrics::MetricsProxy, strike::BitmapStrikesProxy, FontRef}, - cff::CffProxy, color::ColorProxy, glyf::GlyfProxy, }; @@ -8,7 +9,7 @@ use super::{ #[derive(Copy, Clone)] pub enum OutlinesProxy { None, - Cff(CffProxy), + Cff, Glyf(GlyfProxy), } @@ -25,8 +26,8 @@ impl ScalerProxy { pub fn from_font(font: &FontRef) -> Self { let outlines = if let Some(glyf) = GlyfProxy::from_font(font) { OutlinesProxy::Glyf(glyf) - } else if let Some(cff) = CffProxy::from_font(font) { - OutlinesProxy::Cff(cff) + } else if font.table(raw_tag(b"CFF ")).is_some() || font.table(raw_tag(b"CFF2")).is_some() { + OutlinesProxy::Cff } else { OutlinesProxy::None }; From 3199ff9c2ab45d71b8cb74054691fe69780eb6a5 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 30 Jan 2024 12:02:38 -0500 Subject: [PATCH 4/4] remove old CFF impl --- src/scale/cff/cff.rs | 1862 --------------------------- src/scale/cff/hint.rs | 1616 +++++++++++++++-------- src/scale/cff/mod.rs | 151 +-- src/scale/{cff3 => cff}/outlines.rs | 0 src/scale/cff2/hint.rs | 979 -------------- src/scale/cff2/mod.rs | 132 -- src/scale/cff2/scaler.rs | 588 --------- src/scale/cff3/hint.rs | 1479 --------------------- src/scale/cff3/mod.rs | 137 -- src/scale/mod.rs | 10 +- 10 files changed, 1131 insertions(+), 5823 deletions(-) delete mode 100644 src/scale/cff/cff.rs rename src/scale/{cff3 => cff}/outlines.rs (100%) delete mode 100644 src/scale/cff2/hint.rs delete mode 100644 src/scale/cff2/mod.rs delete mode 100644 src/scale/cff2/scaler.rs delete mode 100644 src/scale/cff3/hint.rs delete mode 100644 src/scale/cff3/mod.rs diff --git a/src/scale/cff/cff.rs b/src/scale/cff/cff.rs deleted file mode 100644 index dff2ca5..0000000 --- a/src/scale/cff/cff.rs +++ /dev/null @@ -1,1862 +0,0 @@ -//! Adobe compact font format. - -use super::hint::{Hinter, HinterState}; -use super::internal::*; -use crate::{FontRef, GlyphId}; - -use super::TRACE; - -use core::ops::Range; - -pub const CFF_: RawTag = raw_tag(b"CFF "); -pub const CFF2: RawTag = raw_tag(b"CFF2"); - -/// Compact font format (CFF or CFF2). -#[derive(Copy, Clone)] -pub struct Cff<'a> { - data: &'a [u8], - proxy: CffProxy, -} - -impl<'a> Cff<'a> { - /// Returns the glyph with the specified glyph id. - pub fn get(&self, glyph_id: GlyphId) -> Option { - Glyph::new(self.data, &self.proxy, glyph_id) - } -} - -/// Proxy for rematerializing a CFF table. -#[derive(Copy, Clone)] -pub struct CffProxy { - cff: u32, - gsubrs: Index, - subrs: Index, - char_strings: Index, - fd_array: Index, - fd_select: u32, - private: (u32, u32), - vstore: u32, - vsindex: u16, - font_matrix: Option, - is_cff2: bool, - is_simple: bool, - units_per_em: u16, -} - -impl CffProxy { - pub fn from_font<'a>(font: &FontRef<'a>) -> Option { - let mut offset = font.table_offset(CFF2); - if offset == 0 { - offset = font.table_offset(CFF_); - } - if offset == 0 { - return None; - } - let units_per_em = font.head().map(|head| head.units_per_em()).unwrap_or(1000); - - Self::parse(font.data, offset, units_per_em) - } - - pub fn materialize<'a>(&self, font: &FontRef<'a>) -> Cff<'a> { - let data = font.data.get(self.cff as usize..).unwrap_or(&[]); - Cff { data, proxy: *self } - } - - fn parse(data: &[u8], cff: u32, units_per_em: u16) -> Option { - let data = data.get(cff as usize..)?; - let mut c = Stream::new(data); - let major = c.read::()?; - let _minor = c.read::()?; - let is_cff2 = if major == 2 { - true - } else if major != 1 { - return None; - } else { - false - }; - let header_size = c.read::()? as usize; - let top_dict_range; - let gsubrs = if is_cff2 { - if header_size < 5 { - return None; - } - let top_dict_len = c.read::()? as usize; - c.set_offset(header_size)?; - let start = c.offset(); - top_dict_range = start..start + top_dict_len; - c.skip(top_dict_len); - Index::new2(data, c.offset() as u32)? - } else { - if header_size < 4 { - return None; - } - c.set_offset(header_size)?; - c.skip(Index::new(data, c.offset() as u32)?.len(data)? as usize)?; - let top_idx = Index::new(data, c.offset() as u32)?; - if top_idx.count(data) != 1 { - return None; - } - c.skip(top_idx.len(data)?)?; - top_dict_range = top_idx.get_range(data, 0)?; - c.skip(Index::new(data, c.offset() as u32)?.len(data)? as usize)?; - Index::new(data, c.offset() as u32)? - }; - let mut loader = Loader { - char_strings: 0, - subrs: 0, - private: 0..0, - fd_array: 0, - fd_select: 0, - font_matrix: None, - vstore: 0, - vsindex: 0, - ok: true, - units_per_em, - }; - parse_dict(data, top_dict_range, None, &mut loader)?; - if !loader.ok { - return None; - } - let mut is_simple = !is_cff2; - let mut fd_array = Index::empty(); - let mut private = (0u32, 0u32); - let mut vstore = 0; - if is_cff2 { - if loader.vstore != 0 { - vstore = loader.vstore as u32 + 2; - } - fd_array = Index::new2(data, loader.fd_array as u32)?; - if fd_array.count(data) == 0 { - is_simple = true; - } else if fd_array.count(data) == 1 { - is_simple = true; - let font_dict_range = fd_array.get_range(data, 0)?; - parse_dict(data, font_dict_range, None, &mut loader)?; - private = (loader.private.start as u32, loader.private.end as u32); - if private.0 != 0 { - let blend = if vstore != 0 { - Some(BlendData::new(data, vstore, &[])) - } else { - None - }; - parse_dict(data, loader.private.clone(), blend, &mut loader)?; - } - } else if loader.fd_select == 0 { - return None; - } - } else { - private = (loader.private.start as u32, loader.private.end as u32); - if loader.fd_array != 0 { - fd_array = Index::new(data, loader.fd_array as u32)?; - is_simple = false; - } - } - if private.0 != 0 { - parse_dict(data, loader.private.clone(), None, &mut loader)?; - } - if !loader.ok || loader.char_strings == 0 { - return None; - } - let char_strings = if is_cff2 { - Index::new2(data, loader.char_strings as u32)? - } else { - Index::new(data, loader.char_strings as u32)? - }; - let subrs = if loader.subrs != 0 { - if is_cff2 { - Index::new2(data, loader.subrs as u32)? - } else { - Index::new(data, loader.subrs as u32)? - } - } else { - Index::empty() - }; - Some(Self { - cff, - gsubrs, - subrs, - char_strings, - fd_array, - fd_select: loader.fd_select as u32, - font_matrix: loader.font_matrix, - private, - vstore, - vsindex: loader.vsindex, - is_cff2, - is_simple, - units_per_em: loader.units_per_em, - }) - } -} - -/// Compact font format glyph outline. -#[derive(Copy, Clone)] -pub struct Glyph<'a> { - data: &'a [u8], - proxy: CffProxy, - fd: u16, - private: (u32, u32), - vsindex: u16, - subrs: Index, - range: (u32, u32), -} - -impl<'a> Glyph<'a> { - /// Returns a char string for the specified CFF table and glyph id. - fn new(data: &'a [u8], proxy: &CffProxy, glyph_id: GlyphId) -> Option { - let range = proxy.char_strings.get_range(data, glyph_id as u32)?; - let mut fd = 0u16; - let mut private = proxy.private; - let mut vsindex = proxy.vsindex; - let mut subrs = proxy.subrs; - if !proxy.is_simple { - fd = parse_fd_select(data, proxy.fd_select, glyph_id)? as u16; - let mut loader = Loader { - char_strings: 0, - subrs: 0, - private: 0..0, - fd_array: 0, - fd_select: 0, - font_matrix: None, - vstore: 0, - vsindex, - ok: true, - units_per_em: proxy.units_per_em, - }; - let fd_range = proxy.fd_array.get_range(data, fd as u32)?; - let blend = if proxy.vstore != 0 { - Some(BlendData::new(data, proxy.vstore, &[])) - } else { - None - }; - parse_dict(data, fd_range, blend, &mut loader)?; - private = (loader.private.start as u32, loader.private.end as u32); - parse_dict(data, loader.private.clone(), blend, &mut loader)?; - vsindex = loader.vsindex; - if loader.subrs != 0 { - subrs = if proxy.is_cff2 { - Index::new2(data, loader.subrs as u32)? - } else { - Index::new(data, loader.subrs as u32)? - }; - } - } - Some(Self { - data, - proxy: *proxy, - fd, - private, - vsindex, - subrs, - range: (range.start as u32, range.end as u32), - }) - } - - /// Returns the index of the associated font dictionary. - pub fn subfont_index(&self) -> u16 { - self.fd - } - - /// Computes the bounding box of the glyph. - pub fn _bounds(&self, scale: f32, coords: &[i16]) -> [f32; 4] { - let mut sink = PathBounds { - some: false, - xmin: core::f32::MAX, - xmax: core::f32::MIN, - ymin: core::f32::MAX, - ymax: core::f32::MIN, - }; - if let Some(xform) = self.proxy.font_matrix { - let xform = Transform::combine(&xform, &Transform::scale(scale)); - let mut sink = TransformSink { - xform, - inner: &mut sink, - }; - self.parse(coords, &mut sink); - } else if scale != 1. { - let mut sink = ScaleSink { - scale, - inner: &mut sink, - }; - self.parse(coords, &mut sink); - } else { - self.parse(coords, &mut sink); - } - if sink.some { - [sink.xmin, sink.xmax, sink.ymin, sink.ymax] - } else { - [0.; 4] - } - } - - /// Evaluates the glyph, directing the result to the specified sink. - pub fn path( - &self, - scale: f32, - coords: &[i16], - hinting: Option<&HinterState>, - sink: &mut impl GlyphSink, - ) -> bool { - if let Some(state) = hinting { - let scale = state.scale(); - if let Some(mut xform) = self.proxy.font_matrix { - xform.x *= scale; - xform.y *= scale; - let mut sink = TransformSink { xform, inner: sink }; - let mut hinter = Hinter::new(state, &mut sink); - self.parse(coords, &mut hinter) - } else { - let mut hinter = Hinter::new(state, sink); - self.parse(coords, &mut hinter) - } - } else if let Some(xform) = self.proxy.font_matrix { - let xform = Transform::combine(&xform, &Transform::scale(scale)); - let mut sink = TransformSink { xform, inner: sink }; - self.parse(coords, &mut sink) - } else { - let mut sink = ScaleSink { scale, inner: sink }; - self.parse(coords, &mut sink) - } - } - - /// Attempts to parse the private dictionary associated with the char string, - /// directing the results to the supplied sink. - pub(super) fn eval_private_dict(&self, coords: &[i16], sink: &mut impl DictionarySink) -> bool { - if self.private.0 == 0 { - return true; - } - let blend = if self.proxy.vstore != 0 { - Some(BlendData::new(self.data, self.proxy.vstore, coords)) - } else { - None - }; - let range = self.private.0 as usize..self.private.1 as usize; - parse_dict(self.data, range, blend, sink).is_some() - } - - pub(super) fn parse(&self, coords: &[i16], sink: &mut impl GlyphSink) -> bool { - if self.range.0 == self.range.1 { - return true; - } - let mut state = ParseState { - open: false, - have_width: false, - stem_count: 0, - vsindex: self.vsindex, - }; - let blend = if self.proxy.vstore != 0 && !coords.is_empty() { - Some(BlendData::new(self.data, self.proxy.vstore, coords)) - } else { - None - }; - let mut blend_state = BlendState::new(); - let range = self.range.0 as usize..self.range.1 as usize; - let mut stack = Stack::new(); - self.parse_imp( - &mut state, - blend, - &mut blend_state, - &mut stack, - range, - 0, - 0., - 0., - sink, - ) - .is_some() - } - - fn parse_imp( - &self, - s: &mut ParseState, - blend: Option, - blend_state: &mut BlendState, - stack: &mut Stack, - range: Range, - depth: u32, - mut x: f32, - mut y: f32, - b: &mut impl GlyphSink, - ) -> Option<(f32, f32)> { - if depth > 10 { - return None; - } - use opcodes::*; - let mut c = Stream::with_range(self.data, range)?; - while c.remaining() != 0 { - let mut op = c.read::()? as u16; - if op == ESCAPE { - let b1 = c.read::()?; - op = b1 as u16 | 12 << 8; - } - if TRACE && op < 38 { - //println!("{}", opcodes::NAMES[op as usize]); - } - match op { - 0 | 2 | 9 | 13 | 17 => { - return None; - } - VSINDEX => { - if !self.proxy.is_cff2 { - return None; - } - if stack.is_empty() { - return None; - } - s.vsindex = stack.pop() as u16; - } - BLEND => { - if !self.proxy.is_cff2 { - return None; - } - let mut total = stack.len(); - if total < 1 { - return None; - } - total -= 1; - let count = stack.pop() as usize; - if count > total { - return None; - } - if let Some(ref blend) = blend { - total = blend_state.apply( - blend, - s.vsindex, - &mut stack.elements[0..total], - count, - )?; - } - stack.top -= total - count; - } - SHORTINT => { - stack.push(c.read::()? as f32)?; - } - 32..=246 => { - stack.push(op as f32 - 139.)?; - } - 247..=250 => { - let b1 = c.read::()? as f32; - stack.push((op as f32 - 247.) * 256. + b1 + 108.)?; - } - 251..=254 => { - let b1 = c.read::()? as f32; - stack.push(-((op as f32 - 251.) * 256. + b1) - 108.)?; - } - 255 => { - stack.push(c.read::()? as f32 / 65536.)?; - } - RETURN => { - break; - } - ENDCHAR => { - if !stack.is_empty() && !s.have_width { - s.have_width = true; - stack.clear(); - } - if s.open { - s.open = false; - b.close(); - } - break; - } - HSTEM | VSTEM | HSTEMHM | VSTEMHM => { - let mut i = 0; - let len = if stack.is_odd() && !s.have_width { - s.have_width = true; - i = 1; - stack.len() - 1 - } else { - stack.len() - }; - let horz = op == HSTEM || op == HSTEMHM; - let mut u = 0.; - while i < stack.len() { - u += stack.get(i); - let w = stack.get(i + 1); - let v = u + w; - if horz { - b.hstem(u, v); - } else { - b.vstem(u, v); - } - u = v; - i += 2; - } - s.stem_count += len / 2; - stack.clear(); - } - HINTMASK | CNTRMASK => { - let mut i = 0; - let len = if stack.is_odd() && !s.have_width { - s.have_width = true; - i = 1; - stack.len() - 1 - } else { - stack.len() - }; - let mut u = 0.; - while i < stack.len() { - u += stack.get(i); - let w = stack.get(i + 1); - let v = u + w; - b.vstem(u, v); - u = v; - i += 2; - } - s.stem_count += len / 2; - let count = (s.stem_count + 7) / 8; - let mask = c.read_bytes(count)?; - if op == HINTMASK { - b.hint_mask(mask); - } else { - b.counter_mask(mask); - } - stack.clear(); - } - RMOVETO => { - let mut i = 0; - if stack.len() == 3 && !s.have_width { - s.have_width = true; - i = 1; - } else if stack.len() < 2 { - return None; - } - if !s.open { - s.open = true; - } else { - b.close(); - } - x += stack.get(i); - y += stack.get(i + 1); - b.move_to(x, y); - stack.clear(); - } - HMOVETO | VMOVETO => { - let mut i = 0; - if stack.len() == 2 && !s.have_width { - s.have_width = true; - i = 1; - } else if stack.is_empty() { - return None; - } - if !s.open { - s.open = true; - } else { - b.close(); - } - if op == HMOVETO { - x += stack.get(i); - } else { - y += stack.get(i); - } - b.move_to(x, y); - stack.clear(); - } - RLINETO => { - if stack.is_odd() { - return None; - } - let mut i = 0; - while i < stack.len() { - x += stack.get(i); - y += stack.get(i + 1); - b.line_to(x, y); - i += 2; - } - stack.clear(); - } - HLINETO => { - let mut i = 0; - while i < stack.len() { - x += stack.get(i); - i += 1; - b.line_to(x, y); - if i == stack.len() { - break; - } - y += stack.get(i); - i += 1; - b.line_to(x, y); - } - stack.clear(); - } - VLINETO => { - let mut i = 0; - while i < stack.len() { - y += stack.get(i); - i += 1; - b.line_to(x, y); - if i == stack.len() { - break; - } - x += stack.get(i); - i += 1; - b.line_to(x, y); - } - stack.clear(); - } - RRCURVETO => { - if stack.len() % 6 != 0 { - return None; - } - let mut i = 0; - while i < stack.len() { - let x1 = x + stack.get(i); - let y1 = y + stack.get(i + 1); - let x2 = x1 + stack.get(i + 2); - let y2 = y1 + stack.get(i + 3); - x = x2 + stack.get(i + 4); - y = y2 + stack.get(i + 5); - b.curve_to(x1, y1, x2, y2, x, y); - i += 6; - } - stack.clear(); - } - RCURVELINE => { - if stack.len() < 8 || (stack.len() - 2) % 6 != 0 { - return None; - } - let mut i = 0; - while i < stack.len() - 2 { - let x1 = x + stack.get(i); - let y1 = y + stack.get(i + 1); - let x2 = x1 + stack.get(i + 2); - let y2 = y1 + stack.get(i + 3); - x = x2 + stack.get(i + 4); - y = y2 + stack.get(i + 5); - b.curve_to(x1, y1, x2, y2, x, y); - i += 6; - } - x += stack.get(i); - y += stack.get(i + 1); - b.line_to(x, y); - stack.clear(); - } - RLINECURVE => { - if stack.len() < 8 || (stack.len() - 6) & 1 != 0 { - return None; - } - let mut i = 0; - while i < stack.len() - 6 { - x += stack.get(i); - y += stack.get(i + 1); - b.line_to(x, y); - i += 2; - } - let x1 = x + stack.get(i); - let y1 = y + stack.get(i + 1); - let x2 = x1 + stack.get(i + 2); - let y2 = y1 + stack.get(i + 3); - x = x2 + stack.get(i + 4); - y = y2 + stack.get(i + 5); - b.curve_to(x1, y1, x2, y2, x, y); - stack.clear(); - } - VVCURVETO => { - let mut i = 0; - if stack.is_odd() { - x += stack.get(0); - i += 1; - } - if (stack.len() - i) % 4 != 0 { - return None; - } - while i < stack.len() { - let x1 = x; - let y1 = y + stack.get(i); - let x2 = x1 + stack.get(i + 1); - let y2 = y1 + stack.get(i + 2); - x = x2; - y = y2 + stack.get(i + 3); - b.curve_to(x1, y1, x2, y2, x, y); - i += 4; - } - stack.clear(); - } - HHCURVETO => { - let mut i = 0; - if stack.is_odd() { - y += stack.get(0); - i += 1; - } - if (stack.len() - i) % 4 != 0 { - return None; - } - while i < stack.len() { - let x1 = x + stack.get(i); - let y1 = y; - let x2 = x1 + stack.get(i + 1); - let y2 = y1 + stack.get(i + 2); - x = x2 + stack.get(i + 3); - y = y2; - b.curve_to(x1, y1, x2, y2, x, y); - i += 4; - } - stack.clear(); - } - VHCURVETO => { - if stack.len() < 4 { - return None; - } - stack.reverse(); - while !stack.is_empty() { - if stack.len() < 4 { - return None; - } - let x1 = x; - let y1 = y + stack.pop(); - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - x = x2 + stack.pop(); - y = y2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - if stack.is_empty() { - break; - } - if stack.len() < 4 { - return None; - } - let x1 = x + stack.pop(); - let y1 = y; - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - y = y2 + stack.pop(); - x = x2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - } - debug_assert!(stack.is_empty()); - } - HVCURVETO => { - if stack.len() < 4 { - return None; - } - stack.reverse(); - while !stack.is_empty() { - if stack.len() < 4 { - return None; - } - let x1 = x + stack.pop(); - let y1 = y; - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - y = y2 + stack.pop(); - x = x2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - if stack.is_empty() { - break; - } - if stack.len() < 4 { - return None; - } - let x1 = x; - let y1 = y + stack.pop(); - let x2 = x1 + stack.pop(); - let y2 = y1 + stack.pop(); - x = x2 + stack.pop(); - y = y2 + if stack.len() == 1 { stack.pop() } else { 0.0 }; - b.curve_to(x1, y1, x2, y2, x, y); - } - debug_assert!(stack.is_empty()); - } - HFLEX => { - if stack.len() != 7 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y; - let dx2 = dx1 + stack.get(1); - let dy2 = dy1 + stack.get(2); - let dx3 = dx2 + stack.get(3); - let dy3 = dy2; - let dx4 = dx3 + stack.get(4); - let dy4 = dy2; - let dx5 = dx4 + stack.get(5); - let dy5 = y; - x = dx5 + stack.get(6); - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - FLEX => { - if stack.len() != 13 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y + stack.get(1); - let dx2 = dx1 + stack.get(2); - let dy2 = dy1 + stack.get(3); - let dx3 = dx2 + stack.get(4); - let dy3 = dy2 + stack.get(5); - let dx4 = dx3 + stack.get(6); - let dy4 = dy3 + stack.get(7); - let dx5 = dx4 + stack.get(8); - let dy5 = dy4 + stack.get(9); - x = dx5 + stack.get(10); - y = dy5 + stack.get(11); - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - HFLEX1 => { - if stack.len() != 9 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y + stack.get(1); - let dx2 = dx1 + stack.get(2); - let dy2 = dy1 + stack.get(3); - let dx3 = dx2 + stack.get(4); - let dy3 = dy2; - let dx4 = dx3 + stack.get(5); - let dy4 = dy2; - let dx5 = dx4 + stack.get(6); - let dy5 = dy4 + stack.get(7); - x = dx5 + stack.get(8); - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - FLEX1 => { - if stack.len() != 11 { - return None; - } - let dx1 = x + stack.get(0); - let dy1 = y + stack.get(1); - let dx2 = dx1 + stack.get(2); - let dy2 = dy1 + stack.get(3); - let dx3 = dx2 + stack.get(4); - let dy3 = dy2 + stack.get(5); - let dx4 = dx3 + stack.get(6); - let dy4 = dy3 + stack.get(7); - let dx5 = dx4 + stack.get(8); - let dy5 = dy4 + stack.get(9); - fn abs(x: f32) -> f32 { - if x < 0. { - -x - } else { - x - } - } - if abs(dx5 - x) > abs(dy5 - y) { - x = dx5 + stack.get(10); - } else { - y = dy5 + stack.get(10); - } - b.curve_to(dx1, dy1, dx2, dy2, dx3, dy3); - b.curve_to(dx4, dy4, dx5, dy5, x, y); - stack.clear(); - } - CALLSUBR | CALLGSUBR => { - if stack.is_empty() { - return None; - } - let data = self.data; - let count = if op == CALLSUBR { - self.subrs.count(data) - } else { - self.proxy.gsubrs.count(data) - }; - let bias = if count < 1240 { - 107 - } else if count < 33900 { - 1131 - } else { - 32768 - }; - let index = stack.pop() as i32 + bias; - let range = if op == CALLSUBR { - self.subrs.get_range(data, index as u32)? - } else { - self.proxy.gsubrs.get_range(data, index as u32)? - }; - let pos = - self.parse_imp(s, blend, blend_state, stack, range, depth + 1, x, y, b)?; - x = pos.0; - y = pos.1; - } - _ => { - return None; - } - } - } - Some((x, y)) - } -} - -#[derive(Default)] -struct PathBounds { - some: bool, - xmin: f32, - xmax: f32, - ymin: f32, - ymax: f32, -} - -impl PathBounds { - fn insert(&mut self, x: f32, y: f32) { - if x < self.xmin { - self.xmin = x; - } - if x > self.xmax { - self.xmax = x; - } - if y < self.ymin { - self.ymin = y; - } - if y > self.ymax { - self.ymax = y; - } - } -} - -impl GlyphSink for PathBounds { - fn move_to(&mut self, x: f32, y: f32) { - self.some = true; - self.insert(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.some = true; - self.insert(x, y); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - self.some = true; - self.insert(cx1, cy1); - self.insert(cx2, cy2); - self.insert(x, y); - } - - fn close(&mut self) {} -} - -#[derive(Copy, Clone)] -struct Transform { - pub xx: f32, - pub xy: f32, - pub yx: f32, - pub yy: f32, - pub x: f32, - pub y: f32, -} - -impl Transform { - fn scale(scale: f32) -> Self { - Self { - xx: scale, - xy: 0., - yx: 0., - yy: scale, - x: 0., - y: 0., - } - } - - #[inline(always)] - fn transform_point(&self, x: f32, y: f32) -> (f32, f32) { - ( - (x * self.xx + y * self.yx) + self.x, - (x * self.xy + y * self.yy) + self.y, - ) - } - - #[allow(clippy::suspicious_operation_groupings)] - fn combine(a: &Transform, b: &Transform) -> Self { - let xx = a.xx * b.xx + a.yx * b.xy; - let yx = a.xx * b.yx + a.yx * b.yy; - let xy = a.xy * b.xx + a.yy * b.xy; - let yy = a.xy * b.yx + a.yy * b.yy; - let x = a.x * b.xx + a.y * b.xy + b.x; - let y = a.x * b.yx + a.y * b.yy + b.y; - Self { - xx, - xy, - yx, - yy, - x, - y, - } - } -} - -struct TransformSink<'a, S> { - xform: Transform, - inner: &'a mut S, -} - -impl<'a, S: GlyphSink> GlyphSink for TransformSink<'a, S> { - fn move_to(&mut self, x: f32, y: f32) { - let (x, y) = self.xform.transform_point(x, y); - self.inner.move_to(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - let (x, y) = self.xform.transform_point(x, y); - self.inner.line_to(x, y); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - let (cx1, cy1) = self.xform.transform_point(cx1, cy1); - let (cx2, cy2) = self.xform.transform_point(cx2, cy2); - let (x, y) = self.xform.transform_point(x, y); - self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) { - self.inner.close(); - } -} - -struct ScaleSink<'a, S> { - scale: f32, - inner: &'a mut S, -} - -impl<'a, S: GlyphSink> GlyphSink for ScaleSink<'a, S> { - fn move_to(&mut self, x: f32, y: f32) { - let s = self.scale; - self.inner.move_to(x * s, y * s); - } - - fn line_to(&mut self, x: f32, y: f32) { - let s = self.scale; - self.inner.line_to(x * s, y * s); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - let s = self.scale; - self.inner - .curve_to(cx1 * s, cy1 * s, cx2 * s, cy2 * s, x * s, y * s); - } - - fn close(&mut self) { - self.inner.close(); - } -} - -/// Trait for receiving the path commands of a glyph. -#[allow(unused_variables)] -pub trait GlyphSink { - fn hstem(&mut self, y: f32, dy: f32) {} - fn vstem(&mut self, x: f32, dx: f32) {} - fn hint_mask(&mut self, mask: &[u8]) {} - fn counter_mask(&mut self, mask: &[u8]) {} - fn move_to(&mut self, x: f32, y: f32); - fn line_to(&mut self, x: f32, y: f32); - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32); - fn close(&mut self); -} - -/// A trait that acts as a callback during dictionary parsing. -#[allow(unused_variables)] -pub trait DictionarySink { - /// Specifies the char string type. Only type 2 is supported. - fn char_string_type(&mut self, ty: u32) {} - - /// Specifies the offset to the char string index. - fn char_strings(&mut self, offset: usize) {} - - /// Specifies the offset and length of the private dictionary. - fn private_dictionary(&mut self, offset: usize, len: usize) {} - - /// Specifies the hinting alignment zones. - fn blue_values(&mut self, values: &[f32]) {} - - /// Specifies the hinting alignment zones for the font family. - fn family_blues(&mut self, values: &[f32]) {} - - /// Specifies other hinting alignment zones. - fn other_blues(&mut self, values: &[f32]) {} - - /// Specifies other hinting alignment zones for the font family. - fn family_other_blues(&mut self, values: &[f32]) {} - - /// Specifies a value for overshoot suppression. - fn blue_scale(&mut self, scale: f32) {} - - /// Specifies a value for small overshoot supression. - fn blue_shift(&mut self, shift: f32) {} - - /// Specifies an alignment zone extension. - fn blue_fuzz(&mut self, fuzz: f32) {} - - /// Specifies the offset to the local subroutine index. - fn subroutines(&mut self, offset: usize) {} - - /// Specifies the offset to the font dictionary index. - fn fd_array(&mut self, offset: usize) {} - - /// Specifies the offset to the font dictionary selector table. - fn fd_select(&mut self, offset: usize) {} - - fn font_matrix(&mut self, values: &[f32]) {} - - fn language_group(&mut self, group: u32) {} - - /// Specifies the offset to the variation store. - fn variation_store(&mut self, offset: usize) {} - - /// Specifies the outer index in the variation store. - fn variation_store_index(&mut self, index: u16) {} -} - -struct Loader { - char_strings: usize, - subrs: usize, - private: Range, - fd_select: usize, - fd_array: usize, - font_matrix: Option, - vstore: usize, - vsindex: u16, - ok: bool, - units_per_em: u16, -} - -impl DictionarySink for Loader { - fn char_string_type(&mut self, ty: u32) { - if ty != 2 { - self.ok = false; - } - } - - fn char_strings(&mut self, offset: usize) { - self.char_strings = offset; - } - - fn private_dictionary(&mut self, offset: usize, len: usize) { - self.private = offset..offset + len; - } - - fn subroutines(&mut self, offset: usize) { - self.subrs = self.private.start + offset; - } - - fn fd_select(&mut self, offset: usize) { - self.fd_select = offset; - } - - fn fd_array(&mut self, offset: usize) { - self.fd_array = offset; - } - - fn font_matrix(&mut self, values: &[f32]) { - if values.len() == 6 { - self.font_matrix = Some(Transform { - xx: values[0] * self.units_per_em as f32, - xy: values[1] * self.units_per_em as f32, - yx: values[2] * self.units_per_em as f32, - yy: values[3] * self.units_per_em as f32, - x: values[4], - y: values[5], - }); - } - } - - fn variation_store(&mut self, offset: usize) { - self.vstore = offset; - } - - fn variation_store_index(&mut self, index: u16) { - self.vsindex = index; - } -} - -struct IndexMetadata { - count: u32, - offsets_offset: u32, - data_offset: u32, - offset_size: u32, -} - -impl IndexMetadata { - pub fn unpack(data: &[u8], key: u32) -> Option { - let is_cff2 = key & 1 != 0; - let offset = key >> 1; - let b = Bytes::with_offset(data, offset as usize)?; - if is_cff2 { - let count = b.read::(0)?; - if count == 0 { - Some(Self { - count: 0, - offsets_offset: 4, - data_offset: 0, - offset_size: 0, - }) - } else { - let offset_size = b.read::(4)? as u32; - let offsets_offset = offset + 5; - let data_offset = 5 + offset_size * (count + 1); - Some(Self { - count, - offsets_offset, - data_offset, - offset_size, - }) - } - } else { - let count = b.read::(0)? as u32; - if count == 0 { - Some(Self { - count: 0, - offsets_offset: 2, - data_offset: 0, - offset_size: 0, - }) - } else { - let offset_size = b.read::(2)? as u32; - let offsets_offset = offset + 3; - let data_offset = 3 + offset_size * (count + 1); - Some(Self { - count, - offsets_offset, - data_offset, - offset_size, - }) - } - } - } -} - -#[derive(Copy, Clone, Default)] -struct Index { - key: u32, -} - -impl Index { - fn empty() -> Self { - Self { key: 0 } - } - - fn new(data: &[u8], offset: u32) -> Option { - let key = offset << 1; - IndexMetadata::unpack(data, key)?; - Some(Self { key }) - } - - fn new2(data: &[u8], offset: u32) -> Option { - let key = (offset << 1) | 1; - IndexMetadata::unpack(data, key)?; - Some(Self { key }) - } - - #[inline(always)] - fn get_offset(&self, data: &[u8], meta: &IndexMetadata, index: u32) -> Option { - let offsize = meta.offset_size; - let base = (meta.offsets_offset + offsize * index) as usize; - let b = Bytes::new(data); - match offsize { - 1 => Some(b.read::(base)? as usize), - 2 => Some(b.read::(base)? as usize), - 3 => Some(b.read::(base)?.0 as usize), - 4 => Some(b.read::(base)? as usize), - _ => None, - } - } - - fn count(&self, data: &[u8]) -> u32 { - let is_cff2 = self.key & 1 != 0; - if is_cff2 { - Bytes::new(data).read_or_default::((self.key >> 1) as usize) - } else { - Bytes::new(data).read_or_default::((self.key >> 1) as usize) as u32 - } - } - - fn len(&self, data: &[u8]) -> Option { - let meta = IndexMetadata::unpack(data, self.key)?; - if meta.count == 0 { - return Some(meta.offsets_offset as usize); - } - let last = self.get_offset(data, &meta, meta.count)?; - Some(meta.data_offset as usize + last - 1) - } - - fn get_range(&self, data: &[u8], index: u32) -> Option> { - let meta = IndexMetadata::unpack(data, self.key)?; - if index >= meta.count { - return None; - } - let base = ((self.key >> 1) + meta.data_offset) as usize; - let offset1 = self.get_offset(data, &meta, index)?; - let offset2 = self.get_offset(data, &meta, index + 1)?; - let start = base + offset1 - 1; - let end = base + offset2 - 1; - if end < start || end > data.len() { - return None; - } - Some(start..end) - } -} - -#[derive(Copy, Clone)] -struct BlendData<'a> { - store: Bytes<'a>, - coords: &'a [i16], -} - -impl<'a> BlendData<'a> { - fn new(data: &'a [u8], store: u32, coords: &'a [i16]) -> Self { - let store = Bytes::with_offset(data, store as usize).unwrap_or_else(|| Bytes::new(&[])); - Self { store, coords } - } -} - -#[derive(Copy, Clone)] -struct BlendState { - scalars: [f32; 32], - region_count: usize, - index: u16, - present: bool, -} - -impl BlendState { - fn new() -> Self { - Self { - scalars: [0f32; 32], - region_count: 0, - index: 0, - present: false, - } - } - - fn apply( - &mut self, - data: &BlendData, - index: u16, - values: &mut [f32], - count: usize, - ) -> Option { - let len = values.len(); - if count >= len { - return None; - } - if !self.present || index != self.index { - let (ok, region_count) = Self::compute_scalars(data, index, &mut self.scalars)?; - self.region_count = region_count; - self.index = index; - self.present = true; - if !ok { - for i in 0..region_count { - self.scalars[i] = 0.; - } - } - } - let region_count = self.region_count; - let region_value_count = region_count * count; - let total_count = region_value_count + count; - if total_count > len { - return None; - } - let base = len - total_count; - let mut region_idx = base + count; - for i in 0..count { - let mut delta = 0.; - for j in 0..region_count { - delta += self.scalars[j] * values[region_idx + j]; - } - region_idx += region_count; - values[base + i] += delta; - } - Some(total_count) - } - - #[allow(clippy::needless_range_loop)] - fn compute_scalars(data: &BlendData, outer: u16, scalars: &mut [f32]) -> Option<(bool, usize)> { - let b = &data.store; - let vary_coords = data.coords; - let coords_len = vary_coords.len(); - let store = 0; - if outer >= b.read::(store + 6)? { - return None; - } - let region_base = store + b.read::(store + 2)? as usize; - let axis_count = b.read::(region_base)? as usize; - let region_record_size = axis_count * 6; - let region_count = b.read::(region_base + 2)? as usize; - let data_base = store + b.read::(store + 8 + outer as usize * 4)? as usize; - let region_index_base = data_base + 6; - let region_index_count = b.read::(data_base + 4)? as usize; - if region_index_count > scalars.len() { - return Some((false, region_index_count)); - } - for i in 0..region_index_count { - let region_index = b.read::(region_index_base + i * 2)? as usize; - if region_index >= region_count { - return None; - } - let region_offset = region_base + 4 + region_index * region_record_size; - let mut scalar = 1.; - for axis in 0..axis_count { - fn f2dot14_to_f32(x: i16) -> f32 { - ((x as i32) << 2) as f32 / 65536. - } - let region_axis_base = region_offset + axis * 6; - let start = f2dot14_to_f32(b.read::(region_axis_base)?); - let peak = f2dot14_to_f32(b.read::(region_axis_base + 2)?); - let end = f2dot14_to_f32(b.read::(region_axis_base + 4)?); - let coord = if axis >= coords_len { - 0. - } else { - f2dot14_to_f32(vary_coords[axis]) - }; - if (start > peak || peak > end) - || (start < 0. && end > 0. && peak != 0.) - || peak == 0. - { - continue; - } else if coord < start || coord > end { - scalar = 0.; - break; - } else if coord == peak { - continue; - } else if coord < peak { - scalar *= (coord - start) / (peak - start) - } else { - scalar *= (end - coord) / (end - peak) - }; - } - scalars[i] = scalar; - } - Some((true, region_index_count)) - } -} - -struct ParseState { - open: bool, - have_width: bool, - stem_count: usize, - vsindex: u16, -} - -const MAX_STACK: usize = 513; - -#[derive(Copy, Clone)] -struct Stack { - elements: [f32; MAX_STACK], - top: usize, -} - -impl Stack { - fn new() -> Self { - Self { - elements: [0.; MAX_STACK], - top: 0, - } - } - - fn push(&mut self, value: f32) -> Option<()> { - if self.top == MAX_STACK { - return None; - } - self.elements[self.top] = value; - self.top += 1; - Some(()) - } - - fn pop(&mut self) -> f32 { - self.top -= 1; - self.elements[self.top] - } - - fn len(&self) -> usize { - self.top - } - - fn is_empty(&self) -> bool { - self.top == 0 - } - - fn is_odd(&self) -> bool { - self.top & 1 == 1 - } - - fn get(&self, index: usize) -> f32 { - self.elements[index] - } - - fn reverse(&mut self) { - self.elements.split_at_mut(self.top).0.reverse(); - } - - fn clear(&mut self) { - self.top = 0; - } -} - -fn parse_dict( - data: &[u8], - range: Range, - blend: Option, - sink: &mut Sink, -) -> Option<()> { - use opcodes::*; - if range.is_empty() { - return Some(()); - } - let mut s = Stream::with_range(data, range)?; - let mut operands = [0f32; MAX_STACK]; - let mut blend_state = BlendState::new(); - let mut vsindex = 0u16; - loop { - let (mut op, mut n) = parse_dict_entry(&mut s, &mut operands)?; - if op == DICT_BLEND { - if n == 0 { - return None; - } - let count = operands[n - 1] as usize; - if let Some(ref blend) = blend { - blend_state.apply(blend, vsindex, &mut operands[0..n - 1], count); - n = count; - } else { - n = count; - } - let (op2, n2) = parse_dict_entry(&mut s, &mut operands)?; - if n2 != 0 { - return None; - } - op = op2; - } - let ops = &mut operands[0..n]; - match op { - 0xFFFF => break, - CHAR_STRING_TYPE => { - if n != 1 { - return None; - } - sink.char_string_type(ops[0] as u32); - } - CHAR_STRINGS => { - if n != 1 { - return None; - } - sink.char_strings(ops[0] as usize); - } - PRIVATE => { - if n != 2 { - return None; - } - sink.private_dictionary(ops[1] as usize, ops[0] as usize); - } - FD_ARRAY => { - if n != 1 { - return None; - } - sink.fd_array(ops[0] as usize); - } - FD_SELECT => { - if n != 1 { - return None; - } - sink.fd_select(ops[0] as usize); - } - FONT_MATRIX => { - if n == 6 { - sink.font_matrix(ops); - } - } - LANGUAGE_GROUP => { - sink.language_group(ops[0] as u32); - } - BLUE_VALUES => { - delta_vector(ops); - sink.blue_values(ops); - } - OTHER_BLUES => { - delta_vector(ops); - sink.other_blues(ops); - } - FAMILY_BLUES => { - delta_vector(ops); - sink.family_blues(ops); - } - FAMILY_OTHER_BLUES => { - delta_vector(ops); - sink.family_other_blues(ops); - } - BLUE_SCALE => { - if n != 1 { - return None; - } - sink.blue_scale(ops[0]); - } - BLUE_SHIFT => { - if n != 1 { - return None; - } - sink.blue_shift(ops[0]); - } - BLUE_FUZZ => { - if n != 1 { - return None; - } - sink.blue_fuzz(ops[0]); - } - DICT_VSINDEX => { - if n != 1 { - return None; - } - vsindex = ops[0] as u16; - } - DICT_BLEND => {} - VSTORE => { - if n != 1 { - return None; - } - sink.variation_store(ops[0] as usize); - } - STD_HW => {} - STD_VW => {} - STEM_SNAP_H => {} - STEM_SNAP_V => {} - SUBRS => { - if n != 1 { - return None; - } - sink.subroutines(ops[0] as usize); - } - _ => {} - } - } - Some(()) -} - -fn delta_vector(ops: &mut [f32]) { - if ops.is_empty() { - return; - } - let mut s = ops[0]; - for v in ops.iter_mut().skip(1) { - s += *v; - *v = s; - } -} - -fn parse_dict_entry(s: &mut Stream, operands: &mut [f32]) -> Option<(u16, usize)> { - let max_operands = operands.len(); - let mut n = 0; - loop { - if s.remaining() == 0 { - return Some((0xFFFF, 0)); - } - let b0 = s.read::()?; - if b0 <= 24 { - let mut b1 = 0u8; - if b0 == 12 { - b1 = s.read()?; - } - return Some(((b0 as u16) << 8 | b1 as u16, n)); - } - match b0 { - 28 | 29 | 32..=254 => { - if n == max_operands { - return None; - } - operands[n] = parse_integer(s, b0 as i32)?; - n += 1; - } - 30 => { - if n == max_operands { - return None; - } - operands[n] = parse_real(s)?; - n += 1; - } - _ => { - return None; - } - } - } -} - -#[inline(always)] -fn parse_integer(s: &mut Stream, b0: i32) -> Option { - match b0 { - 28 => Some(s.read::()? as f32), - 29 => Some(s.read::()? as f32), - 32..=246 => Some((b0 - 139) as f32), - 247..=250 => Some(((b0 - 247) * 256 + s.read::()? as i32 + 108) as f32), - 251..=254 => Some((-(b0 - 251) * 256 - s.read::()? as i32 - 108) as f32), - _ => None, - } -} - -fn parse_real(s: &mut Stream) -> Option { - const MAX_LEN: usize = 64; - let mut buf = [0u8; MAX_LEN]; - let mut n = 0; - let mut done = false; - let mut missing_exp = true; - loop { - let b = s.read::()?; - let n0 = (b >> 4) & 0xF; - let n1 = b & 0xF; - for j in 0..2 { - if n + 1 == MAX_LEN { - return None; - } - let nibble = if j == 0 { n0 } else { n1 }; - match nibble { - 0x0..=0x9 => { - buf[n] = b'0' + nibble; - n += 1; - missing_exp = false; - } - 0xA => { - buf[n] = b'.'; - n += 1; - } - 0xB => { - buf[n] = b'E'; - n += 1; - missing_exp = true; - } - 0xC => { - buf[n] = b'E'; - buf[n + 1] = b'-'; - n += 2; - missing_exp = true; - } - 0xE => { - buf[n] = b'-'; - n += 1; - } - 0xF => { - done = true; - break; - } - _ => { - return None; - } - } - } - if done { - break; - } - } - if missing_exp && n < MAX_LEN { - buf[n] = b'0'; - n += 1; - } - core::str::from_utf8(&buf[0..n]).map_or(None, |b| b.parse::().ok()) -} - -fn parse_fd_select(data: &[u8], offset: u32, glyph_id: u16) -> Option { - if offset == 0 { - return Some(0); - } - let d = Bytes::new(data); - let base = offset as usize; - let fmt = d.read::(base)?; - if fmt == 0 { - return Some(d.read::(base + 1 + glyph_id as usize)? as usize); - } else if fmt == 3 { - let nranges = d.read::(base + 1)? as usize; - let mut l = 0; - let mut h = nranges; - while l < h { - let i = (l + h) / 2; - let rec = base + 3 + i * 3; - let first = d.read::(rec)?; - if glyph_id < first { - h = i; - } else if glyph_id >= d.read::(rec + 3)? { - l = i + 1; - } else { - return Some(d.read::(rec + 2)? as usize); - } - } - } else if fmt == 4 { - let nranges = d.read::(base + 1)? as usize; - let mut l = 0; - let mut h = nranges; - while l < h { - let i = (l + h) / 2; - let rec = base + 5 + i * 6; - let first = d.read::(rec)?; - if (glyph_id as u32) < first { - h = i; - } else if glyph_id as u32 > d.read::(rec + 6)? { - l = i + 1; - } else { - return Some(d.read::(rec + 4)? as usize); - } - } - } - None -} - -#[allow(dead_code)] -mod opcodes { - // top - pub const CHAR_STRING_TYPE: u16 = 12 << 8 | 6; - pub const CHAR_STRINGS: u16 = 17 << 8; - pub const PRIVATE: u16 = 18 << 8; - pub const FD_ARRAY: u16 = 12 << 8 | 36; - pub const FD_SELECT: u16 = 12 << 8 | 37; - pub const FONT_MATRIX: u16 = 12 << 8 | 7; - - // private - pub const BLUE_VALUES: u16 = 6 << 8; - pub const OTHER_BLUES: u16 = 7 << 8; - pub const FAMILY_BLUES: u16 = 8 << 8; - pub const FAMILY_OTHER_BLUES: u16 = 9 << 8; - pub const BLUE_SCALE: u16 = 12 << 8 | 9; - pub const BLUE_SHIFT: u16 = 12 << 8 | 10; - pub const BLUE_FUZZ: u16 = 12 << 8 | 11; - pub const STD_HW: u16 = 10 << 8; - pub const STD_VW: u16 = 11 << 8; - pub const STEM_SNAP_H: u16 = 12 << 8 | 12; - pub const STEM_SNAP_V: u16 = 12 << 8 | 13; - pub const LANGUAGE_GROUP: u16 = 12 << 8 | 17; - pub const SUBRS: u16 = 19 << 8; - pub const DEFAULT_WIDTH_X: u16 = 20 << 8; - pub const NOMINAL_WIDTH_X: u16 = 21 << 8; - pub const DICT_VSINDEX: u16 = 22 << 8; - pub const DICT_BLEND: u16 = 23 << 8; - pub const VSTORE: u16 = 24 << 8; - - // char strings - pub const HSTEM: u16 = 1; - pub const VSTEM: u16 = 3; - pub const VMOVETO: u16 = 4; - pub const RLINETO: u16 = 5; - pub const HLINETO: u16 = 6; - pub const VLINETO: u16 = 7; - pub const RRCURVETO: u16 = 8; - pub const CALLSUBR: u16 = 10; - pub const RETURN: u16 = 11; - pub const ESCAPE: u16 = 12; - pub const ENDCHAR: u16 = 14; - pub const VSINDEX: u16 = 15; - pub const BLEND: u16 = 16; - pub const HSTEMHM: u16 = 18; - pub const HINTMASK: u16 = 19; - pub const CNTRMASK: u16 = 20; - pub const RMOVETO: u16 = 21; - pub const HMOVETO: u16 = 22; - pub const VSTEMHM: u16 = 23; - pub const RCURVELINE: u16 = 24; - pub const RLINECURVE: u16 = 25; - pub const VVCURVETO: u16 = 26; - pub const HHCURVETO: u16 = 27; - pub const SHORTINT: u16 = 28; - pub const CALLGSUBR: u16 = 29; - pub const VHCURVETO: u16 = 30; - pub const HVCURVETO: u16 = 31; - - // 2 bytes - pub const HFLEX: u16 = 34 | 12 << 8; - pub const FLEX: u16 = 35 | 12 << 8; - pub const HFLEX1: u16 = 36 | 12 << 8; - pub const FLEX1: u16 = 37 | 12 << 8; - - #[rustfmt::skip] - pub const NAMES: [&'static str; 38] = [ - "", "hstem", "", "vstem", "vmoveto", "rlineto", "hlineto", "vlineto", "rrcurveto", "", - "callsubr", "return", "escape", "", "endchar", "vsindex", "blend", "", "hstemhm", "hintmask", - "cntrmask", "rmoveto", "hmoveto", "vstemhm", "rcurveline", "rlinecurve", "vvcurveto", - "hhcurveto", "shortint", "callgsubr", "vhcurveto", "hvcurveto", "", "", "hflex", "flex", - "hflex1", "flex1", - ]; -} diff --git a/src/scale/cff/hint.rs b/src/scale/cff/hint.rs index 41defb4..84aeae9 100644 --- a/src/scale/cff/hint.rs +++ b/src/scale/cff/hint.rs @@ -1,396 +1,343 @@ -use super::cff::{DictionarySink, Glyph, GlyphSink}; -use super::internal::fixed::{muldiv, Fixed}; +//! CFF hinting. -use super::TRACE; +use read_fonts::{ + tables::postscript::{charstring::CommandSink, dict::Blues}, + types::Fixed, +}; -/// Hinting state for a compact font format glyph. +// "Default values for OS/2 typoAscender/Descender.." +// See +const ICF_TOP: Fixed = Fixed::from_i32(880); +const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); + +// +const MAX_BLUES: usize = 7; +const MAX_OTHER_BLUES: usize = 5; +const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES; + +// +const MAX_HINTS: usize = 96; + +// One bit per stem hint +// +const HINT_MASK_SIZE: usize = (MAX_HINTS + 7) / 8; + +// Constant for hint adjustment and em box hint placement. +// +const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); + +// +const EPSILON: Fixed = Fixed::from_bits(1); + +/// Parameters used to generate the stem and counter zones for the hinting +/// algorithm. +#[derive(Clone)] +pub(crate) struct HintParams { + pub blues: Blues, + pub family_blues: Blues, + pub other_blues: Blues, + pub family_other_blues: Blues, + pub blue_scale: Fixed, + pub blue_shift: Fixed, + pub blue_fuzz: Fixed, + pub language_group: i32, +} + +impl Default for HintParams { + fn default() -> Self { + Self { + blues: Blues::default(), + other_blues: Blues::default(), + family_blues: Blues::default(), + family_other_blues: Blues::default(), + // See + blue_scale: Fixed::from_f64(0.039625), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ONE, + language_group: 0, + } + } +} + +/// See +#[derive(Copy, Clone, PartialEq, Default, Debug)] +struct BlueZone { + is_bottom: bool, + cs_bottom_edge: Fixed, + cs_top_edge: Fixed, + cs_flat_edge: Fixed, + ds_flat_edge: Fixed, +} + +/// Hinting state for a PostScript subfont. /// /// Note that hinter states depend on the scale, subfont index and /// variation coordinates of a glyph. They can be retained and reused /// if those values remain the same. -#[derive(Copy, Clone)] -pub struct HinterState { +#[derive(Copy, Clone, PartialEq, Default)] +pub(crate) struct HintState { scale: Fixed, - fscale: f32, - blues: BlueValues, - other_blues: BlueValues, - family_blues: BlueValues, - family_other_blues: BlueValues, blue_scale: Fixed, blue_shift: Fixed, blue_fuzz: Fixed, - language_group: u8, + language_group: i32, supress_overshoot: bool, - em_box_hints: bool, + do_em_box_hints: bool, boost: Fixed, + darken_y: Fixed, zones: [BlueZone; MAX_BLUE_ZONES], - zone_count: u16, + zone_count: usize, } -impl HinterState { - /// Creates a new hinting state for the specified glyph, scale and - /// variation coordinates. - pub fn new(glyph: &Glyph, scale: f32, coords: &[i16]) -> Self { +impl HintState { + pub fn new(params: &HintParams, scale: Fixed) -> Self { let mut state = Self { - scale: Fixed::from_f32(scale), - fscale: scale, - blues: BlueValues::new(), - other_blues: BlueValues::new(), - family_blues: BlueValues::new(), - family_other_blues: BlueValues::new(), - blue_scale: DEFAULT_BLUE_SCALE, - blue_shift: DEFAULT_BLUE_SHIFT, - blue_fuzz: DEFAULT_BLUE_FUZZ, - language_group: 0, + scale, + blue_scale: params.blue_scale, + blue_shift: params.blue_shift, + blue_fuzz: params.blue_fuzz, + language_group: params.language_group, supress_overshoot: false, - em_box_hints: false, - boost: Fixed(0), + do_em_box_hints: false, + boost: Fixed::ZERO, + darken_y: Fixed::ZERO, zones: [BlueZone::default(); MAX_BLUE_ZONES], zone_count: 0, }; - glyph.eval_private_dict(coords, &mut state); - state.initialize_zones(); + state.build_zones(params); state } - /// Returns the current scaling factor of the hinter state. - pub fn scale(&self) -> f32 { - self.fscale + fn zones(&self) -> &[BlueZone] { + &self.zones[..self.zone_count] } - fn initialize_zones(&mut self) { - self.em_box_hints = false; - if self.language_group == 1 { - if self.blues.len == 2 { - let v = self.blues.values(); - if v[0].0 < ICF_BOTTOM - && v[0].1 < ICF_BOTTOM - && v[1].0 > ICF_TOP - && v[1].1 > ICF_TOP + /// Initialize zones from the set of blues values. + /// + /// See + fn build_zones(&mut self, params: &HintParams) { + self.do_em_box_hints = false; + // + match (self.language_group, params.blues.values().len()) { + (1, 2) => { + let blues = params.blues.values(); + if blues[0].0 < ICF_BOTTOM + && blues[0].1 < ICF_BOTTOM + && blues[1].0 > ICF_TOP + && blues[1].1 > ICF_TOP { - self.em_box_hints = true; + // FreeType generates synthetic hints here. We'll do it + // later when building the hint map. + self.do_em_box_hints = true; + return; } - } else if self.blues.len == 0 { - self.em_box_hints = true; } - } - if self.em_box_hints { - return; + (1, 0) => { + self.do_em_box_hints = true; + return; + } + _ => {} } let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; - let darken_y = Fixed(0); - let mut max_height = Fixed(0); - let mut z = 0usize; - for blue in self.blues.values() { - let bottom = blue.0; - let top = blue.1; - let height = top - bottom; - if height.0 < 0 { + let mut max_zone_height = Fixed::ZERO; + let mut zone_ix = 0usize; + // Copy blues and other blues to a combined array of top and bottom zones. + for blue in params.blues.values().iter().take(MAX_BLUES) { + // FreeType loads blues as integers and then expands to 16.16 + // at initialization. We load them as 16.16 so floor them here + // to ensure we match. + // + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height continue; } - if height > max_height { - max_height = height; - } - let zone = &mut zones[z]; - zone.bottom = bottom; - zone.top = top; - if z == 0 { + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_ix]; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + if zone_ix == 0 { + // First blue value is bottom zone zone.is_bottom = true; - zone.flat = top; + zone.cs_flat_edge = top; } else { - zone.top += Fixed(2) * darken_y; - zone.bottom += Fixed(2) * darken_y; + // Remaining blue values are top zones zone.is_bottom = false; - zone.flat = zone.bottom; + // Adjust both edges of top zone upward by twice darkening amount + zone.cs_top_edge += twice(self.darken_y); + zone.cs_bottom_edge += twice(self.darken_y); + zone.cs_flat_edge = zone.cs_bottom_edge; } - z += 1; + zone_ix += 1; } - let units_per_pixel = Fixed::ONE / self.scale; - for blue in self.other_blues.values() { - let bottom = blue.0; - let top = blue.1; - let height = top - bottom; - if height.0 < 0 { + for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { + let bottom = blue.0.floor(); + let top = blue.1.floor(); + let zone_height = top - bottom; + if zone_height < Fixed::ZERO { + // Reject zones with negative height continue; } - if height > max_height { - max_height = height; - } - let zone = &mut zones[z]; - zone.bottom = bottom; - zone.top = top; + max_zone_height = max_zone_height.max(zone_height); + let zone = &mut zones[zone_ix]; + // All "other" blues are bottom zone zone.is_bottom = true; - zone.flat = top; - z += 1; + zone.cs_bottom_edge = bottom; + zone.cs_top_edge = top; + zone.cs_flat_edge = top; + zone_ix += 1; } - for zone in &mut zones[..z] { - let flat = zone.flat; + // Adjust for family blues + let units_per_pixel = Fixed::ONE / self.scale; + for zone in &mut zones[..zone_ix] { + let flat = zone.cs_flat_edge; let mut min_diff = Fixed::MAX; if zone.is_bottom { - for blue in self.family_other_blues.values() { + // In a bottom zone, the top edge is the flat edge. + // Search family other blues for bottom zones. Look for the + // closest edge that is within the one pixel threshold. + for blue in params.family_other_blues.values() { let family_flat = blue.1; let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { - zone.flat = family_flat; + zone.cs_flat_edge = family_flat; min_diff = diff; - if diff.0 == 0 { + if diff == Fixed::ZERO { break; } } } - if self.family_blues.len > 0 { - let family_flat = self.family_blues.values[0].1; + // Check the first member of family blues, which is a bottom + // zone + if !params.family_blues.values().is_empty() { + let family_flat = params.family_blues.values()[0].1; let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { - zone.flat = family_flat; + zone.cs_flat_edge = family_flat; } } } else { - for blue in self.family_blues.values().iter().skip(1) { - let family_flat = blue.0 + Fixed(2) * darken_y; + // In a top zone, the bottom edge is the flat edge. + // Search family blues for top zones, skipping the first, which + // is a bottom zone. Look for closest family edge that is + // within the one pixel threshold. + for blue in params.family_blues.values().iter().skip(1) { + let family_flat = blue.0 + twice(self.darken_y); let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { - zone.flat = family_flat; + zone.cs_flat_edge = family_flat; min_diff = diff; - if diff.0 == 0 { + if diff == Fixed::ZERO { break; } } } } } - if max_height.0 > 0 && self.blue_scale > (Fixed::ONE / max_height) { - self.blue_scale = Fixed::ONE / max_height; + if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { + // Clamp at maximum scale + self.blue_scale = Fixed::ONE / max_zone_height; } + // Suppress overshoot and boost blue zones at small sizes if self.scale < self.blue_scale { self.supress_overshoot = true; - self.boost = Fixed::from_f32(0.6) - - Fixed(muldiv( - Fixed::from_f32(0.6).0, - self.scale.0, - self.blue_scale.0, - )); - if self.boost.0 > 0x7FFF { - self.boost.0 = 0x7FFF; - } + self.boost = + Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); + // boost must remain less than 0.5, or baseline could go negative + self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); } - if darken_y.0 != 0 { - self.boost.0 = 0; + if self.darken_y != Fixed::ZERO { + self.boost = Fixed::ZERO; } + // Set device space alignment for each zone; apply boost amount before + // rounding flat edge let scale = self.scale; let boost = self.boost; - for zone in &mut zones[..z] { + for zone in &mut zones[..zone_ix] { let boost = if zone.is_bottom { -boost } else { boost }; - zone.ds_flat = (zone.flat * scale + boost).round(); + zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); } self.zones = zones; - self.zone_count = z as u16; + self.zone_count = zone_ix; } - fn capture(&self, bottom: &mut Hint, top: &mut Hint) -> bool { + /// Check whether a hint is captured by one of the blue zones. + /// + /// See + fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool { let fuzz = self.blue_fuzz; let mut captured = false; - let mut adjustment = Fixed(0); - for zone in &self.zones[..self.zone_count as usize] { + let mut adjustment = Fixed::ZERO; + for zone in self.zones() { if zone.is_bottom - && bottom.is_bottom() - && (zone.bottom - fuzz) <= bottom.coord - && bottom.coord <= (zone.top + fuzz) + && bottom_edge.is_bottom() + && (zone.cs_bottom_edge - fuzz) <= bottom_edge.cs_coord + && bottom_edge.cs_coord <= (zone.cs_top_edge + fuzz) { + // Bottom edge captured by bottom zone. adjustment = if self.supress_overshoot { - zone.ds_flat - } else if zone.top - bottom.coord >= self.blue_shift { - bottom.ds_coord.round().min(zone.ds_flat - Fixed::ONE) + zone.ds_flat_edge + } else if zone.cs_top_edge - bottom_edge.cs_coord >= self.blue_shift { + // Guarantee minimum of 1 pixel overshoot + bottom_edge + .ds_coord + .round() + .min(zone.ds_flat_edge - Fixed::ONE) } else { - bottom.ds_coord.round() - } - bottom.ds_coord; + bottom_edge.ds_coord.round() + }; + adjustment -= bottom_edge.ds_coord; captured = true; break; } if !zone.is_bottom - && top.is_top() - && (zone.bottom - fuzz) <= top.coord - && top.coord <= (zone.top + fuzz) + && top_edge.is_top() + && (zone.cs_bottom_edge - fuzz) <= top_edge.cs_coord + && top_edge.cs_coord <= (zone.cs_top_edge + fuzz) { + // Top edge captured by top zone. adjustment = if self.supress_overshoot { - zone.ds_flat - } else if top.coord - zone.bottom >= self.blue_shift { - top.ds_coord.round().max(zone.ds_flat + Fixed::ONE) + zone.ds_flat_edge + } else if top_edge.cs_coord - zone.cs_bottom_edge >= self.blue_shift { + // Guarantee minimum of 1 pixel overshoot + top_edge + .ds_coord + .round() + .max(zone.ds_flat_edge + Fixed::ONE) } else { - top.ds_coord.round() - } - top.ds_coord; + top_edge.ds_coord.round() + }; + adjustment -= top_edge.ds_coord; captured = true; break; } } if captured { - if bottom.is_valid() { - bottom.ds_coord += adjustment; - bottom.lock(); + // Move both edges and mark them as "locked" + if bottom_edge.is_valid() { + bottom_edge.ds_coord += adjustment; + bottom_edge.lock(); } - if top.is_valid() { - top.ds_coord += adjustment; - top.lock(); + if top_edge.is_valid() { + top_edge.ds_coord += adjustment; + top_edge.lock(); } } captured } - - // pub(super) fn dump_blues(&self) { - // for (i, zone) in (&self.zones[..self.zone_count as usize]).iter().enumerate() { - // println!( - // "[{}] {} t: {} b: {} f: {} ds: {}", - // i, - // if zone.is_bottom { "B" } else { "T" }, - // zone.top.0, - // zone.bottom.0, - // zone.flat.0, - // zone.ds_flat.0, - // ); - // } - // } -} - -pub struct Hinter<'a, 'b, Sink> { - state: &'a HinterState, - sink: &'b mut Sink, - stem_hints: [StemHint; MAX_STEM_HINTS], - stem_count: u8, - mask: HintMask, - initial_map: HintMap, - map: HintMap, } -impl<'a, 'b, Sink: GlyphSink> Hinter<'a, 'b, Sink> { - pub fn new(state: &'a HinterState, sink: &'b mut Sink) -> Self { - Self { - state, - sink, - stem_hints: [StemHint::default(); MAX_STEM_HINTS], - stem_count: 0, - mask: HintMask::all(), - initial_map: HintMap::new(), - map: HintMap::new(), - } - } - - fn hint(&mut self, coord: f32) -> f32 { - if !self.map.valid { - self.build_hint_map(Some(self.mask), Fixed(0)); - } - let f = (coord * 65536.) as i32; - let mapped = self.map.map(self.state.scale, Fixed(f)); - ((mapped.0 >> 10) as f32) / 64. - } - - #[inline(always)] - fn scale(&self, coord: f32) -> f32 { - ((Fixed::from_f32(coord) * self.state.scale).0 >> 10) as f32 / 64. - } - - fn add_stem(&mut self, min: Fixed, max: Fixed) { - let index = self.stem_count as usize; - if index >= MAX_STEM_HINTS || self.map.valid { - return; - } - let stem = &mut self.stem_hints[index]; - stem.min = min; - stem.max = max; - stem.used = false; - stem.ds_min = Fixed(0); - stem.ds_max = Fixed(0); - self.stem_count = index as u8 + 1; - } - - fn build_hint_map(&mut self, mask: Option, origin: Fixed) { - self.map.build( - &self.state, - mask, - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - origin, - false, - ); - } -} - -impl<'a, 'b, Sink: GlyphSink> GlyphSink for Hinter<'a, 'b, Sink> { - fn hstem(&mut self, y: f32, dy: f32) { - let y = (y * 65536.) as i32; - let dy = (dy * 65536.) as i32; - self.add_stem(Fixed(y), Fixed(dy)); - } - - fn hint_mask(&mut self, mask: &[u8]) { - let mut hint_mask = HintMask::new(); - hint_mask.set_mask(mask); - if TRACE { - //println!("Got hintmask: {:?}", mask); - } - if hint_mask != self.mask { - self.mask = hint_mask; - self.map.valid = false; - } - } - - fn counter_mask(&mut self, mask: &[u8]) { - let mut hint_mask = HintMask::new(); - hint_mask.set_mask(mask); - let mut map = HintMap::new(); - map.build( - &self.state, - Some(hint_mask), - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - Fixed(0), - false, - ); - } - - fn move_to(&mut self, x: f32, y: f32) { - let x = self.scale(x); - let y = self.hint(y); - self.sink.move_to(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - let x = self.scale(x); - let y = self.hint(y); - self.sink.line_to(x, y); - } - - fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - let cx1 = self.scale(cx1); - let cy1 = self.hint(cy1); - let cx2 = self.scale(cx2); - let cy2 = self.hint(cy2); - let x = self.scale(x); - let y = self.hint(y); - self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) {} -} - -const ICF_TOP: Fixed = Fixed::from_i32(880); -const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); - -const MAX_BLUE_VALUES: usize = 7; -const MAX_BLUE_ZONES: usize = 12; -const MAX_STEM_HINTS: usize = 48; -const MAX_HINTS: usize = MAX_STEM_HINTS * 2; - -const HINT_MASK_SIZE: usize = (MAX_STEM_HINTS + 7) / 4; - -const DEFAULT_BLUE_SCALE: Fixed = Fixed((0.039625 * 65536. + 0.5) as i32); -const DEFAULT_BLUE_SHIFT: Fixed = Fixed::from_i32(7); -const DEFAULT_BLUE_FUZZ: Fixed = Fixed::ONE; - +/// #[derive(Copy, Clone, Default)] struct StemHint { - used: bool, + /// If true, device space position is valid + is_used: bool, + // Character space position min: Fixed, max: Fixed, + // Device space position after first use ds_min: Fixed, ds_max: Fixed, } @@ -403,11 +350,13 @@ const PAIR_TOP: u8 = 0x8; const LOCKED: u8 = 0x10; const SYNTHETIC: u8 = 0x20; -#[derive(Copy, Clone, Default)] +/// +#[derive(Copy, Clone, PartialEq, Default, Debug)] struct Hint { flags: u8, + /// Index in original stem hint array (if not synthetic) index: u8, - coord: Fixed, + cs_coord: Fixed, ds_coord: Fixed, scale: Fixed, } @@ -445,6 +394,9 @@ impl Hint { self.flags |= LOCKED } + /// Hint initialization from an incoming stem hint. + /// + /// See fn setup( &mut self, stem: &StemHint, @@ -454,43 +406,58 @@ impl Hint { darken_y: Fixed, is_bottom: bool, ) { + // "Ghost hints" are used to align a single edge rather than a + // stem-- think the top and bottom edges of an uppercase + // sans-serif I. + // These are encoded internally with stem hints of width -21 + // and -20 for bottom and top hints, respectively. + const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21); + const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20); let width = stem.max - stem.min; - if width == Fixed::from_i32(-21) { + if width == GHOST_BOTTOM_WIDTH { if is_bottom { - self.coord = stem.max; + self.cs_coord = stem.max; self.flags = GHOST_BOTTOM; } else { self.flags = 0; } - } else if width == Fixed::from_i32(-20) { + } else if width == GHOST_TOP_WIDTH { if !is_bottom { - self.coord = stem.min; + self.cs_coord = stem.min; self.flags = GHOST_TOP; } else { self.flags = 0; } - } else if width < Fixed(0) { + } else if width < Fixed::ZERO { + // If width < 0, this is an inverted pair. We follow FreeType and + // swap the coordinates if is_bottom { - self.coord = stem.max; + self.cs_coord = stem.max; self.flags = PAIR_BOTTOM; } else { - self.coord = stem.min; + self.cs_coord = stem.min; self.flags = PAIR_TOP; } - } else if is_bottom { - self.coord = stem.min; - self.flags = PAIR_BOTTOM; } else { - self.coord = stem.max; - self.flags = PAIR_TOP; + // This is a normal pair + if is_bottom { + self.cs_coord = stem.min; + self.flags = PAIR_BOTTOM; + } else { + self.cs_coord = stem.max; + self.flags = PAIR_TOP; + } } if self.is_top() { - self.coord += Fixed::from_i32(2) * darken_y; + // For top hints, adjust character space position up by twice the + // darkening amount + self.cs_coord += twice(darken_y); } - self.coord += origin; + self.cs_coord += origin; self.scale = scale; self.index = index; - if self.flags != 0 && stem.used { + // If original stem hint was used, copy the position + if self.flags != 0 && stem.is_used { if self.is_top() { self.ds_coord = stem.ds_max; } else { @@ -498,188 +465,211 @@ impl Hint { } self.lock(); } else { - self.ds_coord = self.coord * scale; + self.ds_coord = self.cs_coord * scale; } } } +/// Collection of adjusted hint edges. +/// +/// #[derive(Copy, Clone)] struct HintMap { - hints: [Hint; MAX_HINTS], + edges: [Hint; MAX_HINTS], len: usize, - valid: bool, + is_valid: bool, + scale: Fixed, } impl HintMap { - fn new() -> Self { + fn new(scale: Fixed) -> Self { Self { - hints: [Hint::default(); MAX_HINTS], + edges: [Hint::default(); MAX_HINTS], len: 0, - valid: false, + is_valid: false, + scale, } } fn clear(&mut self) { self.len = 0; - self.valid = false; + self.is_valid = false; } - fn map(&self, scale: Fixed, coord: Fixed) -> Fixed { + /// Transform character space coordinate to device space. + /// + /// Based on + fn transform(&self, coord: Fixed) -> Fixed { if self.len == 0 { - return coord * scale; + return coord * self.scale; } let limit = self.len - 1; let mut i = 0; - while i < limit && coord >= self.hints[i + 1].coord { + while i < limit && coord >= self.edges[i + 1].cs_coord { i += 1; } - while i > 0 && coord < self.hints[i].coord { + while i > 0 && coord < self.edges[i].cs_coord { i -= 1; } - let hint = &self.hints[i]; - if i == 0 && coord < self.hints[0].coord { - ((coord - self.hints[0].coord) * scale) + self.hints[0].ds_coord + let first_edge = &self.edges[0]; + if i == 0 && coord < first_edge.cs_coord { + // Special case for points below first edge: use uniform scale + ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord } else { - ((coord - hint.coord) * hint.scale) + hint.ds_coord + // Use highest edge where cs_coord >= edge.cs_coord + let edge = &self.edges[i]; + ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord } } - fn insert(&mut self, bottom: &Hint, top: &Hint, scale: Fixed, initial: Option<&HintMap>) { - let (is_pair, mut first) = if !bottom.is_valid() { + /// Insert hint edges into map, sorted by character space coordinate. + /// + /// Based on + fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) { + let (is_pair, mut first_edge) = if !bottom.is_valid() { + // Bottom is invalid: insert only top edge (false, *top) } else if !top.is_valid() { + // Top is invalid: insert only bottom edge (false, *bottom) } else { + // We have a valid pair! (true, *bottom) }; - let mut second = *top; - if is_pair && top.coord < bottom.coord { + let mut second_edge = *top; + if is_pair && top.cs_coord < bottom.cs_coord { + // Paired edges must be in proper order. FT just ignores the hint. return; } - let count = if is_pair { 2 } else { 1 }; - if self.len + count > MAX_HINTS { + let edge_count = if is_pair { 2 } else { 1 }; + if self.len + edge_count > MAX_HINTS { + // Won't fit. Again, ignore. return; } - if TRACE { - // println!( - // " Got hint at {} ({})", - // first.coord.to_f32(), - // first.ds_coord.to_f32() - // ); - // if is_pair { - // println!( - // " Got hint at {} ({})", - // second.coord.to_f32(), - // second.ds_coord.to_f32() - // ); - // } - } - let mut insertion_index = 0; - while insertion_index < self.len { - if self.hints[insertion_index].coord >= first.coord { + // Find insertion index that keeps the edge list sorted + let mut insert_ix = 0; + while insert_ix < self.len { + if self.edges[insert_ix].cs_coord >= first_edge.cs_coord { break; } - insertion_index += 1; + insert_ix += 1; } - if insertion_index < self.len { - let current = &self.hints[insertion_index]; - if (current.coord == first.coord) - || (is_pair && current.coord <= second.coord) + // Discard hints that overlap in character space + if insert_ix < self.len { + let current = &self.edges[insert_ix]; + // Existing edge is the same + if (current.cs_coord == first_edge.cs_coord) + // Pair straddles the next edge + || (is_pair && current.cs_coord <= second_edge.cs_coord) + // Inserting between paired edges || current.is_pair_top() { return; } } - if !first.is_locked() { - if let Some(ref initial) = initial { + // Recompute device space locations using initial hint map + if !first_edge.is_locked() { + if let Some(initial) = initial { if is_pair { - let mid = initial.map(scale, first.coord + (second.coord + first.coord) / 2); - let half = (second.coord - first.coord) / 2 * scale; - first.ds_coord = mid - half; - second.ds_coord = mid + half; + // Preserve stem width: position center of stem with + // initial hint map and two edges with nominal scale + let mid = initial.transform( + first_edge.cs_coord + half(second_edge.cs_coord - first_edge.cs_coord), + ); + let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale; + first_edge.ds_coord = mid - half_width; + second_edge.ds_coord = mid + half_width; } else { - first.ds_coord = initial.map(scale, first.coord); + first_edge.ds_coord = initial.transform(first_edge.cs_coord); } } } - if insertion_index > 0 && first.ds_coord < self.hints[insertion_index - 1].ds_coord { + // Now discard hints that overlap in device space: + if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord { + // Inserting after an existing edge return; } - if insertion_index < self.len - && ((is_pair && second.ds_coord > self.hints[insertion_index].ds_coord) - || first.ds_coord > self.hints[insertion_index].ds_coord) + if insert_ix < self.len + && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord) + || first_edge.ds_coord > self.edges[insert_ix].ds_coord) { + // Inserting before an existing edge return; } - if insertion_index != self.len { + // If we're inserting in the middle, make room in the edge array + if insert_ix != self.len { let mut src_index = self.len - 1; - let mut dst_index = self.len + count - 1; + let mut dst_index = self.len + edge_count - 1; loop { - self.hints[dst_index] = self.hints[src_index]; - if src_index == insertion_index { + self.edges[dst_index] = self.edges[src_index]; + if src_index == insert_ix { break; } src_index -= 1; dst_index -= 1; } } - self.hints[insertion_index] = first; + self.edges[insert_ix] = first_edge; if is_pair { - self.hints[insertion_index + 1] = second; - } - if TRACE { - // println!( - // " Inserting hint at {} ({})", - // first.coord.to_f32(), - // first.ds_coord.to_f32() - // ); - // if is_pair { - // println!( - // " Inserting hint at {} ({})", - // second.coord.to_f32(), - // second.ds_coord.to_f32() - // ); - // } + self.edges[insert_ix + 1] = second_edge; } - self.len += count; + self.len += edge_count; } + /// Adjust hint pairs so that one of the two edges is on a pixel boundary. + /// + /// Based on fn adjust(&mut self) { - let mut saved = [(0usize, Fixed(0)); MAX_HINTS]; + let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; let mut saved_count = 0usize; let mut i = 0; + // From FT with adjustments for variable names: + // "First pass is bottom-up (font hint order) without look-ahead. + // Locked edges are already adjusted. + // Unlocked edges begin with ds_coord from `initial_map'. + // Save edges that are not optimally adjusted in `saved' array, + // and process them in second pass." let limit = self.len; while i < limit { - let is_pair = self.hints[i].is_pair(); + let is_pair = self.edges[i].is_pair(); let j = if is_pair { i + 1 } else { i }; - if !self.hints[i].is_locked() { - let frac_down = self.hints[i].ds_coord.fract(); - let frac_up = self.hints[j].ds_coord.fract(); - let down_move_down = Fixed(0) - frac_down; - let up_move_down = Fixed(0) - frac_up; - let down_move_up = if frac_down == Fixed(0) { - Fixed(0) + if !self.edges[i].is_locked() { + // We can adjust hint edges that are not locked + let frac_down = self.edges[i].ds_coord.fract(); + let frac_up = self.edges[j].ds_coord.fract(); + // There are four possibilities. We compute them all. + // (moves down are negative) + let down_move_down = Fixed::ZERO - frac_down; + let up_move_down = Fixed::ZERO - frac_up; + let down_move_up = if frac_down == Fixed::ZERO { + Fixed::ZERO } else { Fixed::ONE - frac_down }; - let up_move_up = if frac_up == Fixed(0) { - Fixed(0) + let up_move_up = if frac_up == Fixed::ZERO { + Fixed::ZERO } else { Fixed::ONE - frac_up }; - let move_up = Fixed(down_move_up.0.min(up_move_up.0)); - let move_down = Fixed(down_move_down.0.max(up_move_down.0)); - const MIN_COUNTER: Fixed = Fixed(0x8000); + // Smallest move up + let move_up = down_move_up.min(up_move_up); + // Smallest move down + let move_down = down_move_down.max(up_move_down); let mut save_edge = false; let adjustment; + // Check for room to move up: + // 1. We're at the top of the array, or + // 2. The next edge is at or above the proposed move up if j >= self.len - 1 - || self.hints[j + 1].ds_coord - >= (self.hints[j].ds_coord + move_up + MIN_COUNTER) + || self.edges[j + 1].ds_coord + >= (self.edges[j].ds_coord + move_up + MIN_COUNTER) { + // Also check for room to move down... if i == 0 - || self.hints[i - 1].ds_coord - <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) + || self.edges[i - 1].ds_coord + <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) { + // .. and move the smallest distance adjustment = if -move_down < move_up { move_down } else { @@ -689,311 +679,801 @@ impl HintMap { adjustment = move_up; } } else if i == 0 - || self.hints[i - 1].ds_coord - <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) + || self.edges[i - 1].ds_coord + <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) { + // We can move down adjustment = move_down; + // True if the move is not optimum save_edge = move_up < -move_down; } else { - adjustment = Fixed(0); + // We can't move either way without overlapping + adjustment = Fixed::ZERO; save_edge = true; } - if save_edge && j < self.len - 1 && !self.hints[j + 1].is_locked() { + // Capture non-optimal adjustments and save them for a second + // pass. This is only possible if the edge above is unlocked + // and can be moved. + if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() { + // (index, desired adjustment) saved[saved_count] = (j, move_up - adjustment); saved_count += 1; } - self.hints[i].ds_coord += adjustment; + // Apply the adjustment + self.edges[i].ds_coord += adjustment; if is_pair { - self.hints[j].ds_coord += adjustment; + self.edges[j].ds_coord += adjustment; } } - if i > 0 && self.hints[i].coord != self.hints[i - 1].coord { - let a = self.hints[i]; - let b = self.hints[i - 1]; - self.hints[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); + // Compute the new edge scale + if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord { + let a = self.edges[i]; + let b = self.edges[i - 1]; + self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); } if is_pair { - if self.hints[j].coord != self.hints[j - 1].coord { - let a = self.hints[j]; - let b = self.hints[j - 1]; - self.hints[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); + if self.edges[j].cs_coord != self.edges[j - 1].cs_coord { + let a = self.edges[j]; + let b = self.edges[j - 1]; + self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); } i += 1; } i += 1; } - for i in (0..saved_count).rev() { - let (j, adjustment) = saved[i]; - if self.hints[j + 1].ds_coord - >= (self.hints[j].ds_coord + adjustment + Fixed::from_f32(0.5)) - { - self.hints[j].ds_coord += adjustment; - if self.hints[j].is_pair() { - self.hints[j - 1].ds_coord += adjustment; + // Second pass tries to move non-optimal edges up if the first + // pass created room + for (j, adjustment) in saved[..saved_count].iter().copied().rev() { + if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) { + self.edges[j].ds_coord += adjustment; + if self.edges[j].is_pair() { + self.edges[j - 1].ds_coord += adjustment; } } } } + /// Builds a hintmap from hints and mask. + /// + /// If `initial_map` is invalid, this recurses one level to initialize + /// it. If `is_initial` is true, simply build the initial map. + /// + /// Based on fn build( &mut self, - state: &HinterState, + state: &HintState, mask: Option, mut initial_map: Option<&mut HintMap>, stems: &mut [StemHint], origin: Fixed, - initial: bool, + is_initial: bool, ) { let scale = state.scale; - let darken_y = Fixed(0); - if !initial { - if let Some(ref mut initial_map) = initial_map { - if !initial_map.valid { - initial_map.build(state, None, None, stems, origin, true); + let darken_y = Fixed::ZERO; + if !is_initial { + if let Some(initial_map) = &mut initial_map { + if !initial_map.is_valid { + // Note: recursive call here to build the initial map if it + // is provided and invalid + initial_map.build(state, Some(HintMask::all()), None, stems, origin, true); } } } let initial_map = initial_map.map(|x| x as &HintMap); self.clear(); + // If the mask is missing or invalid, assume all hints are active let mut mask = mask.unwrap_or_else(HintMask::all); - if !mask.valid { + if !mask.is_valid { mask = HintMask::all(); } - if state.em_box_hints { + if state.do_em_box_hints { + // FreeType generates these during blues initialization. Do + // it here just to avoid carrying the extra state in the + // already large HintState struct. + // let mut bottom = Hint::default(); - bottom.coord = ICF_BOTTOM - Fixed(1); - bottom.ds_coord = (bottom.coord * scale).round() - Fixed::from_f32(0.5); + bottom.cs_coord = ICF_BOTTOM - EPSILON; + bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER; bottom.scale = scale; bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; let mut top = Hint::default(); - top.coord = ICF_TOP + Fixed(1); - top.ds_coord = (top.coord * scale).round() + Fixed::from_f32(0.5); + top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y); + top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER; top.scale = scale; top.flags = GHOST_TOP | LOCKED | SYNTHETIC; let invalid = Hint::default(); - self.insert(&bottom, &invalid, scale, initial_map); - self.insert(&invalid, &top, scale, initial_map); + self.insert(&bottom, &invalid, initial_map); + self.insert(&invalid, &top, initial_map); } let mut tmp_mask = mask; + // FreeType iterates over the hint mask with some fancy bit logic. We + // do the simpler thing and loop over the stems. + // for (i, stem) in stems.iter().enumerate() { if !tmp_mask.get(i) { continue; } + let hint_ix = i as u8; let mut bottom = Hint::default(); let mut top = Hint::default(); - bottom.setup(stem, i as u8, origin, scale, darken_y, true); - top.setup(stem, i as u8, origin, scale, darken_y, false); + bottom.setup(stem, hint_ix, origin, scale, darken_y, true); + top.setup(stem, hint_ix, origin, scale, darken_y, false); + // Insert hints that are locked or captured by a blue zone if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { - if initial { - self.insert(&bottom, &top, scale, None); + if is_initial { + self.insert(&bottom, &top, None); } else { - self.insert(&bottom, &top, scale, initial_map); + self.insert(&bottom, &top, initial_map); } + // Avoid processing this hint in the second pass tmp_mask.clear(i); } } - if initial { - if self.len == 0 || self.hints[0].coord.0 > 0 || self.hints[self.len - 1].coord.0 < 0 { + if is_initial { + // Heuristic: insert a point at (0, 0) if it's not covered by a + // mapping. Ensures a lock at baseline for glyphs missing a + // baseline hint. + if self.len == 0 + || self.edges[0].cs_coord > Fixed::ZERO + || self.edges[self.len - 1].cs_coord < Fixed::ZERO + { let edge = Hint { flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, scale, ..Default::default() }; let invalid = Hint::default(); - self.insert(&edge, &invalid, scale, None); + self.insert(&edge, &invalid, None); } } else { + // Insert hints that were skipped in the first pass for (i, stem) in stems.iter().enumerate() { if !tmp_mask.get(i) { continue; } + let hint_ix = i as u8; let mut bottom = Hint::default(); let mut top = Hint::default(); - bottom.setup(stem, i as u8, origin, scale, darken_y, true); - top.setup(stem, i as u8, origin, scale, darken_y, false); - self.insert(&bottom, &top, scale, initial_map); + bottom.setup(stem, hint_ix, origin, scale, darken_y, true); + top.setup(stem, hint_ix, origin, scale, darken_y, false); + self.insert(&bottom, &top, initial_map); } } - self.dump(); + // Adjust edges that are not locked to blue zones self.adjust(); - self.dump(); - if !initial { - for i in 0..self.len { - let hint = &self.hints[i]; - if hint.is_synthetic() { + if !is_initial { + // Save position of edges that were used by the hint map. + for edge in &self.edges[..self.len] { + if edge.is_synthetic() { continue; } - let stem = &mut stems[hint.index as usize]; - if hint.is_top() { - stem.ds_max = hint.ds_coord; + let stem = &mut stems[edge.index as usize]; + if edge.is_top() { + stem.ds_max = edge.ds_coord; } else { - stem.ds_min = hint.ds_coord; + stem.ds_min = edge.ds_coord; } - stem.used = true; + stem.is_used = true; } - self.valid = true; - } - self.valid = true; - } - - fn dump(&self) { - // if !TRACE { - // return; - // } - // for i in 0..self.len { - // let hint = self.hints[i]; - // println!( - // "[{}] {} {} {} {}{}{}{}{}{}", - // hint.index, - // hint.coord.to_f32(), - // hint.ds_coord.to_f32() / hint.scale.to_f32(), - // hint.scale.to_f32() * 65536., - // if hint.is_pair() { "p" } else { "" }, - // if hint.flags & (GHOST_BOTTOM | GHOST_TOP) != 0 { - // "g" - // } else { - // "" - // }, - // if hint.is_top() { "t" } else { "" }, - // if hint.is_bottom() { "b" } else { "" }, - // if hint.is_locked() { "L" } else { "" }, - // if hint.is_synthetic() { "S" } else { "" }, - // ); - // } - // println!("-------------------------------"); + } + self.is_valid = true; } } -#[derive(Copy, Clone, PartialEq)] -pub struct HintMask { +/// Bitmask that specifies which hints are currently active. +/// +/// "Each bit of the mask, starting with the most-significant bit of +/// the first byte, represents the corresponding hint zone in the +/// order in which the hints were declared at the beginning of +/// the charstring." +/// +/// See +/// Also +#[derive(Copy, Clone, PartialEq, Default)] +struct HintMask { mask: [u8; HINT_MASK_SIZE], - valid: bool, + is_valid: bool, } impl HintMask { - pub fn new() -> Self { - Self { - mask: [0u8; HINT_MASK_SIZE], - valid: false, + fn new(bytes: &[u8]) -> Option { + let len = bytes.len(); + if len > HINT_MASK_SIZE { + return None; } + let mut mask = Self::default(); + mask.mask[..len].copy_from_slice(&bytes[..len]); + mask.is_valid = true; + Some(mask) } - pub fn all() -> Self { + fn all() -> Self { Self { mask: [0xFF; HINT_MASK_SIZE], - valid: true, + is_valid: true, } } - fn clear_all(&mut self) { - self.mask = [0u8; HINT_MASK_SIZE]; - self.valid = true; + fn clear(&mut self, bit: usize) { + self.mask[bit >> 3] &= !msb_mask(bit); } - pub fn set_mask(&mut self, mask: &[u8]) { - self.clear_all(); - if mask.len() > HINT_MASK_SIZE { - return; - } - for (i, b) in mask.iter().enumerate() { - self.mask[i] = *b; + fn get(&self, bit: usize) -> bool { + self.mask[bit >> 3] & msb_mask(bit) != 0 + } +} + +/// Returns a bit mask for the selected bit with the +/// most significant bit at index 0. +fn msb_mask(bit: usize) -> u8 { + 1 << (7 - (bit & 0x7)) +} + +pub(super) struct HintingSink<'a, S> { + state: &'a HintState, + sink: &'a mut S, + stem_hints: [StemHint; MAX_HINTS], + stem_count: u8, + mask: HintMask, + initial_map: HintMap, + map: HintMap, + /// Most recent move_to in character space. + start_point: Option<[Fixed; 2]>, + /// Most recent line_to. First two elements are coords in character + /// space and the last two are in device space. + pending_line: Option<[Fixed; 4]>, +} + +impl<'a, S: CommandSink> HintingSink<'a, S> { + pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { + let scale = state.scale; + Self { + state, + sink, + stem_hints: [StemHint::default(); MAX_HINTS], + stem_count: 0, + mask: HintMask::all(), + initial_map: HintMap::new(scale), + map: HintMap::new(scale), + start_point: None, + pending_line: None, } - self.valid = true; } - #[inline] - #[allow(dead_code)] - pub fn set(&mut self, bit: usize) { - self.mask[bit >> 3] |= 1 << (7 - (bit & 0x7)); + pub fn finish(&mut self) { + self.maybe_close_subpath(); } - #[inline] - pub fn clear(&mut self, bit: usize) { - self.mask[bit >> 3] &= !(1 << (7 - (bit & 0x7))); + fn maybe_close_subpath(&mut self) { + // This requires some explanation. The hint mask can be modified + // during charstring evaluation which changes the set of hints that + // are applied. FreeType ensures that the closing line for any subpath + // is transformed with the same hint map as the starting point for the + // subpath. This is done by stashing a copy of the hint map that is + // active when a new subpath is started. Unlike FreeType, we make use + // of close elements, so we can cheat a bit here and avoid the + // extra hintmap. If we're closing an open subpath and have a pending + // line and the line is not equal to the start point in character + // space, then we emit the saved device space coordinates for the + // line. If the coordinates do match in character space, we omit + // that line. The unconditional close command ensures that the + // start and end points coincide. + // Note: this doesn't apply to subpaths that end in cubics. + match (self.start_point.take(), self.pending_line.take()) { + (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => { + if start != [cs_x, cs_y] { + self.sink.line_to(ds_x, ds_y); + } + self.sink.close(); + } + (Some(_), _) => self.sink.close(), + _ => {} + } } - #[inline] - pub fn get(&self, bit: usize) -> bool { - self.mask[bit >> 3] & (1 << (7 - (bit & 0x7))) != 0 + fn flush_pending_line(&mut self) { + if let Some([_, _, x, y]) = self.pending_line.take() { + self.sink.line_to(x, y); + } } - // pub fn dump(&self) { - // for i in 0..MAX_STEM_HINTS { - // print!("{}", self.get(i) as u8); - // } - // println!(); - // for b in &self.mask { - // print!("{:#8b}", *b) - // } - // println!(); - // } -} + fn hint(&mut self, coord: Fixed) -> Fixed { + if !self.map.is_valid { + self.build_hint_map(Some(self.mask), Fixed::ZERO); + } + trunc(self.map.transform(coord)) + } -#[derive(Copy, Clone, Default)] -struct BlueZone { - is_bottom: bool, - bottom: Fixed, - top: Fixed, - flat: Fixed, - ds_flat: Fixed, -} + fn scale(&self, coord: Fixed) -> Fixed { + trunc(coord * self.state.scale) + } -#[derive(Copy, Clone, Default)] -struct BlueValues { - values: [(Fixed, Fixed); MAX_BLUE_VALUES], - len: u32, + fn add_stem(&mut self, min: Fixed, max: Fixed) { + let index = self.stem_count as usize; + if index >= MAX_HINTS || self.map.is_valid { + return; + } + let stem = &mut self.stem_hints[index]; + stem.min = min; + stem.max = max; + stem.is_used = false; + stem.ds_min = Fixed::ZERO; + stem.ds_max = Fixed::ZERO; + self.stem_count = index as u8 + 1; + } + + fn build_hint_map(&mut self, mask: Option, origin: Fixed) { + self.map.build( + self.state, + mask, + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + origin, + false, + ); + } } -impl BlueValues { - fn new() -> Self { - Self::default() +impl<'a, S: CommandSink> CommandSink for HintingSink<'a, S> { + fn hstem(&mut self, min: Fixed, max: Fixed) { + self.add_stem(min, max); } - fn set(&mut self, values: &[f32]) { - let len = values.len().min(MAX_BLUE_VALUES * 2); - for i in 0..len / 2 { - let a = values[i * 2]; - let b = values[i * 2 + 1]; - self.values[i] = (Fixed::from_i32(a as i32), Fixed::from_i32(b as i32)); + fn hint_mask(&mut self, mask: &[u8]) { + // For invalid hint masks, FreeType assumes all hints are active. + // See + let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); + if mask != self.mask { + self.mask = mask; + self.map.is_valid = false; } - self.len = len as u32 / 2; } - fn values(&self) -> &[(Fixed, Fixed)] { - &self.values[..self.len as usize] + fn counter_mask(&mut self, mask: &[u8]) { + // For counter masks, we build a temporary hint map "just to + // place and lock those stems participating in the counter + // mask." Building the map modifies the stem hint array as a + // side effect. + // See + let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); + let mut map = HintMap::new(self.state.scale); + map.build( + self.state, + Some(mask), + Some(&mut self.initial_map), + &mut self.stem_hints[..self.stem_count as usize], + Fixed::ZERO, + false, + ); + } + + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.maybe_close_subpath(); + self.start_point = Some([x, y]); + let x = self.scale(x); + let y = self.hint(y); + self.sink.move_to(x, y); + } + + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.flush_pending_line(); + let ds_x = self.scale(x); + let ds_y = self.hint(y); + self.pending_line = Some([x, y, ds_x, ds_y]); + } + + fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { + self.flush_pending_line(); + let cx1 = self.scale(cx1); + let cy1 = self.hint(cy1); + let cx2 = self.scale(cx2); + let cy2 = self.hint(cy2); + let x = self.scale(x); + let y = self.hint(y); + self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); + } + + fn close(&mut self) { + // We emit close commands based on the sequence of moves. + // See `maybe_close_subpath` } } -impl DictionarySink for HinterState { - fn blue_values(&mut self, values: &[f32]) { - self.blues.set(values); +/// FreeType converts from 16.16 to 26.6 by truncation. We keep our +/// values in 16.16 so simply zero the low 10 bits to match the +/// precision when converting to f32. +fn trunc(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() & !0x3FF) +} + +fn half(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits() / 2) +} + +fn twice(value: Fixed) -> Fixed { + Fixed::from_bits(value.to_bits().wrapping_mul(2)) +} + +#[cfg(test)] +mod tests { + use read_fonts::{tables::postscript::charstring::CommandSink, types::F2Dot14, FontRef}; + + use super::{ + BlueZone, Blues, Fixed, Hint, HintMap, HintMask, HintParams, HintState, HintingSink, + StemHint, GHOST_BOTTOM, GHOST_TOP, HINT_MASK_SIZE, LOCKED, PAIR_BOTTOM, PAIR_TOP, + }; + + fn make_hint_state() -> HintState { + fn make_blues(values: &[f64]) -> Blues { + Blues::new(values.iter().copied().map(Fixed::from_f64)) + } + // + // + // + // + // + let params = HintParams { + blues: make_blues(&[ + -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, + ]), + other_blues: make_blues(&[-255.0, -240.0]), + blue_scale: Fixed::from_f64(0.05), + blue_shift: Fixed::from_i32(7), + blue_fuzz: Fixed::ZERO, + ..Default::default() + }; + HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)) } - fn family_blues(&mut self, values: &[f32]) { - self.family_blues.set(values); + #[test] + fn scaled_blue_zones() { + let state = make_hint_state(); + assert!(!state.do_em_box_hints); + assert_eq!(state.zone_count, 6); + assert_eq!(state.boost, Fixed::from_bits(27035)); + assert!(state.supress_overshoot); + // FreeType generates the following zones: + let expected_zones = &[ + // csBottomEdge -983040 int + // csTopEdge 0 int + // csFlatEdge 0 int + // dsFlatEdge 0 int + // bottomZone 1 '\x1' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(-983040), + is_bottom: true, + ..Default::default() + }, + // csBottomEdge 35127296 int + // csTopEdge 35848192 int + // csFlatEdge 35127296 int + // dsFlatEdge 589824 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(35127296), + cs_top_edge: Fixed::from_bits(35848192), + cs_flat_edge: Fixed::from_bits(35127296), + ds_flat_edge: Fixed::from_bits(589824), + is_bottom: false, + }, + // csBottomEdge 37421056 int + // csTopEdge 38141952 int + // csFlatEdge 37421056 int + // dsFlatEdge 589824 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(37421056), + cs_top_edge: Fixed::from_bits(38141952), + cs_flat_edge: Fixed::from_bits(37421056), + ds_flat_edge: Fixed::from_bits(589824), + is_bottom: false, + }, + // csBottomEdge 46792704 int + // csTopEdge 47579136 int + // csFlatEdge 46792704 int + // dsFlatEdge 786432 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(46792704), + cs_top_edge: Fixed::from_bits(47579136), + cs_flat_edge: Fixed::from_bits(46792704), + ds_flat_edge: Fixed::from_bits(786432), + is_bottom: false, + }, + // csBottomEdge 49807360 int + // csTopEdge 50593792 int + // csFlatEdge 49807360 int + // dsFlatEdge 786432 int + // bottomZone 0 '\0' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(49807360), + cs_top_edge: Fixed::from_bits(50593792), + cs_flat_edge: Fixed::from_bits(49807360), + ds_flat_edge: Fixed::from_bits(786432), + is_bottom: false, + }, + // csBottomEdge -16711680 int + // csTopEdge -15728640 int + // csFlatEdge -15728640 int + // dsFlatEdge -262144 int + // bottomZone 1 '\x1' unsigned char + BlueZone { + cs_bottom_edge: Fixed::from_bits(-16711680), + cs_top_edge: Fixed::from_bits(-15728640), + cs_flat_edge: Fixed::from_bits(-15728640), + ds_flat_edge: Fixed::from_bits(-262144), + is_bottom: true, + }, + ]; + assert_eq!(state.zones(), expected_zones); } - fn other_blues(&mut self, values: &[f32]) { - self.other_blues.set(values); + #[test] + fn blue_zone_capture() { + let state = make_hint_state(); + let bottom_edge = Hint { + flags: PAIR_BOTTOM, + ds_coord: Fixed::from_f64(2.3), + ..Default::default() + }; + let top_edge = Hint { + flags: PAIR_TOP, + // This value chosen to fit within the first "top" blue zone + cs_coord: Fixed::from_bits(35127297), + ds_coord: Fixed::from_f64(2.3), + ..Default::default() + }; + // Capture both + { + let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge); + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(bottom_edge.is_locked()); + assert!(top_edge.is_locked()); + } + // Capture none + { + // Used to guarantee the edges are below all blue zones and will + // not be captured + let min_cs_coord = Fixed::MIN; + let mut bottom_edge = Hint { + cs_coord: min_cs_coord, + ..bottom_edge + }; + let mut top_edge = Hint { + cs_coord: min_cs_coord, + ..top_edge + }; + assert!(!state.capture(&mut bottom_edge, &mut top_edge)); + assert!(!bottom_edge.is_locked()); + assert!(!top_edge.is_locked()); + } + // Capture bottom, ignore invalid top + { + let mut bottom_edge = bottom_edge; + let mut top_edge = Hint { + // Empty flags == invalid hint + flags: 0, + ..top_edge + }; + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(bottom_edge.is_locked()); + assert!(!top_edge.is_locked()); + } + // Capture top, ignore invalid bottom + { + let mut bottom_edge = Hint { + // Empty flags == invalid hint + flags: 0, + ..bottom_edge + }; + let mut top_edge = top_edge; + assert!(state.capture(&mut bottom_edge, &mut top_edge)); + assert!(!bottom_edge.is_locked()); + assert!(top_edge.is_locked()); + } } - fn family_other_blues(&mut self, values: &[f32]) { - self.family_other_blues.set(values); + #[test] + fn hint_mask_ops() { + const MAX_BITS: usize = HINT_MASK_SIZE * 8; + let all_bits = HintMask::all(); + for i in 0..MAX_BITS { + assert!(all_bits.get(i)); + } + let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap(); + for i in 0..MAX_BITS { + assert_eq!(i & 1 != 0, odd_bits.get(i)); + } + let mut cleared_bits = odd_bits; + for i in 0..MAX_BITS { + if i & 1 != 0 { + cleared_bits.clear(i); + } + } + assert_eq!(cleared_bits.mask, HintMask::default().mask); } - fn blue_scale(&mut self, scale: f32) { - self.blue_scale = Fixed::from_f32(scale); + #[test] + fn hint_mapping() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff_font = super::super::outlines::Outlines::new(&font).unwrap(); + let state = cff_font + .subfont(0, 8.0, &[F2Dot14::from_f32(-1.0); 2]) + .unwrap() + .hint_state; + let mut initial_map = HintMap::new(state.scale); + let mut map = HintMap::new(state.scale); + // Stem hints from Cantarell-VF.otf glyph id 2 + let mut stems = [ + StemHint { + min: Fixed::from_bits(1376256), + max: Fixed::ZERO, + ..Default::default() + }, + StemHint { + min: Fixed::from_bits(16318464), + max: Fixed::from_bits(17563648), + ..Default::default() + }, + StemHint { + min: Fixed::from_bits(45481984), + max: Fixed::from_bits(44171264), + ..Default::default() + }, + ]; + map.build( + &state, + Some(HintMask::all()), + Some(&mut initial_map), + &mut stems, + Fixed::ZERO, + false, + ); + // FT generates the following hint map: + // + // index csCoord dsCoord scale flags + // 0 0.00 0.00 526 gbL + // 1 249.00 250.14 524 pb + // 1 268.00 238.22 592 pt + // 2 694.00 750.41 524 gtL + let expected_edges = [ + Hint { + index: 0, + cs_coord: Fixed::from_f64(0.0), + ds_coord: Fixed::from_f64(0.0), + scale: Fixed::from_bits(526), + flags: GHOST_BOTTOM | LOCKED, + }, + Hint { + index: 1, + cs_coord: Fixed::from_bits(16318464), + ds_coord: Fixed::from_bits(131072), + scale: Fixed::from_bits(524), + flags: PAIR_BOTTOM, + }, + Hint { + index: 1, + cs_coord: Fixed::from_bits(17563648), + ds_coord: Fixed::from_bits(141028), + scale: Fixed::from_bits(592), + flags: PAIR_TOP, + }, + Hint { + index: 2, + cs_coord: Fixed::from_bits(45481984), + ds_coord: Fixed::from_bits(393216), + scale: Fixed::from_bits(524), + flags: GHOST_TOP | LOCKED, + }, + ]; + assert_eq!(expected_edges, &map.edges[..map.len]); + // And FT generates the following mappings + let mappings = [ + // (coord in font units, expected hinted coord in device space) in 16.16 + (0, 0), // 0 -> 0 + (44302336, 382564), // 676 -> 5.828125 + (45481984, 393216), // 694 -> 6 + (16318464, 131072), // 249 -> 2 + (17563648, 141028), // 268 -> 2.140625 + (49676288, 426752), // 758 -> 6.5 + (56754176, 483344), // 866 -> 7.375 + (57868288, 492252), // 883 -> 7.5 + (50069504, 429896), // 764 -> 6.546875 + ]; + for (coord, expected) in mappings { + assert_eq!( + map.transform(Fixed::from_bits(coord)), + Fixed::from_bits(expected) + ); + } } - fn blue_shift(&mut self, shift: f32) { - self.blue_shift = Fixed::from_f32(shift); + /// HintingSink is mostly pass-through. This test captures the logic + /// around omission of pending lines that match subpath start. + /// See HintingSink::maybe_close_subpath for details. + #[test] + fn hinting_sink_omits_closing_line_that_matches_start() { + let state = HintState { + scale: Fixed::ONE, + ..Default::default() + }; + let mut path = Path::default(); + let mut sink = HintingSink::new(&state, &mut path); + let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; + let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)]; + let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; + let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)]; + let curve = [ + Fixed::from_f64(3.0), + Fixed::from_f64(4.0), + Fixed::from_f64(5.0), + Fixed::from_f64(6.0), + Fixed::from_f64(1.0), + Fixed::from_f64(2.0), + ]; + // First subpath, closing line matches start + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.line_to(line1_2[0], line1_2[1]); + // Second subpath, closing line does not match start + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.line_to(line3_4[0], line3_4[1]); + // Third subpath, ends with cubic. Still emits a close command + // even though end point matches start. + sink.move_to(move1_2[0], move1_2[1]); + sink.line_to(line2_3[0], line2_3[1]); + sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]); + sink.finish(); + // Subpaths always end with a close command. If a final line coincides + // with the start of a subpath, it is omitted. + assert_eq!( + &path.0, + &[ + // First subpath + MoveTo(move1_2), + LineTo(line2_3), + // line1_2 is omitted + Close, + // Second subpath + MoveTo(move1_2), + LineTo(line2_3), + LineTo(line3_4), + Close, + // Third subpath + MoveTo(move1_2), + LineTo(line2_3), + CurveTo(curve), + Close, + ] + ); } - fn blue_fuzz(&mut self, fuzz: f32) { - self.blue_fuzz = Fixed::from_f32(fuzz); + #[derive(Copy, Clone, PartialEq, Debug)] + enum Command { + MoveTo([Fixed; 2]), + LineTo([Fixed; 2]), + CurveTo([Fixed; 6]), + Close, } - fn language_group(&mut self, group: u32) { - self.language_group = group as u8; + use Command::*; + + #[derive(Default)] + struct Path(Vec); + + impl CommandSink for Path { + fn move_to(&mut self, x: Fixed, y: Fixed) { + self.0.push(MoveTo([x, y])); + } + fn line_to(&mut self, x: Fixed, y: Fixed) { + self.0.push(LineTo([x, y])); + } + fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) { + self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y])); + } + fn close(&mut self) { + self.0.push(Close); + } } } diff --git a/src/scale/cff/mod.rs b/src/scale/cff/mod.rs index 63d17ae..b2f6ae8 100644 --- a/src/scale/cff/mod.rs +++ b/src/scale/cff/mod.rs @@ -1,26 +1,18 @@ -/*! -PostScript outlines. - -*/ - -#[allow(clippy::module_inception)] -mod cff; mod hint; +mod outlines; -pub use cff::{Cff, CffProxy, Glyph, GlyphSink}; -pub use hint::HinterState; +pub(crate) use outlines::Outlines; -use super::{internal, TRACE}; +use super::Outline; +use read_fonts::types::{F2Dot14, GlyphId}; -use crate::font::FontRef; - -pub struct Scaler { +pub struct SubfontCache { entries: Vec, max_entries: usize, epoch: u64, } -impl Scaler { +impl SubfontCache { pub fn new(max_entries: usize) -> Self { Self { entries: Vec::new(), @@ -31,82 +23,70 @@ impl Scaler { pub fn scale( &mut self, - font: &FontRef, + outlines: &outlines::Outlines, id: u64, + glyph_id: u16, + size: f32, coords: &[i16], - proxy: &CffProxy, - scale: f32, hint: bool, - glyph_id: u16, - sink: &mut impl GlyphSink, + outline: &mut Outline, ) -> Option<()> { - let cff = proxy.materialize(font); - let glyph = cff.get(glyph_id)?; - if hint { - let dict = glyph.subfont_index(); - let state = self.entry(id, dict, coords, scale, &glyph); - if glyph.path(scale, coords, Some(state), sink) { - Some(()) - } else { - None - } - } else if glyph.path(scale, coords, None, sink) { - Some(()) - } else { - None - } - } - - fn entry( - &mut self, - id: u64, - dict: u16, - coords: &[i16], - scale: f32, - glyph: &Glyph, - ) -> &HinterState { let epoch = self.epoch; - let (found, index) = self.find_entry(id, dict, coords, scale); - if found { - let entry = &mut self.entries[index]; + let gid = GlyphId::new(glyph_id); + let subfont_index = outlines.subfont_index(gid); + let (found, entry_index) = self.find_entry(id, subfont_index, coords, size); + let (subfont, coords) = if found { + let entry = &mut self.entries[entry_index]; entry.epoch = epoch; - &entry.state + (&entry.subfont, &entry.coords) } else { self.epoch += 1; - let state = HinterState::new(glyph, scale, coords); - if index == self.entries.len() { + let epoch = self.epoch; + if entry_index == self.entries.len() { + let coords: Vec = coords.iter().map(|x| F2Dot14::from_bits(*x)).collect(); + let subfont = outlines.subfont(subfont_index, size, &coords).ok()?; self.entries.push(Entry { - epoch, id, - dict, - state, - coords: Vec::from(coords), - scale, + epoch, + subfont, + subfont_index, + size, + coords, }); - &self.entries[index].state + let entry = &self.entries[entry_index]; + (&entry.subfont, &entry.coords) } else { - let entry = &mut self.entries[index]; + let entry = &mut self.entries[entry_index]; + entry.id = u64::MAX; entry.epoch = epoch; - entry.id = id; - entry.dict = dict; - entry.state = state; entry.coords.clear(); - entry.coords.extend_from_slice(coords); - entry.scale = scale; - &entry.state + entry + .coords + .extend(coords.iter().map(|x| F2Dot14::from_bits(*x))); + entry.subfont = outlines.subfont(subfont_index, size, &entry.coords).ok()?; + entry.id = id; + entry.subfont_index = subfont_index; + entry.size = size; + (&entry.subfont, &entry.coords) } - } + }; + outlines + .draw(subfont, gid, coords, hint, &mut OutlineBuilder(outline)) + .ok()?; + Some(()) } - fn find_entry(&self, id: u64, dict: u16, coords: &[i16], scale: f32) -> (bool, usize) { + fn find_entry(&self, id: u64, index: u32, coords: &[i16], size: f32) -> (bool, usize) { let mut lowest_epoch = self.epoch; let mut lowest_index = 0; - let vary = !coords.is_empty(); for (i, entry) in self.entries.iter().enumerate() { if entry.id == id - && entry.dict == dict - && entry.scale == scale - && (!vary || (coords == &entry.coords[..])) + && entry.subfont_index == index + && entry.size == size + && coords + .iter() + .map(|x| F2Dot14::from_bits(*x)) + .eq(entry.coords.iter().copied()) { return (true, i); } @@ -125,8 +105,33 @@ impl Scaler { struct Entry { epoch: u64, id: u64, - dict: u16, - state: HinterState, - coords: Vec, - scale: f32, + subfont: outlines::Subfont, + subfont_index: u32, + size: f32, + coords: Vec, +} + +struct OutlineBuilder<'a>(&'a mut Outline); + +impl read_fonts::types::Pen for OutlineBuilder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, y).into()); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x, y).into()); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.0.quad_to((cx0, cy0).into(), (x, y).into()); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0 + .curve_to((cx0, cy0).into(), (cx1, cy1).into(), (x, y).into()); + } + + fn close(&mut self) { + self.0.close(); + } } diff --git a/src/scale/cff3/outlines.rs b/src/scale/cff/outlines.rs similarity index 100% rename from src/scale/cff3/outlines.rs rename to src/scale/cff/outlines.rs diff --git a/src/scale/cff2/hint.rs b/src/scale/cff2/hint.rs deleted file mode 100644 index 291f39e..0000000 --- a/src/scale/cff2/hint.rs +++ /dev/null @@ -1,979 +0,0 @@ -//! PostScript hinting. - -const TRACE: bool = false; - -use read_fonts::tables::postscript::{charstring::CommandSink, dict::Blues}; -use read_fonts::types::Fixed; - -// "Default values for OS/2 typoAscender/Descender.." -// See -const ICF_TOP: Fixed = Fixed::from_i32(880); -const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); - -// -const MAX_OTHER_BLUES: usize = 5; -const MAX_BLUE_ZONES: usize = 12; - -const MAX_STEM_HINTS: usize = 48; -const MAX_HINTS: usize = MAX_STEM_HINTS * 2; - -const HINT_MASK_SIZE: usize = (MAX_STEM_HINTS + 7) / 4; - -const EPSILON: Fixed = Fixed::from_bits(1); - -/// Parameters used to generate the stem and counter zones for the hinting -/// algorithm. -#[derive(Clone)] -pub(crate) struct HintParams { - pub blues: Blues, - pub family_blues: Blues, - pub other_blues: Blues, - pub family_other_blues: Blues, - pub blue_scale: Fixed, - pub blue_shift: Fixed, - pub blue_fuzz: Fixed, - pub language_group: i32, -} - -impl Default for HintParams { - fn default() -> Self { - Self { - blues: Blues::default(), - other_blues: Blues::default(), - family_blues: Blues::default(), - family_other_blues: Blues::default(), - // See - blue_scale: Fixed::from_f64(0.039625), - blue_shift: Fixed::from_i32(7), - blue_fuzz: Fixed::ONE, - language_group: 0, - } - } -} - -/// Hinting state for a PostScript subfont. -/// -/// Note that hinter states depend on the scale, subfont index and -/// variation coordinates of a glyph. They can be retained and reused -/// if those values remain the same. -#[derive(Copy, Clone)] -pub(crate) struct HintState { - scale: Fixed, - blue_scale: Fixed, - // These will be used in later code. - #[allow(dead_code)] - blue_shift: Fixed, - #[allow(dead_code)] - blue_fuzz: Fixed, - language_group: u8, - supress_overshoot: bool, - do_em_box_hints: bool, - boost: Fixed, - darken_y: Fixed, - zones: [BlueZone; MAX_BLUE_ZONES], - zone_count: usize, -} - -impl HintState { - pub fn new(params: &HintParams, scale: Fixed) -> Self { - let mut state = Self { - scale, - blue_scale: params.blue_scale, - blue_shift: params.blue_shift, - blue_fuzz: params.blue_fuzz, - language_group: params.language_group as u8, - supress_overshoot: false, - do_em_box_hints: false, - boost: Fixed::ZERO, - darken_y: Fixed::ZERO, - zones: [BlueZone::default(); MAX_BLUE_ZONES], - zone_count: 0, - }; - state.build_zones(params); - state - } - - /// Initialize zones from the set of blues values. - /// - /// See - fn build_zones(&mut self, params: &HintParams) { - self.do_em_box_hints = false; - // - match (self.language_group, params.blues.values().len()) { - (1, 2) => { - let blues = params.blues.values(); - if blues[0].0 < ICF_BOTTOM - && blues[0].1 < ICF_BOTTOM - && blues[1].0 > ICF_TOP - && blues[1].1 > ICF_TOP - { - // FreeType generates synthetic hints here. We'll do it - // later when building the hint map. - self.do_em_box_hints = true; - return; - } - } - (1, 0) => { - self.do_em_box_hints = true; - return; - } - _ => {} - } - let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; - let mut max_zone_height = Fixed::ZERO; - let mut zone_count = 0usize; - // Copy blues and other blues to a combined array of top and bottom zones. - for blue in params.blues.values() { - // FreeType loads blues as integers and then expands to 16.16 - // at initialization. We load them as 16.16 so floor them here - // to ensure we match. - // - let bottom = blue.0.floor(); - let top = blue.1.floor(); - let zone_height = top - bottom; - if zone_height < Fixed::ZERO { - // Reject zones with negative height - continue; - } - max_zone_height = max_zone_height.max(zone_height); - let zone = &mut zones[zone_count]; - zone.cs_bottom_edge = bottom; - zone.cs_top_edge = top; - if zone_count == 0 { - // First blue value is bottom zone - zone.is_bottom = true; - zone.cs_flat_edge = top; - } else { - // Adjust both edges of top zone upward by twice darkening amount - zone.cs_top_edge += twice(self.darken_y); - zone.cs_bottom_edge += twice(self.darken_y); - // Remaining blue values are top zones - zone.is_bottom = false; - zone.cs_flat_edge = zone.cs_bottom_edge; - } - zone_count += 1; - } - for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { - let bottom = blue.0.floor(); - let top = blue.1.floor(); - let zone_height = top - bottom; - if zone_height < Fixed::ZERO { - // Reject zones with negative height - continue; - } - max_zone_height = max_zone_height.max(zone_height); - let zone = &mut zones[zone_count]; - // All "other" blues are bottom zone - zone.is_bottom = true; - zone.cs_bottom_edge = bottom; - zone.cs_top_edge = top; - zone.cs_flat_edge = top; - zone_count += 1; - } - // Adjust for family blues - let units_per_pixel = Fixed::ONE / self.scale; - for zone in &mut zones[..zone_count] { - let flat = zone.cs_flat_edge; - let mut min_diff = Fixed::MAX; - if zone.is_bottom { - // In a bottom zone, the top edge is the flat edge. - // Search family other blues for bottom zones. Look for the - // closest edge that is within the one pixel threshold. - for blue in params.family_other_blues.values() { - let family_flat = blue.1; - let diff = (flat - family_flat).abs(); - if diff < min_diff && diff < units_per_pixel { - zone.cs_flat_edge = family_flat; - min_diff = diff; - if diff == Fixed::ZERO { - break; - } - } - } - // Check the first member of family blues, which is a bottom - // zone - if !params.family_blues.values().is_empty() { - let family_flat = params.family_blues.values()[0].1; - let diff = (flat - family_flat).abs(); - if diff < min_diff && diff < units_per_pixel { - zone.cs_flat_edge = family_flat; - } - } - } else { - // In a top zone, the bottom edge is the flat edge. - // Search family blues for top zones, skipping the first, which - // is a bottom zone. Look for closest family edge that is - // within the one pixel threshold. - for blue in params.family_blues.values().iter().skip(1) { - let family_flat = blue.0 + twice(self.darken_y); - let diff = (flat - family_flat).abs(); - if diff < min_diff && diff < units_per_pixel { - zone.cs_flat_edge = family_flat; - min_diff = diff; - if diff == Fixed::ZERO { - break; - } - } - } - } - } - if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { - // Clamp at maximum scale - self.blue_scale = Fixed::ONE / max_zone_height; - } - // Suppress overshoot and boost blue zones at small sizes - if self.scale < self.blue_scale { - self.supress_overshoot = true; - self.boost = - Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); - // boost must remain less than 0.5, or baseline could go negative - self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); - } - if self.darken_y != Fixed::ZERO { - self.boost = Fixed::ZERO; - } - // Set device space alignment for each zone; apply boost amount before - // rounding flat edge - let scale = self.scale; - let boost = self.boost; - for zone in &mut zones[..zone_count] { - let boost = if zone.is_bottom { -boost } else { boost }; - zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); - } - self.zones = zones; - self.zone_count = zone_count; - } - - fn capture(&self, bottom: &mut Hint, top: &mut Hint) -> bool { - let fuzz = self.blue_fuzz; - let mut captured = false; - let mut adjustment = Fixed::ZERO; - for zone in &self.zones[..self.zone_count] { - if zone.is_bottom - && bottom.is_bottom() - && (zone.cs_bottom_edge - fuzz) <= bottom.coord - && bottom.coord <= (zone.cs_top_edge + fuzz) - { - adjustment = if self.supress_overshoot { - zone.ds_flat_edge - } else if zone.cs_top_edge - bottom.coord >= self.blue_shift { - bottom.ds_coord.round().min(zone.ds_flat_edge - Fixed::ONE) - } else { - bottom.ds_coord.round() - } - bottom.ds_coord; - captured = true; - break; - } - if !zone.is_bottom - && top.is_top() - && (zone.cs_bottom_edge - fuzz) <= top.coord - && top.coord <= (zone.cs_top_edge + fuzz) - { - adjustment = if self.supress_overshoot { - zone.ds_flat_edge - } else if top.coord - zone.cs_bottom_edge >= self.blue_shift { - top.ds_coord.round().max(zone.ds_flat_edge + Fixed::ONE) - } else { - top.ds_coord.round() - } - top.ds_coord; - captured = true; - break; - } - } - if captured { - if bottom.is_valid() { - bottom.ds_coord += adjustment; - bottom.lock(); - } - if top.is_valid() { - top.ds_coord += adjustment; - top.lock(); - } - } - captured - } -} - -pub(crate) struct Hinter<'a, S> { - state: &'a HintState, - sink: &'a mut S, - stem_hints: [StemHint; MAX_STEM_HINTS], - stem_count: u8, - mask: HintMask, - initial_map: HintMap, - map: HintMap, -} - -impl<'a, S: CommandSink> Hinter<'a, S> { - pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { - Self { - state, - sink, - stem_hints: [StemHint::default(); MAX_STEM_HINTS], - stem_count: 0, - mask: HintMask::all(), - initial_map: HintMap::new(), - map: HintMap::new(), - } - } - - fn hint(&mut self, coord: Fixed) -> Fixed { - if !self.map.valid { - self.build_hint_map(Some(self.mask), Fixed::ZERO); - } - Fixed::from_bits(self.map.apply(self.state.scale, coord).to_bits() >> 6) - } - - #[inline(always)] - fn scale(&self, coord: Fixed) -> Fixed { - Fixed::from_bits((coord * self.state.scale).to_bits() >> 6) - } - - fn add_stem(&mut self, min: Fixed, max: Fixed) { - let index = self.stem_count as usize; - if index >= MAX_STEM_HINTS || self.map.valid { - return; - } - let stem = &mut self.stem_hints[index]; - stem.min = min; - stem.max = max; - stem.used = false; - stem.ds_min = Fixed::ZERO; - stem.ds_max = Fixed::ZERO; - self.stem_count = index as u8 + 1; - } - - fn build_hint_map(&mut self, mask: Option, origin: Fixed) { - self.map.build( - self.state, - mask, - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - origin, - false, - ); - } -} - -impl<'a, S: CommandSink> CommandSink for Hinter<'a, S> { - fn hstem(&mut self, y: Fixed, dy: Fixed) { - self.add_stem(y, dy); - } - - fn hint_mask(&mut self, mask: &[u8]) { - let mut hint_mask = HintMask::new(); - hint_mask.set_mask(mask); - if TRACE { - //println!("Got hintmask: {:?}", mask); - } - if hint_mask != self.mask { - self.mask = hint_mask; - self.map.valid = false; - } - } - - fn counter_mask(&mut self, mask: &[u8]) { - let mut hint_mask = HintMask::new(); - hint_mask.set_mask(mask); - let mut map = HintMap::new(); - map.build( - self.state, - Some(hint_mask), - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - Fixed::ZERO, - false, - ); - } - - fn move_to(&mut self, x: Fixed, y: Fixed) { - let x = self.scale(x); - let y = self.hint(y); - self.sink.move_to(x, y); - } - - fn line_to(&mut self, x: Fixed, y: Fixed) { - let x = self.scale(x); - let y = self.hint(y); - self.sink.line_to(x, y); - } - - fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { - let cx1 = self.scale(cx1); - let cy1 = self.hint(cy1); - let cx2 = self.scale(cx2); - let cy2 = self.hint(cy2); - let x = self.scale(x); - let y = self.hint(y); - self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) { - self.sink.close(); - } -} - -/// See -#[derive(Copy, Clone, PartialEq, Default, Debug)] -struct BlueZone { - is_bottom: bool, - cs_bottom_edge: Fixed, - cs_top_edge: Fixed, - cs_flat_edge: Fixed, - ds_flat_edge: Fixed, -} - -#[derive(Copy, Clone, Default)] -struct StemHint { - used: bool, - min: Fixed, - max: Fixed, - ds_min: Fixed, - ds_max: Fixed, -} - -// Hint flags -const GHOST_BOTTOM: u8 = 0x1; -const GHOST_TOP: u8 = 0x2; -const PAIR_BOTTOM: u8 = 0x4; -const PAIR_TOP: u8 = 0x8; -const LOCKED: u8 = 0x10; -const SYNTHETIC: u8 = 0x20; - -/// See -#[derive(Copy, Clone, Default)] -struct Hint { - flags: u8, - index: u8, - coord: Fixed, - ds_coord: Fixed, - scale: Fixed, -} - -impl Hint { - fn is_valid(&self) -> bool { - self.flags != 0 - } - - fn is_bottom(&self) -> bool { - self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0 - } - - fn is_top(&self) -> bool { - self.flags & (GHOST_TOP | PAIR_TOP) != 0 - } - - fn is_pair(&self) -> bool { - self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0 - } - - fn is_pair_top(&self) -> bool { - self.flags & PAIR_TOP != 0 - } - - fn is_locked(&self) -> bool { - self.flags & LOCKED != 0 - } - - fn is_synthetic(&self) -> bool { - self.flags & SYNTHETIC != 0 - } - - fn lock(&mut self) { - self.flags |= LOCKED - } - - fn setup( - &mut self, - stem: &StemHint, - index: u8, - origin: Fixed, - scale: Fixed, - darken_y: Fixed, - is_bottom: bool, - ) { - let width = stem.max - stem.min; - if width == Fixed::from_i32(-21) { - if is_bottom { - self.coord = stem.max; - self.flags = GHOST_BOTTOM; - } else { - self.flags = 0; - } - } else if width == Fixed::from_i32(-20) { - if !is_bottom { - self.coord = stem.min; - self.flags = GHOST_TOP; - } else { - self.flags = 0; - } - } else if width < Fixed::ZERO { - if is_bottom { - self.coord = stem.max; - self.flags = PAIR_BOTTOM; - } else { - self.coord = stem.min; - self.flags = PAIR_TOP; - } - } else if is_bottom { - self.coord = stem.min; - self.flags = PAIR_BOTTOM; - } else { - self.coord = stem.max; - self.flags = PAIR_TOP; - } - if self.is_top() { - self.coord += twice(darken_y); - } - self.coord += origin; - self.scale = scale; - self.index = index; - if self.flags != 0 && stem.used { - if self.is_top() { - self.ds_coord = stem.ds_max; - } else { - self.ds_coord = stem.ds_min; - } - self.lock(); - } else { - self.ds_coord = self.coord * scale; - } - } -} - -#[derive(Copy, Clone)] -struct HintMap { - hints: [Hint; MAX_HINTS], - len: usize, - valid: bool, -} - -impl HintMap { - fn new() -> Self { - Self { - hints: [Hint::default(); MAX_HINTS], - len: 0, - valid: false, - } - } - - fn clear(&mut self) { - self.len = 0; - self.valid = false; - } - - fn apply(&self, scale: Fixed, coord: Fixed) -> Fixed { - if self.len == 0 { - return coord * scale; - } - let limit = self.len - 1; - let mut i = 0; - while i < limit && coord >= self.hints[i + 1].coord { - i += 1; - } - while i > 0 && coord < self.hints[i].coord { - i -= 1; - } - let hint = &self.hints[i]; - if i == 0 && coord < self.hints[0].coord { - ((coord - self.hints[0].coord) * scale) + self.hints[0].ds_coord - } else { - ((coord - hint.coord) * hint.scale) + hint.ds_coord - } - } - - fn insert(&mut self, bottom: &Hint, top: &Hint, scale: Fixed, initial: Option<&HintMap>) { - let (is_pair, mut first) = if !bottom.is_valid() { - (false, *top) - } else if !top.is_valid() { - (false, *bottom) - } else { - (true, *bottom) - }; - let mut second = *top; - if is_pair && top.coord < bottom.coord { - return; - } - let count = if is_pair { 2 } else { 1 }; - if self.len + count > MAX_HINTS { - return; - } - if TRACE { - // println!( - // " Got hint at {} ({})", - // first.coord.to_f64(), - // first.ds_coord.to_f64() - // ); - // if is_pair { - // println!( - // " Got hint at {} ({})", - // second.coord.to_f64(), - // second.ds_coord.to_f64() - // ); - // } - } - let mut insertion_index = 0; - while insertion_index < self.len { - if self.hints[insertion_index].coord >= first.coord { - break; - } - insertion_index += 1; - } - if insertion_index < self.len { - let current = &self.hints[insertion_index]; - if (current.coord == first.coord) - || (is_pair && current.coord <= second.coord) - || current.is_pair_top() - { - return; - } - } - if !first.is_locked() { - if let Some(initial) = initial { - if is_pair { - let mid = initial.apply(scale, first.coord + half(second.coord - first.coord)); - let half = half(second.coord - first.coord) * scale; - first.ds_coord = mid - half; - second.ds_coord = mid + half; - } else { - first.ds_coord = initial.apply(scale, first.coord); - } - } - } - if insertion_index > 0 && first.ds_coord < self.hints[insertion_index - 1].ds_coord { - return; - } - if insertion_index < self.len - && ((is_pair && second.ds_coord > self.hints[insertion_index].ds_coord) - || first.ds_coord > self.hints[insertion_index].ds_coord) - { - return; - } - if insertion_index != self.len { - let mut src_index = self.len - 1; - let mut dst_index = self.len + count - 1; - loop { - self.hints[dst_index] = self.hints[src_index]; - if src_index == insertion_index { - break; - } - src_index -= 1; - dst_index -= 1; - } - } - self.hints[insertion_index] = first; - if is_pair { - self.hints[insertion_index + 1] = second; - } - if TRACE { - // println!( - // " Inserting hint at {} ({})", - // first.coord.to_f64(), - // first.ds_coord.to_f64() - // ); - // if is_pair { - // println!( - // " Inserting hint at {} ({})", - // second.coord.to_f64(), - // second.ds_coord.to_f64() - // ); - // } - } - self.len += count; - } - - fn adjust(&mut self) { - let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; - let mut saved_count = 0usize; - let mut i = 0; - let limit = self.len; - while i < limit { - let is_pair = self.hints[i].is_pair(); - let j = if is_pair { i + 1 } else { i }; - if !self.hints[i].is_locked() { - let frac_down = self.hints[i].ds_coord.fract(); - let frac_up = self.hints[j].ds_coord.fract(); - let down_move_down = Fixed::ZERO - frac_down; - let up_move_down = Fixed::ZERO - frac_up; - let down_move_up = if frac_down == Fixed::ZERO { - Fixed::ZERO - } else { - Fixed::ONE - frac_down - }; - let up_move_up = if frac_up == Fixed::ZERO { - Fixed::ZERO - } else { - Fixed::ONE - frac_up - }; - let move_up = down_move_up.min(up_move_up); - let move_down = down_move_down.max(up_move_down); - const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); - let mut save_edge = false; - let adjustment; - if j >= self.len - 1 - || self.hints[j + 1].ds_coord - >= (self.hints[j].ds_coord + move_up + MIN_COUNTER) - { - if i == 0 - || self.hints[i - 1].ds_coord - <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) - { - adjustment = if -move_down < move_up { - move_down - } else { - move_up - }; - } else { - adjustment = move_up; - } - } else if i == 0 - || self.hints[i - 1].ds_coord - <= (self.hints[i].ds_coord + move_down - MIN_COUNTER) - { - adjustment = move_down; - save_edge = move_up < -move_down; - } else { - adjustment = Fixed::ZERO; - save_edge = true; - } - if save_edge && j < self.len - 1 && !self.hints[j + 1].is_locked() { - saved[saved_count] = (j, move_up - adjustment); - saved_count += 1; - } - self.hints[i].ds_coord += adjustment; - if is_pair { - self.hints[j].ds_coord += adjustment; - } - } - if i > 0 && self.hints[i].coord != self.hints[i - 1].coord { - let a = self.hints[i]; - let b = self.hints[i - 1]; - self.hints[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); - } - if is_pair { - if self.hints[j].coord != self.hints[j - 1].coord { - let a = self.hints[j]; - let b = self.hints[j - 1]; - self.hints[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.coord - b.coord); - } - i += 1; - } - i += 1; - } - for i in (0..saved_count).rev() { - let (j, adjustment) = saved[i]; - if self.hints[j + 1].ds_coord - >= (self.hints[j].ds_coord + adjustment + Fixed::from_f64(0.5)) - { - self.hints[j].ds_coord += adjustment; - if self.hints[j].is_pair() { - self.hints[j - 1].ds_coord += adjustment; - } - } - } - } - - fn build( - &mut self, - state: &HintState, - mask: Option, - mut initial_map: Option<&mut HintMap>, - stems: &mut [StemHint], - origin: Fixed, - initial: bool, - ) { - let scale = state.scale; - let darken_y = Fixed::ZERO; - if !initial { - if let Some(ref mut initial_map) = initial_map { - if !initial_map.valid { - initial_map.build(state, None, None, stems, origin, true); - } - } - } - let initial_map = initial_map.map(|x| x as &HintMap); - self.clear(); - let mut mask = mask.unwrap_or_else(HintMask::all); - if !mask.valid { - mask = HintMask::all(); - } - if state.do_em_box_hints { - let mut bottom = Hint::default(); - bottom.coord = ICF_BOTTOM - EPSILON; - bottom.ds_coord = (bottom.coord * scale).round() - Fixed::from_f64(0.5); - bottom.scale = scale; - bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; - let mut top = Hint::default(); - top.coord = ICF_TOP + EPSILON + twice(state.darken_y); - top.ds_coord = (top.coord * scale).round() + Fixed::from_f64(0.5); - top.scale = scale; - top.flags = GHOST_TOP | LOCKED | SYNTHETIC; - let invalid = Hint::default(); - self.insert(&bottom, &invalid, scale, initial_map); - self.insert(&invalid, &top, scale, initial_map); - } - let mut tmp_mask = mask; - for (i, stem) in stems.iter().enumerate() { - if !tmp_mask.get(i) { - continue; - } - let mut bottom = Hint::default(); - let mut top = Hint::default(); - bottom.setup(stem, i as u8, origin, scale, darken_y, true); - top.setup(stem, i as u8, origin, scale, darken_y, false); - if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { - if initial { - self.insert(&bottom, &top, scale, None); - } else { - self.insert(&bottom, &top, scale, initial_map); - } - tmp_mask.clear(i); - } - } - if initial { - if self.len == 0 - || self.hints[0].coord > Fixed::ZERO - || self.hints[self.len - 1].coord < Fixed::ZERO - { - let edge = Hint { - flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, - scale, - ..Default::default() - }; - let invalid = Hint::default(); - self.insert(&edge, &invalid, scale, None); - } - } else { - for (i, stem) in stems.iter().enumerate() { - if !tmp_mask.get(i) { - continue; - } - let mut bottom = Hint::default(); - let mut top = Hint::default(); - bottom.setup(stem, i as u8, origin, scale, darken_y, true); - top.setup(stem, i as u8, origin, scale, darken_y, false); - self.insert(&bottom, &top, scale, initial_map); - } - } - self.dump(); - self.adjust(); - self.dump(); - if !initial { - for i in 0..self.len { - let hint = &self.hints[i]; - if hint.is_synthetic() { - continue; - } - let stem = &mut stems[hint.index as usize]; - if hint.is_top() { - stem.ds_max = hint.ds_coord; - } else { - stem.ds_min = hint.ds_coord; - } - stem.used = true; - } - self.valid = true; - } - self.valid = true; - } - - fn dump(&self) { - // if !TRACE { - // return; - // } - // for i in 0..self.len { - // let hint = self.hints[i]; - // println!( - // "[{}] {} {} {} {}{}{}{}{}{}", - // hint.index, - // hint.coord.to_f64(), - // hint.ds_coord.to_f64() / hint.scale.to_f64(), - // hint.scale.to_f64() * 65536., - // if hint.is_pair() { "p" } else { "" }, - // if hint.flags & (GHOST_BOTTOM | GHOST_TOP) != 0 { - // "g" - // } else { - // "" - // }, - // if hint.is_top() { "t" } else { "" }, - // if hint.is_bottom() { "b" } else { "" }, - // if hint.is_locked() { "L" } else { "" }, - // if hint.is_synthetic() { "S" } else { "" }, - // ); - // } - // println!("-------------------------------"); - } -} - -#[derive(Copy, Clone, PartialEq, Default)] -pub struct HintMask { - mask: [u8; HINT_MASK_SIZE], - valid: bool, -} - -impl HintMask { - pub fn new() -> Self { - Self { - mask: [0u8; HINT_MASK_SIZE], - valid: false, - } - } - - pub fn all() -> Self { - Self { - mask: [0xFF; HINT_MASK_SIZE], - valid: true, - } - } - - fn clear_all(&mut self) { - self.mask = [0u8; HINT_MASK_SIZE]; - self.valid = true; - } - - pub fn set_mask(&mut self, mask: &[u8]) { - self.clear_all(); - if mask.len() > HINT_MASK_SIZE { - return; - } - for (i, b) in mask.iter().enumerate() { - self.mask[i] = *b; - } - self.valid = true; - } - - #[inline] - #[allow(dead_code)] - pub fn set(&mut self, bit: usize) { - self.mask[bit >> 3] |= 1 << (7 - (bit & 0x7)); - } - - #[inline] - pub fn clear(&mut self, bit: usize) { - self.mask[bit >> 3] &= !(1 << (7 - (bit & 0x7))); - } - - #[inline] - pub fn get(&self, bit: usize) -> bool { - self.mask[bit >> 3] & (1 << (7 - (bit & 0x7))) != 0 - } - - // pub fn dump(&self) { - // for i in 0..MAX_STEM_HINTS { - // print!("{}", self.get(i) as u8); - // } - // println!(); - // for b in &self.mask { - // print!("{:#8b}", *b) - // } - // println!(); - // } -} - -fn half(value: Fixed) -> Fixed { - Fixed::from_bits(value.to_bits() / 2) -} - -fn twice(value: Fixed) -> Fixed { - Fixed::from_bits(value.to_bits().wrapping_mul(2)) -} diff --git a/src/scale/cff2/mod.rs b/src/scale/cff2/mod.rs deleted file mode 100644 index 11baef0..0000000 --- a/src/scale/cff2/mod.rs +++ /dev/null @@ -1,132 +0,0 @@ -mod hint; -mod scaler; - -pub use scaler::{Scaler, ScalerSubfont}; - -use read_fonts::types::{F2Dot14, GlyphId, Pen}; - -use super::Outline; - -pub struct SubfontCache { - entries: Vec, - max_entries: usize, - epoch: u64, -} - -impl SubfontCache { - pub fn new(max_entries: usize) -> Self { - Self { - entries: Vec::new(), - max_entries, - epoch: 0, - } - } - - pub fn scale( - &mut self, - scaler: &Scaler, - id: u64, - glyph_id: u16, - size: f32, - coords: &[i16], - hint: bool, - outline: &mut Outline, - ) -> Option<()> { - let epoch = self.epoch; - let gid = GlyphId::new(glyph_id); - let subfont_index = scaler.subfont_index(gid); - let (found, entry_index) = self.find_entry(id, subfont_index, coords, size); - let (subfont, coords) = if found { - let entry = &mut self.entries[entry_index]; - entry.epoch = epoch; - (&entry.subfont, &entry.coords) - } else { - self.epoch += 1; - let epoch = self.epoch; - if entry_index == self.entries.len() { - let coords: Vec = coords.iter().map(|x| F2Dot14::from_bits(*x)).collect(); - let subfont = scaler.subfont(subfont_index, size, &coords).ok()?; - self.entries.push(Entry { - id, - epoch, - subfont, - coords, - }); - let entry = &self.entries[entry_index]; - (&entry.subfont, &entry.coords) - } else { - let entry = &mut self.entries[entry_index]; - entry.id = u64::MAX; - entry.epoch = epoch; - entry.coords.clear(); - entry - .coords - .extend(coords.iter().map(|x| F2Dot14::from_bits(*x))); - entry.subfont = scaler.subfont(subfont_index, size, &entry.coords).ok()?; - entry.id = id; - (&entry.subfont, &entry.coords) - } - }; - scaler - .outline(subfont, gid, coords, hint, &mut OutlineBuilder(outline)) - .ok()?; - Some(()) - } - - fn find_entry(&self, id: u64, index: u32, coords: &[i16], size: f32) -> (bool, usize) { - let mut lowest_epoch = self.epoch; - let mut lowest_index = 0; - for (i, entry) in self.entries.iter().enumerate() { - if entry.id == id - && entry.subfont.index() == index - && entry.subfont.size() == size - && coords - .iter() - .map(|x| F2Dot14::from_bits(*x)) - .eq(entry.coords.iter().copied()) - { - return (true, i); - } - if entry.epoch < lowest_epoch { - lowest_epoch = entry.epoch; - lowest_index = i; - } - } - if self.entries.len() < self.max_entries { - lowest_index = self.entries.len(); - } - (false, lowest_index) - } -} - -struct Entry { - epoch: u64, - id: u64, - subfont: ScalerSubfont, - coords: Vec, -} - -struct OutlineBuilder<'a>(&'a mut Outline); - -impl Pen for OutlineBuilder<'_> { - fn move_to(&mut self, x: f32, y: f32) { - self.0.move_to((x, y).into()); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.0.line_to((x, y).into()); - } - - fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { - self.0.quad_to((cx0, cy0).into(), (x, y).into()); - } - - fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { - self.0 - .curve_to((cx0, cy0).into(), (cx1, cy1).into(), (x, y).into()); - } - - fn close(&mut self) { - self.0.close(); - } -} diff --git a/src/scale/cff2/scaler.rs b/src/scale/cff2/scaler.rs deleted file mode 100644 index 099be3d..0000000 --- a/src/scale/cff2/scaler.rs +++ /dev/null @@ -1,588 +0,0 @@ -//! Scaler for CFF outlines. - -use std::ops::Range; - -use read_fonts::{ - tables::{ - cff::Cff, - cff2::Cff2, - postscript::{ - charstring::{self, CommandSink}, - dict, BlendState, Error, FdSelect, Index, - }, - variations::ItemVariationStore, - }, - types::{F2Dot14, Fixed, GlyphId, Pen}, - FontData, FontRead, ReadError, TableProvider, -}; - -use super::hint::{HintParams, HintState}; - -/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. -/// -/// The skrifa crate provides a higher level interface for this that handles -/// caching and abstracting over the different outline formats. Consider using -/// that if detailed control over resources is not required. -/// -/// # Subfonts -/// -/// CFF tables can contain multiple logical "subfonts" which determine the -/// state required for processing some subset of glyphs. This state is -/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28) -/// operators to select an appropriate subfont for any given glyph identifier. -/// This process is exposed on this type with the -/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont -/// index for the requested glyph followed by using the -/// [`subfont`](Self::subfont) method to create an appropriately configured -/// subfont for that glyph. -/// -/// # Example -/// -/// ``` -/// # use read_fonts::{tables::postscript::*, types::*, FontRef}; -/// # fn example(font: FontRef, coords: &[F2Dot14], pen: &mut impl Pen) -> Result<(), Error> { -/// let scaler = Scaler::new(&font)?; -/// let glyph_id = GlyphId::new(24); -/// // Retrieve the subfont index for the requested glyph. -/// let subfont_index = scaler.subfont_index(glyph_id); -/// // Construct a subfont with the given configuration. -/// let size = 16.0; -/// let coords = &[]; -/// let subfont = scaler.subfont(subfont_index, size, coords)?; -/// // Scale the outline using our configured subfont and emit the -/// // result to the given pen. -/// let hint = false; -/// scaler.outline(&subfont, glyph_id, coords, hint, pen)?; -/// # Ok(()) -/// # } -/// ``` -pub struct Scaler<'a> { - version: Version<'a>, - top_dict: ScalerTopDict<'a>, - units_per_em: u16, -} - -impl<'a> Scaler<'a> { - /// Creates a new scaler for the given font. - /// - /// This will choose an underyling CFF2 or CFF table from the font, in that - /// order. - pub fn new(font: &impl TableProvider<'a>) -> Result { - let units_per_em = font.head()?.units_per_em(); - if let Ok(cff2) = font.cff2() { - Self::from_cff2(cff2, units_per_em) - } else { - // "The Name INDEX in the CFF data must contain only one entry; - // that is, there must be only one font in the CFF FontSet" - // So we always pass 0 for Top DICT index when reading from an - // OpenType font. - // - Self::from_cff(font.cff()?, 0, units_per_em) - } - } - - pub fn from_cff( - cff1: Cff<'a>, - top_dict_index: usize, - units_per_em: u16, - ) -> Result { - let top_dict_data = cff1.top_dicts().get(top_dict_index)?; - let top_dict = ScalerTopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; - Ok(Self { - version: Version::Version1(cff1), - top_dict, - units_per_em, - }) - } - - pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result { - let table_data = cff2.offset_data().as_bytes(); - let top_dict = ScalerTopDict::new(table_data, cff2.top_dict_data(), true)?; - Ok(Self { - version: Version::Version2(cff2), - top_dict, - units_per_em, - }) - } - - pub fn is_cff2(&self) -> bool { - matches!(self.version, Version::Version2(_)) - } - - /// Returns the number of available subfonts. - pub fn subfont_count(&self) -> u32 { - self.top_dict - .font_dicts - .as_ref() - .map(|font_dicts| font_dicts.count()) - // All CFF fonts have at least one logical subfont. - .unwrap_or(1) - } - - /// Returns the subfont (or Font DICT) index for the given glyph - /// identifier. - pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 { - // For CFF tables, an FDSelect index will be present for CID-keyed - // fonts. Otherwise, the Top DICT will contain an entry for the - // "global" Private DICT. - // See - // - // CFF2 tables always contain a Font DICT and an FDSelect is only - // present if the size of the DICT is greater than 1. - // See - // - // In both cases, we return a subfont index of 0 when FDSelect is missing. - self.top_dict - .fd_select - .as_ref() - .and_then(|select| select.font_index(glyph_id)) - .unwrap_or(0) as u32 - } - - /// Creates a new subfont for the given index, size, normalized - /// variation coordinates and hinting state. - /// - /// The index of a subfont for a particular glyph can be retrieved with - /// the [`subfont_index`](Self::subfont_index) method. - pub fn subfont( - &self, - index: u32, - size: f32, - coords: &[F2Dot14], - ) -> Result { - let private_dict_range = self.private_dict_range(index)?; - let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?; - let mut hint_params = HintParams::default(); - let mut subrs_offset = None; - let mut store_index = 0; - let blend_state = self - .top_dict - .var_store - .clone() - .map(|store| BlendState::new(store, coords, store_index)) - .transpose()?; - for entry in dict::entries(private_dict_data, blend_state) { - use dict::Entry::*; - match entry? { - BlueValues(values) => hint_params.blues = values, - FamilyBlues(values) => hint_params.family_blues = values, - OtherBlues(values) => hint_params.other_blues = values, - FamilyOtherBlues(values) => hint_params.family_blues = values, - BlueScale(value) => hint_params.blue_scale = value, - BlueShift(value) => hint_params.blue_shift = value, - BlueFuzz(value) => hint_params.blue_fuzz = value, - LanguageGroup(group) => hint_params.language_group = group, - // Subrs offset is relative to the private DICT - SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset), - VariationStoreIndex(index) => store_index = index, - _ => {} - } - } - let scale = if size <= 0.0 { - Fixed::ONE - } else { - // Note: we do an intermediate scale to 26.6 to ensure we - // match FreeType - Fixed::from_bits((size * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32) - }; - let hint_state = HintState::new(&hint_params, scale); - Ok(ScalerSubfont { - is_cff2: self.is_cff2(), - index, - size, - scale, - subrs_offset, - hint_state, - store_index, - }) - } - - /// Loads and scales an outline for the given subfont instance, glyph - /// identifier and normalized variation coordinates. - /// - /// Before calling this method, use [`subfont_index`](Self::subfont_index) - /// to retrieve the subfont index for the desired glyph and then - /// [`subfont`](Self::subfont) to create an instance of the subfont for a - /// particular size and location in variation space. - /// Creating subfont instances is not free, so this process is exposed in - /// discrete steps to allow for caching. - /// - /// The result is emitted to the specified pen. - pub fn outline( - &self, - subfont: &ScalerSubfont, - glyph_id: GlyphId, - coords: &[F2Dot14], - hint: bool, - pen: &mut impl Pen, - ) -> Result<(), Error> { - let charstring_data = self - .top_dict - .charstrings - .as_ref() - .ok_or(Error::Read(ReadError::MalformedData( - "missing charstrings INDEX in CFF table", - )))? - .get(glyph_id.to_u16() as usize)?; - let subrs = subfont.subrs(self)?; - let blend_state = subfont.blend_state(self, coords)?; - let mut pen_sink = charstring::PenSink::new(pen); - let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink); - if hint { - let mut scaling_adapter = ScalingSink26Dot6::new(&mut simplifying_adapter, Fixed::ONE); - let mut hinting_adapter = - super::hint::Hinter::new(&subfont.hint_state, &mut scaling_adapter); - charstring::evaluate( - charstring_data, - self.global_subrs(), - subrs, - blend_state, - &mut hinting_adapter, - )?; - } else { - let mut scaling_adapter = - ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale); - charstring::evaluate( - charstring_data, - self.global_subrs(), - subrs, - blend_state, - &mut scaling_adapter, - )?; - } - simplifying_adapter.finish(); - Ok(()) - } - - fn offset_data(&self) -> FontData<'a> { - match &self.version { - Version::Version1(cff1) => cff1.offset_data(), - Version::Version2(cff2) => cff2.offset_data(), - } - } - - fn global_subrs(&self) -> Index<'a> { - match &self.version { - Version::Version1(cff1) => cff1.global_subrs().into(), - Version::Version2(cff2) => cff2.global_subrs().into(), - } - } - - fn private_dict_range(&self, subfont_index: u32) -> Result, Error> { - if let Some(font_dicts) = &self.top_dict.font_dicts { - // If we have a font dict array, extract the private dict range - // from the font dict at the given index. - let font_dict_data = font_dicts.get(subfont_index as usize)?; - let mut range = None; - for entry in dict::entries(font_dict_data, None) { - if let dict::Entry::PrivateDictRange(r) = entry? { - range = Some(r); - break; - } - } - range - } else { - // Last chance, use the private dict range from the top dict if - // available. - self.top_dict.private_dict_range.clone() - } - .ok_or(Error::Read(ReadError::MalformedData( - "missing Private DICT in CFF table", - ))) - } -} - -enum Version<'a> { - /// - Version1(Cff<'a>), - /// - Version2(Cff2<'a>), -} - -/// Specifies local subroutines and hinting parameters for some subset of -/// glyphs in a CFF or CFF2 table. -/// -/// This type is designed to be cacheable to avoid re-evaluating the private -/// dict every time a charstring is processed. -/// -/// For variable fonts, this is dependent on a location in variation space. -#[derive(Clone)] -pub struct ScalerSubfont { - is_cff2: bool, - index: u32, - size: f32, - scale: Fixed, - subrs_offset: Option, - hint_state: HintState, - store_index: u16, -} - -impl ScalerSubfont { - pub fn index(&self) -> u32 { - self.index - } - - pub fn size(&self) -> f32 { - self.size - } - - /// Returns the local subroutine index. - pub fn subrs<'a>(&self, scaler: &Scaler<'a>) -> Result>, Error> { - if let Some(subrs_offset) = self.subrs_offset { - let offset_data = scaler.offset_data().as_bytes(); - let index_data = offset_data.get(subrs_offset..).unwrap_or_default(); - Ok(Some(Index::new(index_data, self.is_cff2)?)) - } else { - Ok(None) - } - } - - /// Creates a new blend state for the given normalized variation - /// coordinates. - pub fn blend_state<'a>( - &self, - scaler: &Scaler<'a>, - coords: &'a [F2Dot14], - ) -> Result>, Error> { - if let Some(var_store) = scaler.top_dict.var_store.clone() { - Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) - } else { - Ok(None) - } - } -} - -/// Entries that we parse from the Top DICT that are required to support -/// charstring evaluation. -#[derive(Default)] -struct ScalerTopDict<'a> { - charstrings: Option>, - font_dicts: Option>, - fd_select: Option>, - private_dict_range: Option>, - var_store: Option>, -} - -impl<'a> ScalerTopDict<'a> { - fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result { - let mut items = ScalerTopDict::default(); - for entry in dict::entries(top_dict_data, None) { - match entry? { - dict::Entry::CharstringsOffset(offset) => { - items.charstrings = Some(Index::new( - table_data.get(offset..).unwrap_or_default(), - is_cff2, - )?); - } - dict::Entry::FdArrayOffset(offset) => { - items.font_dicts = Some(Index::new( - table_data.get(offset..).unwrap_or_default(), - is_cff2, - )?); - } - dict::Entry::FdSelectOffset(offset) => { - items.fd_select = Some(FdSelect::read(FontData::new( - table_data.get(offset..).unwrap_or_default(), - ))?); - } - dict::Entry::PrivateDictRange(range) => { - items.private_dict_range = Some(range); - } - dict::Entry::VariationStoreOffset(offset) if is_cff2 => { - items.var_store = Some(ItemVariationStore::read(FontData::new( - // IVS is preceded by a 2 byte length - table_data.get(offset + 2..).unwrap_or_default(), - ))?); - } - _ => {} - } - } - Ok(items) - } -} - -/// Command sink adapter that applies a scaling factor. -/// -/// This assumes a 26.6 scaling factor packed into a Fixed and thus, -/// this is not public and exists only to match FreeType's exact -/// scaling process. -struct ScalingSink26Dot6<'a, S> { - inner: &'a mut S, - scale: Fixed, -} - -impl<'a, S> ScalingSink26Dot6<'a, S> { - fn new(sink: &'a mut S, scale: Fixed) -> Self { - Self { scale, inner: sink } - } - - fn scale(&self, coord: Fixed) -> Fixed { - if self.scale != Fixed::ONE { - // The following dance is necessary to exactly match FreeType's - // application of scaling factors. This seems to be the result - // of merging the contributed Adobe code while not breaking the - // FreeType public API. - // 1. Multiply by 1/64 - // - let a = coord * Fixed::from_bits(0x0400); - // 2. Convert to 26.6 by truncation - // - let b = Fixed::from_bits(a.to_bits() >> 10); - // 3. Multiply by the original scale factor - // - let c = b * self.scale; - // Finally, we convert back to 16.16 - Fixed::from_bits(c.to_bits() << 10) - } else { - // Otherwise, simply zero the low 10 bits - Fixed::from_bits(coord.to_bits() & !0x3FF) - } - } -} - -impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> { - fn hstem(&mut self, y: Fixed, dy: Fixed) { - self.inner.hstem(y, dy); - } - - fn vstem(&mut self, x: Fixed, dx: Fixed) { - self.inner.vstem(x, dx); - } - - fn hint_mask(&mut self, mask: &[u8]) { - self.inner.hint_mask(mask); - } - - fn counter_mask(&mut self, mask: &[u8]) { - self.inner.counter_mask(mask); - } - - fn move_to(&mut self, x: Fixed, y: Fixed) { - self.inner.move_to(self.scale(x), self.scale(y)); - } - - fn line_to(&mut self, x: Fixed, y: Fixed) { - self.inner.line_to(self.scale(x), self.scale(y)); - } - - fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { - self.inner.curve_to( - self.scale(cx1), - self.scale(cy1), - self.scale(cx2), - self.scale(cy2), - self.scale(x), - self.scale(y), - ); - } - - fn close(&mut self) { - self.inner.close(); - } -} - -/// Command sink adapter that supresses degenerate move and line commands. -/// -/// FreeType avoids emitting empty contours and zero length lines to prevent -/// artifacts when stem darkening is enabled. We don't support stem darkening -/// because it's not enabled by any of our clients but we remove the degenerate -/// elements regardless to match the output. -/// -/// See -struct NopFilteringSink<'a, S> { - start: Option<(Fixed, Fixed)>, - last: Option<(Fixed, Fixed)>, - pending_move: Option<(Fixed, Fixed)>, - inner: &'a mut S, -} - -impl<'a, S> NopFilteringSink<'a, S> -where - S: CommandSink, -{ - fn new(inner: &'a mut S) -> Self { - Self { - start: None, - last: None, - pending_move: None, - inner, - } - } - - fn flush_pending_move(&mut self) { - if let Some((x, y)) = self.pending_move.take() { - if let Some((last_x, last_y)) = self.start { - if self.last != self.start { - self.inner.line_to(last_x, last_y); - } - } - self.start = Some((x, y)); - self.last = None; - self.inner.move_to(x, y); - } - } - - pub fn finish(&mut self) { - match self.start { - Some((x, y)) if self.last != self.start => { - self.inner.line_to(x, y); - } - _ => {} - } - } -} - -impl<'a, S> CommandSink for NopFilteringSink<'a, S> -where - S: CommandSink, -{ - fn hstem(&mut self, y: Fixed, dy: Fixed) { - self.inner.hstem(y, dy); - } - - fn vstem(&mut self, x: Fixed, dx: Fixed) { - self.inner.vstem(x, dx); - } - - fn hint_mask(&mut self, mask: &[u8]) { - self.inner.hint_mask(mask); - } - - fn counter_mask(&mut self, mask: &[u8]) { - self.inner.counter_mask(mask); - } - - fn move_to(&mut self, x: Fixed, y: Fixed) { - self.pending_move = Some((x, y)); - } - - fn line_to(&mut self, x: Fixed, y: Fixed) { - if self.pending_move == Some((x, y)) { - return; - } - self.flush_pending_move(); - if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) { - return; - } - self.inner.line_to(x, y); - self.last = Some((x, y)); - } - - fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { - self.flush_pending_move(); - self.last = Some((x, y)); - self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) { - if self.pending_move.is_none() { - if let Some((start_x, start_y)) = self.start { - if self.start != self.last { - self.inner.line_to(start_x, start_y); - } - } - self.start = None; - self.last = None; - } - } -} diff --git a/src/scale/cff3/hint.rs b/src/scale/cff3/hint.rs deleted file mode 100644 index 84aeae9..0000000 --- a/src/scale/cff3/hint.rs +++ /dev/null @@ -1,1479 +0,0 @@ -//! CFF hinting. - -use read_fonts::{ - tables::postscript::{charstring::CommandSink, dict::Blues}, - types::Fixed, -}; - -// "Default values for OS/2 typoAscender/Descender.." -// See -const ICF_TOP: Fixed = Fixed::from_i32(880); -const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); - -// -const MAX_BLUES: usize = 7; -const MAX_OTHER_BLUES: usize = 5; -const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES; - -// -const MAX_HINTS: usize = 96; - -// One bit per stem hint -// -const HINT_MASK_SIZE: usize = (MAX_HINTS + 7) / 8; - -// Constant for hint adjustment and em box hint placement. -// -const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); - -// -const EPSILON: Fixed = Fixed::from_bits(1); - -/// Parameters used to generate the stem and counter zones for the hinting -/// algorithm. -#[derive(Clone)] -pub(crate) struct HintParams { - pub blues: Blues, - pub family_blues: Blues, - pub other_blues: Blues, - pub family_other_blues: Blues, - pub blue_scale: Fixed, - pub blue_shift: Fixed, - pub blue_fuzz: Fixed, - pub language_group: i32, -} - -impl Default for HintParams { - fn default() -> Self { - Self { - blues: Blues::default(), - other_blues: Blues::default(), - family_blues: Blues::default(), - family_other_blues: Blues::default(), - // See - blue_scale: Fixed::from_f64(0.039625), - blue_shift: Fixed::from_i32(7), - blue_fuzz: Fixed::ONE, - language_group: 0, - } - } -} - -/// See -#[derive(Copy, Clone, PartialEq, Default, Debug)] -struct BlueZone { - is_bottom: bool, - cs_bottom_edge: Fixed, - cs_top_edge: Fixed, - cs_flat_edge: Fixed, - ds_flat_edge: Fixed, -} - -/// Hinting state for a PostScript subfont. -/// -/// Note that hinter states depend on the scale, subfont index and -/// variation coordinates of a glyph. They can be retained and reused -/// if those values remain the same. -#[derive(Copy, Clone, PartialEq, Default)] -pub(crate) struct HintState { - scale: Fixed, - blue_scale: Fixed, - blue_shift: Fixed, - blue_fuzz: Fixed, - language_group: i32, - supress_overshoot: bool, - do_em_box_hints: bool, - boost: Fixed, - darken_y: Fixed, - zones: [BlueZone; MAX_BLUE_ZONES], - zone_count: usize, -} - -impl HintState { - pub fn new(params: &HintParams, scale: Fixed) -> Self { - let mut state = Self { - scale, - blue_scale: params.blue_scale, - blue_shift: params.blue_shift, - blue_fuzz: params.blue_fuzz, - language_group: params.language_group, - supress_overshoot: false, - do_em_box_hints: false, - boost: Fixed::ZERO, - darken_y: Fixed::ZERO, - zones: [BlueZone::default(); MAX_BLUE_ZONES], - zone_count: 0, - }; - state.build_zones(params); - state - } - - fn zones(&self) -> &[BlueZone] { - &self.zones[..self.zone_count] - } - - /// Initialize zones from the set of blues values. - /// - /// See - fn build_zones(&mut self, params: &HintParams) { - self.do_em_box_hints = false; - // - match (self.language_group, params.blues.values().len()) { - (1, 2) => { - let blues = params.blues.values(); - if blues[0].0 < ICF_BOTTOM - && blues[0].1 < ICF_BOTTOM - && blues[1].0 > ICF_TOP - && blues[1].1 > ICF_TOP - { - // FreeType generates synthetic hints here. We'll do it - // later when building the hint map. - self.do_em_box_hints = true; - return; - } - } - (1, 0) => { - self.do_em_box_hints = true; - return; - } - _ => {} - } - let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; - let mut max_zone_height = Fixed::ZERO; - let mut zone_ix = 0usize; - // Copy blues and other blues to a combined array of top and bottom zones. - for blue in params.blues.values().iter().take(MAX_BLUES) { - // FreeType loads blues as integers and then expands to 16.16 - // at initialization. We load them as 16.16 so floor them here - // to ensure we match. - // - let bottom = blue.0.floor(); - let top = blue.1.floor(); - let zone_height = top - bottom; - if zone_height < Fixed::ZERO { - // Reject zones with negative height - continue; - } - max_zone_height = max_zone_height.max(zone_height); - let zone = &mut zones[zone_ix]; - zone.cs_bottom_edge = bottom; - zone.cs_top_edge = top; - if zone_ix == 0 { - // First blue value is bottom zone - zone.is_bottom = true; - zone.cs_flat_edge = top; - } else { - // Remaining blue values are top zones - zone.is_bottom = false; - // Adjust both edges of top zone upward by twice darkening amount - zone.cs_top_edge += twice(self.darken_y); - zone.cs_bottom_edge += twice(self.darken_y); - zone.cs_flat_edge = zone.cs_bottom_edge; - } - zone_ix += 1; - } - for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { - let bottom = blue.0.floor(); - let top = blue.1.floor(); - let zone_height = top - bottom; - if zone_height < Fixed::ZERO { - // Reject zones with negative height - continue; - } - max_zone_height = max_zone_height.max(zone_height); - let zone = &mut zones[zone_ix]; - // All "other" blues are bottom zone - zone.is_bottom = true; - zone.cs_bottom_edge = bottom; - zone.cs_top_edge = top; - zone.cs_flat_edge = top; - zone_ix += 1; - } - // Adjust for family blues - let units_per_pixel = Fixed::ONE / self.scale; - for zone in &mut zones[..zone_ix] { - let flat = zone.cs_flat_edge; - let mut min_diff = Fixed::MAX; - if zone.is_bottom { - // In a bottom zone, the top edge is the flat edge. - // Search family other blues for bottom zones. Look for the - // closest edge that is within the one pixel threshold. - for blue in params.family_other_blues.values() { - let family_flat = blue.1; - let diff = (flat - family_flat).abs(); - if diff < min_diff && diff < units_per_pixel { - zone.cs_flat_edge = family_flat; - min_diff = diff; - if diff == Fixed::ZERO { - break; - } - } - } - // Check the first member of family blues, which is a bottom - // zone - if !params.family_blues.values().is_empty() { - let family_flat = params.family_blues.values()[0].1; - let diff = (flat - family_flat).abs(); - if diff < min_diff && diff < units_per_pixel { - zone.cs_flat_edge = family_flat; - } - } - } else { - // In a top zone, the bottom edge is the flat edge. - // Search family blues for top zones, skipping the first, which - // is a bottom zone. Look for closest family edge that is - // within the one pixel threshold. - for blue in params.family_blues.values().iter().skip(1) { - let family_flat = blue.0 + twice(self.darken_y); - let diff = (flat - family_flat).abs(); - if diff < min_diff && diff < units_per_pixel { - zone.cs_flat_edge = family_flat; - min_diff = diff; - if diff == Fixed::ZERO { - break; - } - } - } - } - } - if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { - // Clamp at maximum scale - self.blue_scale = Fixed::ONE / max_zone_height; - } - // Suppress overshoot and boost blue zones at small sizes - if self.scale < self.blue_scale { - self.supress_overshoot = true; - self.boost = - Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); - // boost must remain less than 0.5, or baseline could go negative - self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); - } - if self.darken_y != Fixed::ZERO { - self.boost = Fixed::ZERO; - } - // Set device space alignment for each zone; apply boost amount before - // rounding flat edge - let scale = self.scale; - let boost = self.boost; - for zone in &mut zones[..zone_ix] { - let boost = if zone.is_bottom { -boost } else { boost }; - zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); - } - self.zones = zones; - self.zone_count = zone_ix; - } - - /// Check whether a hint is captured by one of the blue zones. - /// - /// See - fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool { - let fuzz = self.blue_fuzz; - let mut captured = false; - let mut adjustment = Fixed::ZERO; - for zone in self.zones() { - if zone.is_bottom - && bottom_edge.is_bottom() - && (zone.cs_bottom_edge - fuzz) <= bottom_edge.cs_coord - && bottom_edge.cs_coord <= (zone.cs_top_edge + fuzz) - { - // Bottom edge captured by bottom zone. - adjustment = if self.supress_overshoot { - zone.ds_flat_edge - } else if zone.cs_top_edge - bottom_edge.cs_coord >= self.blue_shift { - // Guarantee minimum of 1 pixel overshoot - bottom_edge - .ds_coord - .round() - .min(zone.ds_flat_edge - Fixed::ONE) - } else { - bottom_edge.ds_coord.round() - }; - adjustment -= bottom_edge.ds_coord; - captured = true; - break; - } - if !zone.is_bottom - && top_edge.is_top() - && (zone.cs_bottom_edge - fuzz) <= top_edge.cs_coord - && top_edge.cs_coord <= (zone.cs_top_edge + fuzz) - { - // Top edge captured by top zone. - adjustment = if self.supress_overshoot { - zone.ds_flat_edge - } else if top_edge.cs_coord - zone.cs_bottom_edge >= self.blue_shift { - // Guarantee minimum of 1 pixel overshoot - top_edge - .ds_coord - .round() - .max(zone.ds_flat_edge + Fixed::ONE) - } else { - top_edge.ds_coord.round() - }; - adjustment -= top_edge.ds_coord; - captured = true; - break; - } - } - if captured { - // Move both edges and mark them as "locked" - if bottom_edge.is_valid() { - bottom_edge.ds_coord += adjustment; - bottom_edge.lock(); - } - if top_edge.is_valid() { - top_edge.ds_coord += adjustment; - top_edge.lock(); - } - } - captured - } -} - -/// -#[derive(Copy, Clone, Default)] -struct StemHint { - /// If true, device space position is valid - is_used: bool, - // Character space position - min: Fixed, - max: Fixed, - // Device space position after first use - ds_min: Fixed, - ds_max: Fixed, -} - -// Hint flags -const GHOST_BOTTOM: u8 = 0x1; -const GHOST_TOP: u8 = 0x2; -const PAIR_BOTTOM: u8 = 0x4; -const PAIR_TOP: u8 = 0x8; -const LOCKED: u8 = 0x10; -const SYNTHETIC: u8 = 0x20; - -/// -#[derive(Copy, Clone, PartialEq, Default, Debug)] -struct Hint { - flags: u8, - /// Index in original stem hint array (if not synthetic) - index: u8, - cs_coord: Fixed, - ds_coord: Fixed, - scale: Fixed, -} - -impl Hint { - fn is_valid(&self) -> bool { - self.flags != 0 - } - - fn is_bottom(&self) -> bool { - self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0 - } - - fn is_top(&self) -> bool { - self.flags & (GHOST_TOP | PAIR_TOP) != 0 - } - - fn is_pair(&self) -> bool { - self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0 - } - - fn is_pair_top(&self) -> bool { - self.flags & PAIR_TOP != 0 - } - - fn is_locked(&self) -> bool { - self.flags & LOCKED != 0 - } - - fn is_synthetic(&self) -> bool { - self.flags & SYNTHETIC != 0 - } - - fn lock(&mut self) { - self.flags |= LOCKED - } - - /// Hint initialization from an incoming stem hint. - /// - /// See - fn setup( - &mut self, - stem: &StemHint, - index: u8, - origin: Fixed, - scale: Fixed, - darken_y: Fixed, - is_bottom: bool, - ) { - // "Ghost hints" are used to align a single edge rather than a - // stem-- think the top and bottom edges of an uppercase - // sans-serif I. - // These are encoded internally with stem hints of width -21 - // and -20 for bottom and top hints, respectively. - const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21); - const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20); - let width = stem.max - stem.min; - if width == GHOST_BOTTOM_WIDTH { - if is_bottom { - self.cs_coord = stem.max; - self.flags = GHOST_BOTTOM; - } else { - self.flags = 0; - } - } else if width == GHOST_TOP_WIDTH { - if !is_bottom { - self.cs_coord = stem.min; - self.flags = GHOST_TOP; - } else { - self.flags = 0; - } - } else if width < Fixed::ZERO { - // If width < 0, this is an inverted pair. We follow FreeType and - // swap the coordinates - if is_bottom { - self.cs_coord = stem.max; - self.flags = PAIR_BOTTOM; - } else { - self.cs_coord = stem.min; - self.flags = PAIR_TOP; - } - } else { - // This is a normal pair - if is_bottom { - self.cs_coord = stem.min; - self.flags = PAIR_BOTTOM; - } else { - self.cs_coord = stem.max; - self.flags = PAIR_TOP; - } - } - if self.is_top() { - // For top hints, adjust character space position up by twice the - // darkening amount - self.cs_coord += twice(darken_y); - } - self.cs_coord += origin; - self.scale = scale; - self.index = index; - // If original stem hint was used, copy the position - if self.flags != 0 && stem.is_used { - if self.is_top() { - self.ds_coord = stem.ds_max; - } else { - self.ds_coord = stem.ds_min; - } - self.lock(); - } else { - self.ds_coord = self.cs_coord * scale; - } - } -} - -/// Collection of adjusted hint edges. -/// -/// -#[derive(Copy, Clone)] -struct HintMap { - edges: [Hint; MAX_HINTS], - len: usize, - is_valid: bool, - scale: Fixed, -} - -impl HintMap { - fn new(scale: Fixed) -> Self { - Self { - edges: [Hint::default(); MAX_HINTS], - len: 0, - is_valid: false, - scale, - } - } - - fn clear(&mut self) { - self.len = 0; - self.is_valid = false; - } - - /// Transform character space coordinate to device space. - /// - /// Based on - fn transform(&self, coord: Fixed) -> Fixed { - if self.len == 0 { - return coord * self.scale; - } - let limit = self.len - 1; - let mut i = 0; - while i < limit && coord >= self.edges[i + 1].cs_coord { - i += 1; - } - while i > 0 && coord < self.edges[i].cs_coord { - i -= 1; - } - let first_edge = &self.edges[0]; - if i == 0 && coord < first_edge.cs_coord { - // Special case for points below first edge: use uniform scale - ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord - } else { - // Use highest edge where cs_coord >= edge.cs_coord - let edge = &self.edges[i]; - ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord - } - } - - /// Insert hint edges into map, sorted by character space coordinate. - /// - /// Based on - fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) { - let (is_pair, mut first_edge) = if !bottom.is_valid() { - // Bottom is invalid: insert only top edge - (false, *top) - } else if !top.is_valid() { - // Top is invalid: insert only bottom edge - (false, *bottom) - } else { - // We have a valid pair! - (true, *bottom) - }; - let mut second_edge = *top; - if is_pair && top.cs_coord < bottom.cs_coord { - // Paired edges must be in proper order. FT just ignores the hint. - return; - } - let edge_count = if is_pair { 2 } else { 1 }; - if self.len + edge_count > MAX_HINTS { - // Won't fit. Again, ignore. - return; - } - // Find insertion index that keeps the edge list sorted - let mut insert_ix = 0; - while insert_ix < self.len { - if self.edges[insert_ix].cs_coord >= first_edge.cs_coord { - break; - } - insert_ix += 1; - } - // Discard hints that overlap in character space - if insert_ix < self.len { - let current = &self.edges[insert_ix]; - // Existing edge is the same - if (current.cs_coord == first_edge.cs_coord) - // Pair straddles the next edge - || (is_pair && current.cs_coord <= second_edge.cs_coord) - // Inserting between paired edges - || current.is_pair_top() - { - return; - } - } - // Recompute device space locations using initial hint map - if !first_edge.is_locked() { - if let Some(initial) = initial { - if is_pair { - // Preserve stem width: position center of stem with - // initial hint map and two edges with nominal scale - let mid = initial.transform( - first_edge.cs_coord + half(second_edge.cs_coord - first_edge.cs_coord), - ); - let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale; - first_edge.ds_coord = mid - half_width; - second_edge.ds_coord = mid + half_width; - } else { - first_edge.ds_coord = initial.transform(first_edge.cs_coord); - } - } - } - // Now discard hints that overlap in device space: - if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord { - // Inserting after an existing edge - return; - } - if insert_ix < self.len - && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord) - || first_edge.ds_coord > self.edges[insert_ix].ds_coord) - { - // Inserting before an existing edge - return; - } - // If we're inserting in the middle, make room in the edge array - if insert_ix != self.len { - let mut src_index = self.len - 1; - let mut dst_index = self.len + edge_count - 1; - loop { - self.edges[dst_index] = self.edges[src_index]; - if src_index == insert_ix { - break; - } - src_index -= 1; - dst_index -= 1; - } - } - self.edges[insert_ix] = first_edge; - if is_pair { - self.edges[insert_ix + 1] = second_edge; - } - self.len += edge_count; - } - - /// Adjust hint pairs so that one of the two edges is on a pixel boundary. - /// - /// Based on - fn adjust(&mut self) { - let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; - let mut saved_count = 0usize; - let mut i = 0; - // From FT with adjustments for variable names: - // "First pass is bottom-up (font hint order) without look-ahead. - // Locked edges are already adjusted. - // Unlocked edges begin with ds_coord from `initial_map'. - // Save edges that are not optimally adjusted in `saved' array, - // and process them in second pass." - let limit = self.len; - while i < limit { - let is_pair = self.edges[i].is_pair(); - let j = if is_pair { i + 1 } else { i }; - if !self.edges[i].is_locked() { - // We can adjust hint edges that are not locked - let frac_down = self.edges[i].ds_coord.fract(); - let frac_up = self.edges[j].ds_coord.fract(); - // There are four possibilities. We compute them all. - // (moves down are negative) - let down_move_down = Fixed::ZERO - frac_down; - let up_move_down = Fixed::ZERO - frac_up; - let down_move_up = if frac_down == Fixed::ZERO { - Fixed::ZERO - } else { - Fixed::ONE - frac_down - }; - let up_move_up = if frac_up == Fixed::ZERO { - Fixed::ZERO - } else { - Fixed::ONE - frac_up - }; - // Smallest move up - let move_up = down_move_up.min(up_move_up); - // Smallest move down - let move_down = down_move_down.max(up_move_down); - let mut save_edge = false; - let adjustment; - // Check for room to move up: - // 1. We're at the top of the array, or - // 2. The next edge is at or above the proposed move up - if j >= self.len - 1 - || self.edges[j + 1].ds_coord - >= (self.edges[j].ds_coord + move_up + MIN_COUNTER) - { - // Also check for room to move down... - if i == 0 - || self.edges[i - 1].ds_coord - <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) - { - // .. and move the smallest distance - adjustment = if -move_down < move_up { - move_down - } else { - move_up - }; - } else { - adjustment = move_up; - } - } else if i == 0 - || self.edges[i - 1].ds_coord - <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) - { - // We can move down - adjustment = move_down; - // True if the move is not optimum - save_edge = move_up < -move_down; - } else { - // We can't move either way without overlapping - adjustment = Fixed::ZERO; - save_edge = true; - } - // Capture non-optimal adjustments and save them for a second - // pass. This is only possible if the edge above is unlocked - // and can be moved. - if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() { - // (index, desired adjustment) - saved[saved_count] = (j, move_up - adjustment); - saved_count += 1; - } - // Apply the adjustment - self.edges[i].ds_coord += adjustment; - if is_pair { - self.edges[j].ds_coord += adjustment; - } - } - // Compute the new edge scale - if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord { - let a = self.edges[i]; - let b = self.edges[i - 1]; - self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); - } - if is_pair { - if self.edges[j].cs_coord != self.edges[j - 1].cs_coord { - let a = self.edges[j]; - let b = self.edges[j - 1]; - self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); - } - i += 1; - } - i += 1; - } - // Second pass tries to move non-optimal edges up if the first - // pass created room - for (j, adjustment) in saved[..saved_count].iter().copied().rev() { - if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) { - self.edges[j].ds_coord += adjustment; - if self.edges[j].is_pair() { - self.edges[j - 1].ds_coord += adjustment; - } - } - } - } - - /// Builds a hintmap from hints and mask. - /// - /// If `initial_map` is invalid, this recurses one level to initialize - /// it. If `is_initial` is true, simply build the initial map. - /// - /// Based on - fn build( - &mut self, - state: &HintState, - mask: Option, - mut initial_map: Option<&mut HintMap>, - stems: &mut [StemHint], - origin: Fixed, - is_initial: bool, - ) { - let scale = state.scale; - let darken_y = Fixed::ZERO; - if !is_initial { - if let Some(initial_map) = &mut initial_map { - if !initial_map.is_valid { - // Note: recursive call here to build the initial map if it - // is provided and invalid - initial_map.build(state, Some(HintMask::all()), None, stems, origin, true); - } - } - } - let initial_map = initial_map.map(|x| x as &HintMap); - self.clear(); - // If the mask is missing or invalid, assume all hints are active - let mut mask = mask.unwrap_or_else(HintMask::all); - if !mask.is_valid { - mask = HintMask::all(); - } - if state.do_em_box_hints { - // FreeType generates these during blues initialization. Do - // it here just to avoid carrying the extra state in the - // already large HintState struct. - // - let mut bottom = Hint::default(); - bottom.cs_coord = ICF_BOTTOM - EPSILON; - bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER; - bottom.scale = scale; - bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; - let mut top = Hint::default(); - top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y); - top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER; - top.scale = scale; - top.flags = GHOST_TOP | LOCKED | SYNTHETIC; - let invalid = Hint::default(); - self.insert(&bottom, &invalid, initial_map); - self.insert(&invalid, &top, initial_map); - } - let mut tmp_mask = mask; - // FreeType iterates over the hint mask with some fancy bit logic. We - // do the simpler thing and loop over the stems. - // - for (i, stem) in stems.iter().enumerate() { - if !tmp_mask.get(i) { - continue; - } - let hint_ix = i as u8; - let mut bottom = Hint::default(); - let mut top = Hint::default(); - bottom.setup(stem, hint_ix, origin, scale, darken_y, true); - top.setup(stem, hint_ix, origin, scale, darken_y, false); - // Insert hints that are locked or captured by a blue zone - if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { - if is_initial { - self.insert(&bottom, &top, None); - } else { - self.insert(&bottom, &top, initial_map); - } - // Avoid processing this hint in the second pass - tmp_mask.clear(i); - } - } - if is_initial { - // Heuristic: insert a point at (0, 0) if it's not covered by a - // mapping. Ensures a lock at baseline for glyphs missing a - // baseline hint. - if self.len == 0 - || self.edges[0].cs_coord > Fixed::ZERO - || self.edges[self.len - 1].cs_coord < Fixed::ZERO - { - let edge = Hint { - flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, - scale, - ..Default::default() - }; - let invalid = Hint::default(); - self.insert(&edge, &invalid, None); - } - } else { - // Insert hints that were skipped in the first pass - for (i, stem) in stems.iter().enumerate() { - if !tmp_mask.get(i) { - continue; - } - let hint_ix = i as u8; - let mut bottom = Hint::default(); - let mut top = Hint::default(); - bottom.setup(stem, hint_ix, origin, scale, darken_y, true); - top.setup(stem, hint_ix, origin, scale, darken_y, false); - self.insert(&bottom, &top, initial_map); - } - } - // Adjust edges that are not locked to blue zones - self.adjust(); - if !is_initial { - // Save position of edges that were used by the hint map. - for edge in &self.edges[..self.len] { - if edge.is_synthetic() { - continue; - } - let stem = &mut stems[edge.index as usize]; - if edge.is_top() { - stem.ds_max = edge.ds_coord; - } else { - stem.ds_min = edge.ds_coord; - } - stem.is_used = true; - } - } - self.is_valid = true; - } -} - -/// Bitmask that specifies which hints are currently active. -/// -/// "Each bit of the mask, starting with the most-significant bit of -/// the first byte, represents the corresponding hint zone in the -/// order in which the hints were declared at the beginning of -/// the charstring." -/// -/// See -/// Also -#[derive(Copy, Clone, PartialEq, Default)] -struct HintMask { - mask: [u8; HINT_MASK_SIZE], - is_valid: bool, -} - -impl HintMask { - fn new(bytes: &[u8]) -> Option { - let len = bytes.len(); - if len > HINT_MASK_SIZE { - return None; - } - let mut mask = Self::default(); - mask.mask[..len].copy_from_slice(&bytes[..len]); - mask.is_valid = true; - Some(mask) - } - - fn all() -> Self { - Self { - mask: [0xFF; HINT_MASK_SIZE], - is_valid: true, - } - } - - fn clear(&mut self, bit: usize) { - self.mask[bit >> 3] &= !msb_mask(bit); - } - - fn get(&self, bit: usize) -> bool { - self.mask[bit >> 3] & msb_mask(bit) != 0 - } -} - -/// Returns a bit mask for the selected bit with the -/// most significant bit at index 0. -fn msb_mask(bit: usize) -> u8 { - 1 << (7 - (bit & 0x7)) -} - -pub(super) struct HintingSink<'a, S> { - state: &'a HintState, - sink: &'a mut S, - stem_hints: [StemHint; MAX_HINTS], - stem_count: u8, - mask: HintMask, - initial_map: HintMap, - map: HintMap, - /// Most recent move_to in character space. - start_point: Option<[Fixed; 2]>, - /// Most recent line_to. First two elements are coords in character - /// space and the last two are in device space. - pending_line: Option<[Fixed; 4]>, -} - -impl<'a, S: CommandSink> HintingSink<'a, S> { - pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { - let scale = state.scale; - Self { - state, - sink, - stem_hints: [StemHint::default(); MAX_HINTS], - stem_count: 0, - mask: HintMask::all(), - initial_map: HintMap::new(scale), - map: HintMap::new(scale), - start_point: None, - pending_line: None, - } - } - - pub fn finish(&mut self) { - self.maybe_close_subpath(); - } - - fn maybe_close_subpath(&mut self) { - // This requires some explanation. The hint mask can be modified - // during charstring evaluation which changes the set of hints that - // are applied. FreeType ensures that the closing line for any subpath - // is transformed with the same hint map as the starting point for the - // subpath. This is done by stashing a copy of the hint map that is - // active when a new subpath is started. Unlike FreeType, we make use - // of close elements, so we can cheat a bit here and avoid the - // extra hintmap. If we're closing an open subpath and have a pending - // line and the line is not equal to the start point in character - // space, then we emit the saved device space coordinates for the - // line. If the coordinates do match in character space, we omit - // that line. The unconditional close command ensures that the - // start and end points coincide. - // Note: this doesn't apply to subpaths that end in cubics. - match (self.start_point.take(), self.pending_line.take()) { - (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => { - if start != [cs_x, cs_y] { - self.sink.line_to(ds_x, ds_y); - } - self.sink.close(); - } - (Some(_), _) => self.sink.close(), - _ => {} - } - } - - fn flush_pending_line(&mut self) { - if let Some([_, _, x, y]) = self.pending_line.take() { - self.sink.line_to(x, y); - } - } - - fn hint(&mut self, coord: Fixed) -> Fixed { - if !self.map.is_valid { - self.build_hint_map(Some(self.mask), Fixed::ZERO); - } - trunc(self.map.transform(coord)) - } - - fn scale(&self, coord: Fixed) -> Fixed { - trunc(coord * self.state.scale) - } - - fn add_stem(&mut self, min: Fixed, max: Fixed) { - let index = self.stem_count as usize; - if index >= MAX_HINTS || self.map.is_valid { - return; - } - let stem = &mut self.stem_hints[index]; - stem.min = min; - stem.max = max; - stem.is_used = false; - stem.ds_min = Fixed::ZERO; - stem.ds_max = Fixed::ZERO; - self.stem_count = index as u8 + 1; - } - - fn build_hint_map(&mut self, mask: Option, origin: Fixed) { - self.map.build( - self.state, - mask, - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - origin, - false, - ); - } -} - -impl<'a, S: CommandSink> CommandSink for HintingSink<'a, S> { - fn hstem(&mut self, min: Fixed, max: Fixed) { - self.add_stem(min, max); - } - - fn hint_mask(&mut self, mask: &[u8]) { - // For invalid hint masks, FreeType assumes all hints are active. - // See - let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); - if mask != self.mask { - self.mask = mask; - self.map.is_valid = false; - } - } - - fn counter_mask(&mut self, mask: &[u8]) { - // For counter masks, we build a temporary hint map "just to - // place and lock those stems participating in the counter - // mask." Building the map modifies the stem hint array as a - // side effect. - // See - let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); - let mut map = HintMap::new(self.state.scale); - map.build( - self.state, - Some(mask), - Some(&mut self.initial_map), - &mut self.stem_hints[..self.stem_count as usize], - Fixed::ZERO, - false, - ); - } - - fn move_to(&mut self, x: Fixed, y: Fixed) { - self.maybe_close_subpath(); - self.start_point = Some([x, y]); - let x = self.scale(x); - let y = self.hint(y); - self.sink.move_to(x, y); - } - - fn line_to(&mut self, x: Fixed, y: Fixed) { - self.flush_pending_line(); - let ds_x = self.scale(x); - let ds_y = self.hint(y); - self.pending_line = Some([x, y, ds_x, ds_y]); - } - - fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { - self.flush_pending_line(); - let cx1 = self.scale(cx1); - let cy1 = self.hint(cy1); - let cx2 = self.scale(cx2); - let cy2 = self.hint(cy2); - let x = self.scale(x); - let y = self.hint(y); - self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); - } - - fn close(&mut self) { - // We emit close commands based on the sequence of moves. - // See `maybe_close_subpath` - } -} - -/// FreeType converts from 16.16 to 26.6 by truncation. We keep our -/// values in 16.16 so simply zero the low 10 bits to match the -/// precision when converting to f32. -fn trunc(value: Fixed) -> Fixed { - Fixed::from_bits(value.to_bits() & !0x3FF) -} - -fn half(value: Fixed) -> Fixed { - Fixed::from_bits(value.to_bits() / 2) -} - -fn twice(value: Fixed) -> Fixed { - Fixed::from_bits(value.to_bits().wrapping_mul(2)) -} - -#[cfg(test)] -mod tests { - use read_fonts::{tables::postscript::charstring::CommandSink, types::F2Dot14, FontRef}; - - use super::{ - BlueZone, Blues, Fixed, Hint, HintMap, HintMask, HintParams, HintState, HintingSink, - StemHint, GHOST_BOTTOM, GHOST_TOP, HINT_MASK_SIZE, LOCKED, PAIR_BOTTOM, PAIR_TOP, - }; - - fn make_hint_state() -> HintState { - fn make_blues(values: &[f64]) -> Blues { - Blues::new(values.iter().copied().map(Fixed::from_f64)) - } - // - // - // - // - // - let params = HintParams { - blues: make_blues(&[ - -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, - ]), - other_blues: make_blues(&[-255.0, -240.0]), - blue_scale: Fixed::from_f64(0.05), - blue_shift: Fixed::from_i32(7), - blue_fuzz: Fixed::ZERO, - ..Default::default() - }; - HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)) - } - - #[test] - fn scaled_blue_zones() { - let state = make_hint_state(); - assert!(!state.do_em_box_hints); - assert_eq!(state.zone_count, 6); - assert_eq!(state.boost, Fixed::from_bits(27035)); - assert!(state.supress_overshoot); - // FreeType generates the following zones: - let expected_zones = &[ - // csBottomEdge -983040 int - // csTopEdge 0 int - // csFlatEdge 0 int - // dsFlatEdge 0 int - // bottomZone 1 '\x1' unsigned char - BlueZone { - cs_bottom_edge: Fixed::from_bits(-983040), - is_bottom: true, - ..Default::default() - }, - // csBottomEdge 35127296 int - // csTopEdge 35848192 int - // csFlatEdge 35127296 int - // dsFlatEdge 589824 int - // bottomZone 0 '\0' unsigned char - BlueZone { - cs_bottom_edge: Fixed::from_bits(35127296), - cs_top_edge: Fixed::from_bits(35848192), - cs_flat_edge: Fixed::from_bits(35127296), - ds_flat_edge: Fixed::from_bits(589824), - is_bottom: false, - }, - // csBottomEdge 37421056 int - // csTopEdge 38141952 int - // csFlatEdge 37421056 int - // dsFlatEdge 589824 int - // bottomZone 0 '\0' unsigned char - BlueZone { - cs_bottom_edge: Fixed::from_bits(37421056), - cs_top_edge: Fixed::from_bits(38141952), - cs_flat_edge: Fixed::from_bits(37421056), - ds_flat_edge: Fixed::from_bits(589824), - is_bottom: false, - }, - // csBottomEdge 46792704 int - // csTopEdge 47579136 int - // csFlatEdge 46792704 int - // dsFlatEdge 786432 int - // bottomZone 0 '\0' unsigned char - BlueZone { - cs_bottom_edge: Fixed::from_bits(46792704), - cs_top_edge: Fixed::from_bits(47579136), - cs_flat_edge: Fixed::from_bits(46792704), - ds_flat_edge: Fixed::from_bits(786432), - is_bottom: false, - }, - // csBottomEdge 49807360 int - // csTopEdge 50593792 int - // csFlatEdge 49807360 int - // dsFlatEdge 786432 int - // bottomZone 0 '\0' unsigned char - BlueZone { - cs_bottom_edge: Fixed::from_bits(49807360), - cs_top_edge: Fixed::from_bits(50593792), - cs_flat_edge: Fixed::from_bits(49807360), - ds_flat_edge: Fixed::from_bits(786432), - is_bottom: false, - }, - // csBottomEdge -16711680 int - // csTopEdge -15728640 int - // csFlatEdge -15728640 int - // dsFlatEdge -262144 int - // bottomZone 1 '\x1' unsigned char - BlueZone { - cs_bottom_edge: Fixed::from_bits(-16711680), - cs_top_edge: Fixed::from_bits(-15728640), - cs_flat_edge: Fixed::from_bits(-15728640), - ds_flat_edge: Fixed::from_bits(-262144), - is_bottom: true, - }, - ]; - assert_eq!(state.zones(), expected_zones); - } - - #[test] - fn blue_zone_capture() { - let state = make_hint_state(); - let bottom_edge = Hint { - flags: PAIR_BOTTOM, - ds_coord: Fixed::from_f64(2.3), - ..Default::default() - }; - let top_edge = Hint { - flags: PAIR_TOP, - // This value chosen to fit within the first "top" blue zone - cs_coord: Fixed::from_bits(35127297), - ds_coord: Fixed::from_f64(2.3), - ..Default::default() - }; - // Capture both - { - let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge); - assert!(state.capture(&mut bottom_edge, &mut top_edge)); - assert!(bottom_edge.is_locked()); - assert!(top_edge.is_locked()); - } - // Capture none - { - // Used to guarantee the edges are below all blue zones and will - // not be captured - let min_cs_coord = Fixed::MIN; - let mut bottom_edge = Hint { - cs_coord: min_cs_coord, - ..bottom_edge - }; - let mut top_edge = Hint { - cs_coord: min_cs_coord, - ..top_edge - }; - assert!(!state.capture(&mut bottom_edge, &mut top_edge)); - assert!(!bottom_edge.is_locked()); - assert!(!top_edge.is_locked()); - } - // Capture bottom, ignore invalid top - { - let mut bottom_edge = bottom_edge; - let mut top_edge = Hint { - // Empty flags == invalid hint - flags: 0, - ..top_edge - }; - assert!(state.capture(&mut bottom_edge, &mut top_edge)); - assert!(bottom_edge.is_locked()); - assert!(!top_edge.is_locked()); - } - // Capture top, ignore invalid bottom - { - let mut bottom_edge = Hint { - // Empty flags == invalid hint - flags: 0, - ..bottom_edge - }; - let mut top_edge = top_edge; - assert!(state.capture(&mut bottom_edge, &mut top_edge)); - assert!(!bottom_edge.is_locked()); - assert!(top_edge.is_locked()); - } - } - - #[test] - fn hint_mask_ops() { - const MAX_BITS: usize = HINT_MASK_SIZE * 8; - let all_bits = HintMask::all(); - for i in 0..MAX_BITS { - assert!(all_bits.get(i)); - } - let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap(); - for i in 0..MAX_BITS { - assert_eq!(i & 1 != 0, odd_bits.get(i)); - } - let mut cleared_bits = odd_bits; - for i in 0..MAX_BITS { - if i & 1 != 0 { - cleared_bits.clear(i); - } - } - assert_eq!(cleared_bits.mask, HintMask::default().mask); - } - - #[test] - fn hint_mapping() { - let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); - let cff_font = super::super::outlines::Outlines::new(&font).unwrap(); - let state = cff_font - .subfont(0, 8.0, &[F2Dot14::from_f32(-1.0); 2]) - .unwrap() - .hint_state; - let mut initial_map = HintMap::new(state.scale); - let mut map = HintMap::new(state.scale); - // Stem hints from Cantarell-VF.otf glyph id 2 - let mut stems = [ - StemHint { - min: Fixed::from_bits(1376256), - max: Fixed::ZERO, - ..Default::default() - }, - StemHint { - min: Fixed::from_bits(16318464), - max: Fixed::from_bits(17563648), - ..Default::default() - }, - StemHint { - min: Fixed::from_bits(45481984), - max: Fixed::from_bits(44171264), - ..Default::default() - }, - ]; - map.build( - &state, - Some(HintMask::all()), - Some(&mut initial_map), - &mut stems, - Fixed::ZERO, - false, - ); - // FT generates the following hint map: - // - // index csCoord dsCoord scale flags - // 0 0.00 0.00 526 gbL - // 1 249.00 250.14 524 pb - // 1 268.00 238.22 592 pt - // 2 694.00 750.41 524 gtL - let expected_edges = [ - Hint { - index: 0, - cs_coord: Fixed::from_f64(0.0), - ds_coord: Fixed::from_f64(0.0), - scale: Fixed::from_bits(526), - flags: GHOST_BOTTOM | LOCKED, - }, - Hint { - index: 1, - cs_coord: Fixed::from_bits(16318464), - ds_coord: Fixed::from_bits(131072), - scale: Fixed::from_bits(524), - flags: PAIR_BOTTOM, - }, - Hint { - index: 1, - cs_coord: Fixed::from_bits(17563648), - ds_coord: Fixed::from_bits(141028), - scale: Fixed::from_bits(592), - flags: PAIR_TOP, - }, - Hint { - index: 2, - cs_coord: Fixed::from_bits(45481984), - ds_coord: Fixed::from_bits(393216), - scale: Fixed::from_bits(524), - flags: GHOST_TOP | LOCKED, - }, - ]; - assert_eq!(expected_edges, &map.edges[..map.len]); - // And FT generates the following mappings - let mappings = [ - // (coord in font units, expected hinted coord in device space) in 16.16 - (0, 0), // 0 -> 0 - (44302336, 382564), // 676 -> 5.828125 - (45481984, 393216), // 694 -> 6 - (16318464, 131072), // 249 -> 2 - (17563648, 141028), // 268 -> 2.140625 - (49676288, 426752), // 758 -> 6.5 - (56754176, 483344), // 866 -> 7.375 - (57868288, 492252), // 883 -> 7.5 - (50069504, 429896), // 764 -> 6.546875 - ]; - for (coord, expected) in mappings { - assert_eq!( - map.transform(Fixed::from_bits(coord)), - Fixed::from_bits(expected) - ); - } - } - - /// HintingSink is mostly pass-through. This test captures the logic - /// around omission of pending lines that match subpath start. - /// See HintingSink::maybe_close_subpath for details. - #[test] - fn hinting_sink_omits_closing_line_that_matches_start() { - let state = HintState { - scale: Fixed::ONE, - ..Default::default() - }; - let mut path = Path::default(); - let mut sink = HintingSink::new(&state, &mut path); - let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; - let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)]; - let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; - let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)]; - let curve = [ - Fixed::from_f64(3.0), - Fixed::from_f64(4.0), - Fixed::from_f64(5.0), - Fixed::from_f64(6.0), - Fixed::from_f64(1.0), - Fixed::from_f64(2.0), - ]; - // First subpath, closing line matches start - sink.move_to(move1_2[0], move1_2[1]); - sink.line_to(line2_3[0], line2_3[1]); - sink.line_to(line1_2[0], line1_2[1]); - // Second subpath, closing line does not match start - sink.move_to(move1_2[0], move1_2[1]); - sink.line_to(line2_3[0], line2_3[1]); - sink.line_to(line3_4[0], line3_4[1]); - // Third subpath, ends with cubic. Still emits a close command - // even though end point matches start. - sink.move_to(move1_2[0], move1_2[1]); - sink.line_to(line2_3[0], line2_3[1]); - sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]); - sink.finish(); - // Subpaths always end with a close command. If a final line coincides - // with the start of a subpath, it is omitted. - assert_eq!( - &path.0, - &[ - // First subpath - MoveTo(move1_2), - LineTo(line2_3), - // line1_2 is omitted - Close, - // Second subpath - MoveTo(move1_2), - LineTo(line2_3), - LineTo(line3_4), - Close, - // Third subpath - MoveTo(move1_2), - LineTo(line2_3), - CurveTo(curve), - Close, - ] - ); - } - - #[derive(Copy, Clone, PartialEq, Debug)] - enum Command { - MoveTo([Fixed; 2]), - LineTo([Fixed; 2]), - CurveTo([Fixed; 6]), - Close, - } - - use Command::*; - - #[derive(Default)] - struct Path(Vec); - - impl CommandSink for Path { - fn move_to(&mut self, x: Fixed, y: Fixed) { - self.0.push(MoveTo([x, y])); - } - fn line_to(&mut self, x: Fixed, y: Fixed) { - self.0.push(LineTo([x, y])); - } - fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) { - self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y])); - } - fn close(&mut self) { - self.0.push(Close); - } - } -} diff --git a/src/scale/cff3/mod.rs b/src/scale/cff3/mod.rs deleted file mode 100644 index b2f6ae8..0000000 --- a/src/scale/cff3/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -mod hint; -mod outlines; - -pub(crate) use outlines::Outlines; - -use super::Outline; -use read_fonts::types::{F2Dot14, GlyphId}; - -pub struct SubfontCache { - entries: Vec, - max_entries: usize, - epoch: u64, -} - -impl SubfontCache { - pub fn new(max_entries: usize) -> Self { - Self { - entries: Vec::new(), - max_entries, - epoch: 0, - } - } - - pub fn scale( - &mut self, - outlines: &outlines::Outlines, - id: u64, - glyph_id: u16, - size: f32, - coords: &[i16], - hint: bool, - outline: &mut Outline, - ) -> Option<()> { - let epoch = self.epoch; - let gid = GlyphId::new(glyph_id); - let subfont_index = outlines.subfont_index(gid); - let (found, entry_index) = self.find_entry(id, subfont_index, coords, size); - let (subfont, coords) = if found { - let entry = &mut self.entries[entry_index]; - entry.epoch = epoch; - (&entry.subfont, &entry.coords) - } else { - self.epoch += 1; - let epoch = self.epoch; - if entry_index == self.entries.len() { - let coords: Vec = coords.iter().map(|x| F2Dot14::from_bits(*x)).collect(); - let subfont = outlines.subfont(subfont_index, size, &coords).ok()?; - self.entries.push(Entry { - id, - epoch, - subfont, - subfont_index, - size, - coords, - }); - let entry = &self.entries[entry_index]; - (&entry.subfont, &entry.coords) - } else { - let entry = &mut self.entries[entry_index]; - entry.id = u64::MAX; - entry.epoch = epoch; - entry.coords.clear(); - entry - .coords - .extend(coords.iter().map(|x| F2Dot14::from_bits(*x))); - entry.subfont = outlines.subfont(subfont_index, size, &entry.coords).ok()?; - entry.id = id; - entry.subfont_index = subfont_index; - entry.size = size; - (&entry.subfont, &entry.coords) - } - }; - outlines - .draw(subfont, gid, coords, hint, &mut OutlineBuilder(outline)) - .ok()?; - Some(()) - } - - fn find_entry(&self, id: u64, index: u32, coords: &[i16], size: f32) -> (bool, usize) { - let mut lowest_epoch = self.epoch; - let mut lowest_index = 0; - for (i, entry) in self.entries.iter().enumerate() { - if entry.id == id - && entry.subfont_index == index - && entry.size == size - && coords - .iter() - .map(|x| F2Dot14::from_bits(*x)) - .eq(entry.coords.iter().copied()) - { - return (true, i); - } - if entry.epoch < lowest_epoch { - lowest_epoch = entry.epoch; - lowest_index = i; - } - } - if self.entries.len() < self.max_entries { - lowest_index = self.entries.len(); - } - (false, lowest_index) - } -} - -struct Entry { - epoch: u64, - id: u64, - subfont: outlines::Subfont, - subfont_index: u32, - size: f32, - coords: Vec, -} - -struct OutlineBuilder<'a>(&'a mut Outline); - -impl read_fonts::types::Pen for OutlineBuilder<'_> { - fn move_to(&mut self, x: f32, y: f32) { - self.0.move_to((x, y).into()); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.0.line_to((x, y).into()); - } - - fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { - self.0.quad_to((cx0, cy0).into(), (x, y).into()); - } - - fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { - self.0 - .curve_to((cx0, cy0).into(), (cx1, cy1).into(), (x, y).into()); - } - - fn close(&mut self) { - self.0.close(); - } -} diff --git a/src/scale/mod.rs b/src/scale/mod.rs index 0bf5599..721c265 100644 --- a/src/scale/mod.rs +++ b/src/scale/mod.rs @@ -219,7 +219,7 @@ pub mod image; pub mod outline; mod bitmap; -mod cff3; +mod cff; mod color; mod glyf; mod proxy; @@ -286,7 +286,7 @@ pub struct ScaleContext { struct State { glyf_scaler: glyf::Scaler, - cff_cache: cff3::SubfontCache, + cff_cache: cff::SubfontCache, scratch0: Vec, scratch1: Vec, outline: Outline, @@ -308,7 +308,7 @@ impl ScaleContext { fonts: FontCache::new(max_entries), state: State { glyf_scaler: glyf::Scaler::new(max_entries), - cff_cache: cff3::SubfontCache::new(max_entries), + cff_cache: cff::SubfontCache::new(max_entries), scratch0: Vec::new(), scratch1: Vec::new(), outline: Outline::new(), @@ -432,7 +432,7 @@ impl<'a> ScalerBuilder<'a> { read_fonts::FontRef::from_index(self.font.data, index as u32).ok() }) }; - font.and_then(|font| cff3::Outlines::new(&font).ok()) + font.and_then(|font| cff::Outlines::new(&font).ok()) } else { None }; @@ -458,7 +458,7 @@ pub struct Scaler<'a> { state: &'a mut State, font: FontRef<'a>, proxy: &'a ScalerProxy, - cff: Option>, + cff: Option>, id: u64, coords: &'a [i16], size: f32,