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 };