From f87d25c672a1b150debc0c0c1db4d7f98b399b22 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 29 Jun 2023 17:34:26 -0400 Subject: [PATCH 01/10] [read-fonts] Add scaler for CFF/CFF2 --- read-fonts/src/tables/postscript.rs | 11 +- .../src/tables/postscript/charstring.rs | 189 +++++- read-fonts/src/tables/postscript/scale.rs | 566 ++++++++++++++++++ 3 files changed, 757 insertions(+), 9 deletions(-) create mode 100644 read-fonts/src/tables/postscript/scale.rs diff --git a/read-fonts/src/tables/postscript.rs b/read-fonts/src/tables/postscript.rs index 2065323d4..10f905d2b 100644 --- a/read-fonts/src/tables/postscript.rs +++ b/read-fonts/src/tables/postscript.rs @@ -5,6 +5,7 @@ use std::fmt; mod blend; mod fd_select; mod index; +mod scale; mod stack; mod string; @@ -15,6 +16,7 @@ include!("../../generated/generated_postscript.rs"); pub use blend::BlendState; pub use index::Index; +pub use scale::{Scaler, SubfontInstance}; pub use stack::{Number, Stack}; pub use string::{Latin1String, StringId, STANDARD_STRINGS}; @@ -34,6 +36,8 @@ pub enum Error { CharstringNestingDepthLimitExceeded, MissingSubroutines, MissingBlendState, + MissingPrivateDict, + MissingCharstrings, Read(ReadError), } @@ -98,7 +102,12 @@ impl fmt::Display for Error { "encountered a blend operator but no blend state was provided" ) } - + Self::MissingPrivateDict => { + write!(f, "CFF table does not contain a private dictionary") + } + Self::MissingCharstrings => { + write!(f, "CFF table does not contain a charstrings index") + } Self::Read(err) => write!(f, "{err}"), } } diff --git a/read-fonts/src/tables/postscript/charstring.rs b/read-fonts/src/tables/postscript/charstring.rs index 131aa31c1..0ea747e62 100644 --- a/read-fonts/src/tables/postscript/charstring.rs +++ b/read-fonts/src/tables/postscript/charstring.rs @@ -55,21 +55,21 @@ where P: Pen, { fn move_to(&mut self, x: Fixed, y: Fixed) { - self.0.move_to(x.to_f64() as f32, y.to_f64() as f32); + self.0.move_to(x.to_f32(), y.to_f32()); } fn line_to(&mut self, x: Fixed, y: Fixed) { - self.0.line_to(x.to_f64() as f32, y.to_f64() as f32); + self.0.line_to(x.to_f32(), y.to_f32()); } fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) { self.0.curve_to( - cx0.to_f64() as f32, - cy0.to_f64() as f32, - cx1.to_f64() as f32, - cy1.to_f64() as f32, - x.to_f64() as f32, - y.to_f64() as f32, + cx0.to_f32(), + cy0.to_f32(), + cx1.to_f32(), + cy1.to_f32(), + x.to_f32(), + y.to_f32(), ); } @@ -78,6 +78,179 @@ where } } +/// 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. +pub(crate) struct ScalingSink26Dot6<'a, S> { + inner: &'a mut S, + scale: Fixed, +} + +impl<'a, S> ScalingSink26Dot6<'a, S> { + pub 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: + // 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 simplifies path operations. +/// +/// This currently removes degenerate moves and lines. +pub(crate) struct SimplifyingSink<'a, S> { + start: Option<(Fixed, Fixed)>, + last: Option<(Fixed, Fixed)>, + pending_move: Option<(Fixed, Fixed)>, + inner: &'a mut S, +} + +impl<'a, S> SimplifyingSink<'a, S> +where + S: CommandSink, +{ + pub 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 SimplifyingSink<'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() + } + } +} + /// Evaluates the given charstring and emits the resulting commands to the /// specified sink. /// diff --git a/read-fonts/src/tables/postscript/scale.rs b/read-fonts/src/tables/postscript/scale.rs new file mode 100644 index 000000000..702d85f43 --- /dev/null +++ b/read-fonts/src/tables/postscript/scale.rs @@ -0,0 +1,566 @@ +//! Scaler for CFF outlines. + +use std::ops::Range; + +use super::{ + dict::{self, Blues}, + BlendState, Error, FdSelect, Index, +}; +use crate::{ + tables::{cff::Cff, cff2::Cff2, variations::ItemVariationStore}, + types::{F2Dot14, Fixed, GlyphId, Pen}, + FontData, FontRead, TableProvider, +}; + +/// State for reading and scaling glyph outlines from CFF/CFF2 tables. +pub struct Scaler<'a> { + version: Version<'a>, + top_dict: TopDict<'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 = 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 charstrings index. + /// + /// Contains the charstrings of all the glyphs in a font stored in an + /// INDEX structure. Charstring objects contained within this INDEX + /// are accessed by GID. + /// + /// See "CharStrings INDEX" at + pub fn charstrings(&self) -> &Option> { + &self.top_dict.charstrings + } + + /// Returns the font dict index. + /// + /// A Font DICT is used for hinting, variation or subroutine (subr) data + /// used by CharStrings. + /// + /// See + pub fn font_dicts(&self) -> &Option> { + &self.top_dict.font_dicts + } + + /// Returns the fd select table. + /// + /// The FDSelect associates an FD (Font DICT) with a glyph by specifying an + /// FD index for that glyph. The FD index is used to access of of the Font + /// DICTS stored in the Font DICT INDEX. + /// + /// See "FDSelect" at + pub fn fd_select(&self) -> &Option> { + &self.top_dict.fd_select + } + + /// Returns the data for the default Private DICT. + /// + /// See "Private DICT Data" at + pub fn default_private_dict_data(&self) -> Option<&'a [u8]> { + self.offset_data() + .as_bytes() + .get(self.top_dict.private_dict_range.clone()?) + } + + /// Returns the global subroutine index. + /// + /// This contains sub-programs that are referenced by one or more + /// charstrings in the font set. + /// + /// See "Local/Global Subrs INDEXes" at + pub fn global_subrs(&self) -> Index<'a> { + match &self.version { + Version::Version1(cff1) => cff1.global_subrs().into(), + Version::Version2(cff2) => cff2.global_subrs().into(), + } + } + + /// Returns the item variation store that is used for applying variations + /// during dict and charstring evaluation. + /// + /// This is only present in CFF2 tables in variable fonts. + pub fn var_store(&self) -> &Option> { + &self.top_dict.var_store + } + + /// 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 { + self.top_dict + .fd_select + .as_ref() + .and_then(|select| select.font_index(glyph_id)) + // Missing FDSelect assumes either a single Font DICT at index 0, + // or a Private DICT entry in the Top DICT. + .unwrap_or(0) as u32 + } + + /// Returns a new subfont instance for the given index, size and normalized + /// variation coordinates and hinting state. + /// + /// The index of a subfont for a particular glyph can be retrieved with + /// the `subfont_index` method. + pub fn subfont_instance( + &self, + index: u32, + size: f32, + coords: &[F2Dot14], + with_hinting: bool, + ) -> 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, + _ => {} + } + } + // TODO: convert hint params to zones if hinting is requested + let _ = with_hinting; + Ok(SubfontInstance { + is_cff2: self.is_cff2(), + index, + size, + subrs_offset, + hint_params, + store_index, + }) + } + + /// Evalutes a charstring for the given subfont instance, glyph identifier + /// and normalized variation coordinates. + /// + /// Before calling this method, use [`Scaler::subfont_index`] to retrieve + /// the subfont index for the desired glyph and then + /// [`Scaler::subfont_instance`] 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: &SubfontInstance, + glyph_id: GlyphId, + coords: &[F2Dot14], + pen: &mut impl Pen, + ) -> Result<(), Error> { + use super::charstring; + let charstring_data = self + .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 = charstring::SimplifyingSink::new(&mut pen_sink); + let scale = if subfont.size <= 0.0 { + Fixed::ONE + } else { + // Note: we do an intermediate scale to 26.6 to ensure we + // match FreeType + Fixed::from_bits((subfont.size * 64.) as i32) + / Fixed::from_bits(self.units_per_em as i32) + }; + let mut scaling_adapter = + charstring::ScalingSink26Dot6::new(&mut simplifying_adapter, scale); + // TODO: hinting will be another sink adapter that slots in here + 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 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 index, use that + 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 { + // Otherwise, assume the top dict provided a private dict range + self.top_dict.private_dict_range.clone() + } + .ok_or(Error::MissingPrivateDict) + } +} + +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 SubfontInstance { + is_cff2: bool, + index: u32, + size: f32, + subrs_offset: Option, + // TODO: just capturing these for now. We'll soon compute the actual + // hinting state ("blue zones") from these values. + #[allow(dead_code)] + hint_params: HintParams, + store_index: u16, +} + +impl SubfontInstance { + 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.var_store().clone() { + Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) + } else { + Ok(None) + } + } +} + +/// Parameters used to generate the stem and counter zones for the hinting +/// algorithm. +#[derive(Clone)] +pub 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, + } + } +} + +/// Entries that we parse from the Top DICT to support charstring +/// evaluation. +#[derive(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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::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_noto_serif_display() { + let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); + let cff = Scaler::new(&font).unwrap(); + assert!(!cff.is_cff2()); + assert!(cff.var_store().is_none()); + assert!(!cff.font_dicts().is_some()); + assert!(cff.default_private_dict_data().is_some()); + assert!(cff.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_instance(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_cantarell_vf() { + let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); + let cff = Scaler::new(&font).unwrap(); + assert!(cff.is_cff2()); + assert!(cff.var_store().is_some()); + assert!(cff.font_dicts().is_some()); + assert!(cff.default_private_dict_data().is_none()); + assert!(cff.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_instance(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.var_store().is_some()); + assert!(cff.font_dicts().is_some()); + assert!(cff.default_private_dict_data().is_none()); + assert!(cff.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 cantarell_vf_outlines() { + compare_glyphs( + font_test_data::CANTARELL_VF_TRIMMED, + font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, + ); + } + + #[test] + fn noto_serif_display_outlines() { + compare_glyphs( + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, + ); + } + + fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { + let font = FontRef::new(font_data).unwrap(); + let outlines = crate::scaler_test::parse_glyph_outlines(expected_outlines); + let scaler = super::Scaler::new(&font).unwrap(); + let mut path = crate::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_instance( + scaler.subfont_index(expected_outline.glyph_id), + expected_outline.size, + &expected_outline.coords, + false, + ) + .unwrap(); + scaler + .outline( + &subfont, + expected_outline.glyph_id, + &expected_outline.coords, + &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 + ); + } + } + } +} From 1c4ba861fe2fd9b58503ae09e30a56143384bb53 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 30 Jun 2023 03:52:45 -0400 Subject: [PATCH 02/10] teach skrifa to read CFF --- skrifa/src/scale/error.rs | 14 ++++++++- skrifa/src/scale/mod.rs | 36 +++++++++++++++++++--- skrifa/src/scale/scaler.rs | 62 ++++++++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/skrifa/src/scale/error.rs b/skrifa/src/scale/error.rs index c3910a8d0..e98bd2402 100644 --- a/skrifa/src/scale/error.rs +++ b/skrifa/src/scale/error.rs @@ -1,4 +1,7 @@ -use read_fonts::{tables::glyf::ToPathError, types::GlyphId, ReadError}; +use read_fonts::{ + tables::glyf::ToPathError, tables::postscript::Error as PostScriptError, types::GlyphId, + ReadError, +}; use std::fmt; @@ -16,6 +19,8 @@ pub enum Error { HintingFailed(GlyphId), /// An anchor point had invalid indices. InvalidAnchorPoint(GlyphId, u16), + /// Error occurred while loading a PostScript (CFF/CFF2) glyph. + PostScript(PostScriptError), /// Conversion from outline to path failed. ToPath(ToPathError), /// Error occured when reading font data. @@ -34,6 +39,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: PostScriptError) -> Self { + Self::PostScript(value) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -50,6 +61,7 @@ impl fmt::Display for Error { f, "Invalid anchor point index ({index}) for composite glyph {gid}", ), + Self::PostScript(e) => write!(f, "{e}"), Self::ToPath(e) => write!(f, "{e}"), Self::Read(e) => write!(f, "{e}"), } diff --git a/skrifa/src/scale/mod.rs b/skrifa/src/scale/mod.rs index 24c3f16fe..a4360e02b 100644 --- a/skrifa/src/scale/mod.rs +++ b/skrifa/src/scale/mod.rs @@ -219,19 +219,47 @@ impl Context { #[cfg(test)] mod tests { use super::{Context, Size}; - use font_test_data::{VAZIRMATN_VAR, VAZIRMATN_VAR_GLYPHS}; use read_fonts::{scaler_test, FontRef}; #[test] fn vazirmatin_var() { - let font = FontRef::new(VAZIRMATN_VAR).unwrap(); - let outlines = scaler_test::parse_glyph_outlines(VAZIRMATN_VAR_GLYPHS); + compare_glyphs( + font_test_data::VAZIRMATN_VAR, + font_test_data::VAZIRMATN_VAR_GLYPHS, + false, + ); + } + + #[test] + fn cantarell_vf() { + compare_glyphs( + font_test_data::CANTARELL_VF_TRIMMED, + font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, + true, + ); + } + + #[test] + fn noto_serif_display() { + compare_glyphs( + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, + font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, + true, + ); + } + + fn compare_glyphs(font_data: &[u8], expected_outlines: &str, is_cff: bool) { + let font = FontRef::new(font_data).unwrap(); + let outlines = scaler_test::parse_glyph_outlines(expected_outlines); let mut cx = Context::new(); let mut path = scaler_test::Path { elements: vec![], - is_cff: false, + is_cff, }; for expected_outline in &outlines { + if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() { + continue; + } path.elements.clear(); let mut scaler = cx .new_scaler() diff --git a/skrifa/src/scale/scaler.rs b/skrifa/src/scale/scaler.rs index 3583bd960..cc827992f 100644 --- a/skrifa/src/scale/scaler.rs +++ b/skrifa/src/scale/scaler.rs @@ -5,6 +5,7 @@ use super::Hinting; use core::borrow::Borrow; use read_fonts::{ + tables::postscript::Scaler as PostScriptScaler, types::{Fixed, GlyphId}, TableProvider, }; @@ -122,22 +123,24 @@ impl<'a> ScalerBuilder<'a> { pub fn build(mut self, font: &impl TableProvider<'a>) -> Scaler<'a> { self.resolve_variations(font); let coords = &self.context.coords[..]; - let glyf = if let Ok(glyf) = glyf::Scaler::new( + let size = self.size.ppem().unwrap_or_default(); + let outlines = if let Ok(glyf) = glyf::Scaler::new( &mut self.context.glyf, font, self.cache_key, - self.size.ppem().unwrap_or_default(), + size, #[cfg(feature = "hinting")] self.hint, coords, ) { - Some((glyf, &mut self.context.glyf_outline)) + Some(Outlines::TrueType(glyf, &mut self.context.glyf_outline)) } else { - None + PostScriptScaler::new(font).ok().map(Outlines::PostScript) }; Scaler { + size, coords, - outlines: Outlines { glyf }, + outlines, } } @@ -186,8 +189,9 @@ impl<'a> ScalerBuilder<'a> { /// See the [module level documentation](crate::scale#getting-an-outline) /// for more detail. pub struct Scaler<'a> { + size: f32, coords: &'a [NormalizedCoord], - outlines: Outlines<'a>, + outlines: Option>, } impl<'a> Scaler<'a> { @@ -198,32 +202,44 @@ impl<'a> Scaler<'a> { /// Returns true if the scaler has a source for simple outlines. pub fn has_outlines(&self) -> bool { - self.outlines.has_outlines() + self.outlines.is_some() } /// Loads a simple outline for the specified glyph identifier and invokes the functions - /// in the given sink for the sequence of path commands that define the outline. - pub fn outline(&mut self, glyph_id: GlyphId, sink: &mut impl Pen) -> Result<()> { - self.outlines.outline(glyph_id, sink) + /// in the given pen for the sequence of path commands that define the outline. + pub fn outline(&mut self, glyph_id: GlyphId, pen: &mut impl Pen) -> Result<()> { + if let Some(outlines) = &mut self.outlines { + outlines.outline(glyph_id, self.size, self.coords, pen) + } else { + Err(Error::NoSources) + } } } -/// Outline glyph scalers. -struct Outlines<'a> { - glyf: Option<(glyf::Scaler<'a>, &'a mut glyf::Outline)>, +enum Outlines<'a> { + TrueType(glyf::Scaler<'a>, &'a mut glyf::Outline), + PostScript(PostScriptScaler<'a>), } impl<'a> Outlines<'a> { - fn has_outlines(&self) -> bool { - self.glyf.is_some() - } - - fn outline(&mut self, glyph_id: GlyphId, sink: &mut impl Pen) -> Result<()> { - if let Some((scaler, glyf_outline)) = &mut self.glyf { - scaler.load(glyph_id, glyf_outline)?; - Ok(glyf_outline.to_path(sink)?) - } else { - Err(Error::NoSources) + fn outline( + &mut self, + glyph_id: GlyphId, + size: f32, + coords: &'a [NormalizedCoord], + pen: &mut impl Pen, + ) -> Result<()> { + match self { + Self::TrueType(scaler, outline) => { + scaler.load(glyph_id, outline)?; + Ok(outline.to_path(pen)?) + } + Self::PostScript(scaler) => { + let subfont_index = scaler.subfont_index(glyph_id); + // TODO: cache these + let subfont = scaler.subfont_instance(subfont_index, size, coords, false)?; + Ok(scaler.outline(&subfont, glyph_id, coords, pen)?) + } } } } From 6d71cba81884f3d994b848578930cf6560b22ca0 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 30 Jun 2023 04:06:40 -0400 Subject: [PATCH 03/10] fix typos/clean up some comments --- read-fonts/src/tables/postscript/scale.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/read-fonts/src/tables/postscript/scale.rs b/read-fonts/src/tables/postscript/scale.rs index 702d85f43..4a1701572 100644 --- a/read-fonts/src/tables/postscript/scale.rs +++ b/read-fonts/src/tables/postscript/scale.rs @@ -150,11 +150,11 @@ impl<'a> Scaler<'a> { .unwrap_or(0) as u32 } - /// Returns a new subfont instance for the given index, size and normalized + /// Creates a new subfont instance 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` method. + /// the [`subfont_index`](Self::subfont_index) method. pub fn subfont_instance( &self, index: u32, @@ -202,15 +202,15 @@ impl<'a> Scaler<'a> { }) } - /// Evalutes a charstring for the given subfont instance, glyph identifier - /// and normalized variation coordinates. + /// Loads and scales an outline for the given subfont instance, glyph + /// identifier and normalized variation coordinates. /// - /// Before calling this method, use [`Scaler::subfont_index`] to retrieve - /// the subfont index for the desired glyph and then - /// [`Scaler::subfont_instance`] 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. + /// Before calling this method, use [`subfont_index`](Self::subfont_index) + /// to retrieve the subfont index for the desired glyph and then + /// [`subfont_instance`](Self::subfont_instance) 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( From 3979c094b0ea116a1e8cfce6a6ef59a5805400a2 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 30 Jun 2023 13:36:59 -0400 Subject: [PATCH 04/10] cache single MRU subfont instance This is a 60% gain on scaling all glyphs in Source Sans Pro --- skrifa/src/scale/scaler.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/skrifa/src/scale/scaler.rs b/skrifa/src/scale/scaler.rs index cc827992f..ef3e2b986 100644 --- a/skrifa/src/scale/scaler.rs +++ b/skrifa/src/scale/scaler.rs @@ -5,7 +5,7 @@ use super::Hinting; use core::borrow::Borrow; use read_fonts::{ - tables::postscript::Scaler as PostScriptScaler, + tables::postscript::{Scaler as PostScriptScaler, SubfontInstance}, types::{Fixed, GlyphId}, TableProvider, }; @@ -135,7 +135,13 @@ impl<'a> ScalerBuilder<'a> { ) { Some(Outlines::TrueType(glyf, &mut self.context.glyf_outline)) } else { - PostScriptScaler::new(font).ok().map(Outlines::PostScript) + PostScriptScaler::new(font) + .ok() + .and_then(|scaler| { + let first_subfont = scaler.subfont_instance(0, size, coords, false).ok()?; + Some((scaler, first_subfont)) + }) + .map(|(scaler, subfont)| Outlines::PostScript(scaler, subfont)) }; Scaler { size, @@ -216,9 +222,12 @@ impl<'a> Scaler<'a> { } } +// Clippy doesn't like the size discrepancy between the two variants. Ignore +// for now: we'll replace this with a real cache. +#[allow(clippy::large_enum_variant)] enum Outlines<'a> { TrueType(glyf::Scaler<'a>, &'a mut glyf::Outline), - PostScript(PostScriptScaler<'a>), + PostScript(PostScriptScaler<'a>, SubfontInstance), } impl<'a> Outlines<'a> { @@ -234,11 +243,12 @@ impl<'a> Outlines<'a> { scaler.load(glyph_id, outline)?; Ok(outline.to_path(pen)?) } - Self::PostScript(scaler) => { + Self::PostScript(scaler, subfont) => { let subfont_index = scaler.subfont_index(glyph_id); - // TODO: cache these - let subfont = scaler.subfont_instance(subfont_index, size, coords, false)?; - Ok(scaler.outline(&subfont, glyph_id, coords, pen)?) + if subfont_index != subfont.index() { + *subfont = scaler.subfont_instance(subfont_index, size, coords, false)?; + } + Ok(scaler.outline(subfont, glyph_id, coords, pen)?) } } } From f440b7e8a3de95c2cfad519583ed7c95670cb9bd Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 5 Jul 2023 14:19:49 -0400 Subject: [PATCH 05/10] cleanup public API --- read-fonts/src/tables/postscript.rs | 2 +- read-fonts/src/tables/postscript/scale.rs | 138 +++++++--------------- skrifa/src/scale/scaler.rs | 8 +- 3 files changed, 45 insertions(+), 103 deletions(-) diff --git a/read-fonts/src/tables/postscript.rs b/read-fonts/src/tables/postscript.rs index 10f905d2b..a1ffe2284 100644 --- a/read-fonts/src/tables/postscript.rs +++ b/read-fonts/src/tables/postscript.rs @@ -16,7 +16,7 @@ include!("../../generated/generated_postscript.rs"); pub use blend::BlendState; pub use index::Index; -pub use scale::{Scaler, SubfontInstance}; +pub use scale::{Scaler, ScalerSubfont}; pub use stack::{Number, Stack}; pub use string::{Latin1String, StringId, STANDARD_STRINGS}; diff --git a/read-fonts/src/tables/postscript/scale.rs b/read-fonts/src/tables/postscript/scale.rs index 4a1701572..5d14c6a17 100644 --- a/read-fonts/src/tables/postscript/scale.rs +++ b/read-fonts/src/tables/postscript/scale.rs @@ -15,7 +15,7 @@ use crate::{ /// State for reading and scaling glyph outlines from CFF/CFF2 tables. pub struct Scaler<'a> { version: Version<'a>, - top_dict: TopDict<'a>, + top_dict: ScalerTopDict<'a>, units_per_em: u16, } @@ -44,7 +44,7 @@ impl<'a> Scaler<'a> { 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)?; + let top_dict = ScalerTopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; Ok(Self { version: Version::Version1(cff1), top_dict, @@ -54,7 +54,7 @@ impl<'a> Scaler<'a> { 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)?; + let top_dict = ScalerTopDict::new(table_data, cff2.top_dict_data(), true)?; Ok(Self { version: Version::Version2(cff2), top_dict, @@ -66,68 +66,6 @@ impl<'a> Scaler<'a> { matches!(self.version, Version::Version2(_)) } - /// Returns the charstrings index. - /// - /// Contains the charstrings of all the glyphs in a font stored in an - /// INDEX structure. Charstring objects contained within this INDEX - /// are accessed by GID. - /// - /// See "CharStrings INDEX" at - pub fn charstrings(&self) -> &Option> { - &self.top_dict.charstrings - } - - /// Returns the font dict index. - /// - /// A Font DICT is used for hinting, variation or subroutine (subr) data - /// used by CharStrings. - /// - /// See - pub fn font_dicts(&self) -> &Option> { - &self.top_dict.font_dicts - } - - /// Returns the fd select table. - /// - /// The FDSelect associates an FD (Font DICT) with a glyph by specifying an - /// FD index for that glyph. The FD index is used to access of of the Font - /// DICTS stored in the Font DICT INDEX. - /// - /// See "FDSelect" at - pub fn fd_select(&self) -> &Option> { - &self.top_dict.fd_select - } - - /// Returns the data for the default Private DICT. - /// - /// See "Private DICT Data" at - pub fn default_private_dict_data(&self) -> Option<&'a [u8]> { - self.offset_data() - .as_bytes() - .get(self.top_dict.private_dict_range.clone()?) - } - - /// Returns the global subroutine index. - /// - /// This contains sub-programs that are referenced by one or more - /// charstrings in the font set. - /// - /// See "Local/Global Subrs INDEXes" at - pub fn global_subrs(&self) -> Index<'a> { - match &self.version { - Version::Version1(cff1) => cff1.global_subrs().into(), - Version::Version2(cff2) => cff2.global_subrs().into(), - } - } - - /// Returns the item variation store that is used for applying variations - /// during dict and charstring evaluation. - /// - /// This is only present in CFF2 tables in variable fonts. - pub fn var_store(&self) -> &Option> { - &self.top_dict.var_store - } - /// Returns the number of available subfonts. pub fn subfont_count(&self) -> u32 { self.top_dict @@ -150,18 +88,18 @@ impl<'a> Scaler<'a> { .unwrap_or(0) as u32 } - /// Creates a new subfont instance for the given index, size, normalized + /// 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_instance( + pub fn subfont( &self, index: u32, size: f32, coords: &[F2Dot14], with_hinting: bool, - ) -> Result { + ) -> 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(); @@ -192,7 +130,7 @@ impl<'a> Scaler<'a> { } // TODO: convert hint params to zones if hinting is requested let _ = with_hinting; - Ok(SubfontInstance { + Ok(ScalerSubfont { is_cff2: self.is_cff2(), index, size, @@ -215,14 +153,15 @@ impl<'a> Scaler<'a> { /// The result is emitted to the specified pen. pub fn outline( &self, - subfont: &SubfontInstance, + subfont: &ScalerSubfont, glyph_id: GlyphId, coords: &[F2Dot14], pen: &mut impl Pen, ) -> Result<(), Error> { use super::charstring; let charstring_data = self - .charstrings() + .top_dict + .charstrings .as_ref() .ok_or(Error::MissingCharstrings)? .get(glyph_id.to_u16() as usize)?; @@ -259,6 +198,13 @@ impl<'a> Scaler<'a> { } } + 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 index, use that @@ -292,7 +238,7 @@ enum Version<'a> { /// /// For variable fonts, this is dependent on a location in variation space. #[derive(Clone)] -pub struct SubfontInstance { +pub struct ScalerSubfont { is_cff2: bool, index: u32, size: f32, @@ -304,7 +250,7 @@ pub struct SubfontInstance { store_index: u16, } -impl SubfontInstance { +impl ScalerSubfont { pub fn index(&self) -> u32 { self.index } @@ -331,7 +277,7 @@ impl SubfontInstance { scaler: &Scaler<'a>, coords: &'a [F2Dot14], ) -> Result>, Error> { - if let Some(var_store) = scaler.var_store().clone() { + if let Some(var_store) = scaler.top_dict.var_store.clone() { Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) } else { Ok(None) @@ -369,10 +315,10 @@ impl Default for HintParams { } } -/// Entries that we parse from the Top DICT to support charstring -/// evaluation. +/// Entries that we parse from the Top DICT that are required to support +/// charstring evaluation. #[derive(Default)] -struct TopDict<'a> { +struct ScalerTopDict<'a> { charstrings: Option>, font_dicts: Option>, fd_select: Option>, @@ -380,9 +326,9 @@ struct TopDict<'a> { var_store: Option>, } -impl<'a> TopDict<'a> { +impl<'a> ScalerTopDict<'a> { fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result { - let mut items = TopDict::default(); + let mut items = ScalerTopDict::default(); for entry in dict::entries(top_dict_data, None) { match entry? { dict::Entry::CharstringsOffset(offset) => { @@ -436,16 +382,14 @@ mod tests { let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); let cff = Scaler::new(&font).unwrap(); assert!(!cff.is_cff2()); - assert!(cff.var_store().is_none()); - assert!(!cff.font_dicts().is_some()); - assert!(cff.default_private_dict_data().is_some()); - assert!(cff.fd_select().is_none()); + 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_instance(0, 0.0, Default::default(), false) - .unwrap(); + let subfont = cff.subfont(0, 0.0, Default::default(), false).unwrap(); let hinting_params = subfont.hint_params; check_blues( &hinting_params.blues, @@ -468,16 +412,14 @@ mod tests { let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); let cff = Scaler::new(&font).unwrap(); assert!(cff.is_cff2()); - assert!(cff.var_store().is_some()); - assert!(cff.font_dicts().is_some()); - assert!(cff.default_private_dict_data().is_none()); - assert!(cff.fd_select().is_none()); + 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_instance(0, 0.0, Default::default(), false) - .unwrap(); + let subfont = cff.subfont(0, 0.0, Default::default(), false).unwrap(); let hinting_params = subfont.hint_params; check_blues( &hinting_params.blues, @@ -497,10 +439,10 @@ mod tests { ) .unwrap(); assert!(cff.is_cff2()); - assert!(cff.var_store().is_some()); - assert!(cff.font_dicts().is_some()); - assert!(cff.default_private_dict_data().is_none()); - assert!(cff.fd_select().is_none()); + 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); @@ -536,7 +478,7 @@ mod tests { } path.elements.clear(); let subfont = scaler - .subfont_instance( + .subfont( scaler.subfont_index(expected_outline.glyph_id), expected_outline.size, &expected_outline.coords, diff --git a/skrifa/src/scale/scaler.rs b/skrifa/src/scale/scaler.rs index ef3e2b986..65032d56f 100644 --- a/skrifa/src/scale/scaler.rs +++ b/skrifa/src/scale/scaler.rs @@ -5,7 +5,7 @@ use super::Hinting; use core::borrow::Borrow; use read_fonts::{ - tables::postscript::{Scaler as PostScriptScaler, SubfontInstance}, + tables::postscript::{Scaler as PostScriptScaler, ScalerSubfont}, types::{Fixed, GlyphId}, TableProvider, }; @@ -138,7 +138,7 @@ impl<'a> ScalerBuilder<'a> { PostScriptScaler::new(font) .ok() .and_then(|scaler| { - let first_subfont = scaler.subfont_instance(0, size, coords, false).ok()?; + let first_subfont = scaler.subfont(0, size, coords, false).ok()?; Some((scaler, first_subfont)) }) .map(|(scaler, subfont)| Outlines::PostScript(scaler, subfont)) @@ -227,7 +227,7 @@ impl<'a> Scaler<'a> { #[allow(clippy::large_enum_variant)] enum Outlines<'a> { TrueType(glyf::Scaler<'a>, &'a mut glyf::Outline), - PostScript(PostScriptScaler<'a>, SubfontInstance), + PostScript(PostScriptScaler<'a>, ScalerSubfont), } impl<'a> Outlines<'a> { @@ -246,7 +246,7 @@ impl<'a> Outlines<'a> { Self::PostScript(scaler, subfont) => { let subfont_index = scaler.subfont_index(glyph_id); if subfont_index != subfont.index() { - *subfont = scaler.subfont_instance(subfont_index, size, coords, false)?; + *subfont = scaler.subfont(subfont_index, size, coords, false)?; } Ok(scaler.outline(subfont, glyph_id, coords, pen)?) } From 966720b21d47b54a56025289ec9e35777768a3d8 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 5 Jul 2023 14:26:34 -0400 Subject: [PATCH 06/10] fix doc comment link --- read-fonts/src/tables/postscript/scale.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/read-fonts/src/tables/postscript/scale.rs b/read-fonts/src/tables/postscript/scale.rs index 5d14c6a17..113d787ee 100644 --- a/read-fonts/src/tables/postscript/scale.rs +++ b/read-fonts/src/tables/postscript/scale.rs @@ -145,8 +145,8 @@ impl<'a> Scaler<'a> { /// /// Before calling this method, use [`subfont_index`](Self::subfont_index) /// to retrieve the subfont index for the desired glyph and then - /// [`subfont_instance`](Self::subfont_instance) to create an instance of - /// the subfont for a particular size and location in variation space. + /// [`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. /// From 25a62543cf122ffcf5e6be9f935b1cdead4e0e3e Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 5 Jul 2023 14:49:44 -0400 Subject: [PATCH 07/10] add an example to Scaler docs --- read-fonts/src/tables/postscript/scale.rs | 35 ++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/read-fonts/src/tables/postscript/scale.rs b/read-fonts/src/tables/postscript/scale.rs index 113d787ee..f2427bf29 100644 --- a/read-fonts/src/tables/postscript/scale.rs +++ b/read-fonts/src/tables/postscript/scale.rs @@ -12,7 +12,40 @@ use crate::{ FontData, FontRead, TableProvider, }; -/// State for reading and scaling glyph outlines from CFF/CFF2 tables. +/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. +/// +/// # 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 with_hinting = false; +/// let subfont = scaler.subfont(subfont_index, size, coords, with_hinting)?; +/// // Scale the outline using our configured subfont and emit the +/// // result to the given pen. +/// scaler.outline(&subfont, glyph_id, coords, pen)?; +/// # Ok(()) +/// # } +/// ``` pub struct Scaler<'a> { version: Version<'a>, top_dict: ScalerTopDict<'a>, From d281c3db8dc13a9e4ed12ac113cd52731dab3abc Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 11 Jul 2023 14:46:40 -0400 Subject: [PATCH 08/10] review feedback - speculate on why we do the scaling factor fance - also why we filter some path elements - rename SimplifyingSink -> NopFilteringSink - notes and citations for dealing with FDSelect - rephrase confusing language for private dict range - links to spec for Version enum variants - suggest using skrifa as a higher level interface - better names for tests --- .../src/tables/postscript/charstring.rs | 19 ++++++--- read-fonts/src/tables/postscript/scale.rs | 40 ++++++++++++++----- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/read-fonts/src/tables/postscript/charstring.rs b/read-fonts/src/tables/postscript/charstring.rs index 0ea747e62..4911b4cd4 100644 --- a/read-fonts/src/tables/postscript/charstring.rs +++ b/read-fonts/src/tables/postscript/charstring.rs @@ -96,7 +96,9 @@ impl<'a, S> ScalingSink26Dot6<'a, S> { 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: + // 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); @@ -156,17 +158,22 @@ impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> { } } -/// Command sink adapter that simplifies path operations. +/// Command sink adapter that supresses degenerate move and line commands. /// -/// This currently removes degenerate moves and lines. -pub(crate) struct SimplifyingSink<'a, S> { +/// 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 +pub(crate) struct NopFilteringSink<'a, S> { start: Option<(Fixed, Fixed)>, last: Option<(Fixed, Fixed)>, pending_move: Option<(Fixed, Fixed)>, inner: &'a mut S, } -impl<'a, S> SimplifyingSink<'a, S> +impl<'a, S> NopFilteringSink<'a, S> where S: CommandSink, { @@ -202,7 +209,7 @@ where } } -impl<'a, S> CommandSink for SimplifyingSink<'a, S> +impl<'a, S> CommandSink for NopFilteringSink<'a, S> where S: CommandSink, { diff --git a/read-fonts/src/tables/postscript/scale.rs b/read-fonts/src/tables/postscript/scale.rs index f2427bf29..fd6d2cf84 100644 --- a/read-fonts/src/tables/postscript/scale.rs +++ b/read-fonts/src/tables/postscript/scale.rs @@ -14,6 +14,10 @@ use crate::{ /// 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 @@ -112,12 +116,20 @@ impl<'a> Scaler<'a> { /// 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)) - // Missing FDSelect assumes either a single Font DICT at index 0, - // or a Private DICT entry in the Top DICT. .unwrap_or(0) as u32 } @@ -201,7 +213,7 @@ impl<'a> Scaler<'a> { 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 = charstring::SimplifyingSink::new(&mut pen_sink); + let mut simplifying_adapter = charstring::NopFilteringSink::new(&mut pen_sink); let scale = if subfont.size <= 0.0 { Fixed::ONE } else { @@ -240,7 +252,8 @@ impl<'a> Scaler<'a> { 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 index, use that + // 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) { @@ -251,7 +264,8 @@ impl<'a> Scaler<'a> { } range } else { - // Otherwise, assume the top dict provided a private dict range + // Last chance, use the private dict range from the top dict if + // available. self.top_dict.private_dict_range.clone() } .ok_or(Error::MissingPrivateDict) @@ -259,7 +273,9 @@ impl<'a> Scaler<'a> { } enum Version<'a> { + /// Version1(Cff<'a>), + /// Version2(Cff2<'a>), } @@ -411,7 +427,7 @@ mod tests { } #[test] - fn read_noto_serif_display() { + 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()); @@ -441,7 +457,7 @@ mod tests { } #[test] - fn read_cantarell_vf() { + 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()); @@ -482,7 +498,7 @@ mod tests { } #[test] - fn cantarell_vf_outlines() { + fn cff2_variable_outlines_match_freetype() { compare_glyphs( font_test_data::CANTARELL_VF_TRIMMED, font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, @@ -490,13 +506,19 @@ mod tests { } #[test] - fn noto_serif_display_outlines() { + 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 = crate::scaler_test::parse_glyph_outlines(expected_outlines); From c75229f6e1a745bcf2c32e941493124a7004faa1 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 8 Aug 2023 13:03:47 -0400 Subject: [PATCH 09/10] Move CFF scaler implementation to skrifa --- read-fonts/src/tables/postscript.rs | 2 - .../src/tables/postscript/charstring.rs | 180 -------- skrifa/src/scale/cff/hint.rs | 49 +++ skrifa/src/scale/cff/mod.rs | 9 + .../src/scale/cff/scaler.rs | 388 +++++++++++------- skrifa/src/scale/mod.rs | 1 + skrifa/src/scale/scaler.rs | 15 +- 7 files changed, 307 insertions(+), 337 deletions(-) create mode 100644 skrifa/src/scale/cff/hint.rs create mode 100644 skrifa/src/scale/cff/mod.rs rename read-fonts/src/tables/postscript/scale.rs => skrifa/src/scale/cff/scaler.rs (67%) diff --git a/read-fonts/src/tables/postscript.rs b/read-fonts/src/tables/postscript.rs index a1ffe2284..3398c2c6d 100644 --- a/read-fonts/src/tables/postscript.rs +++ b/read-fonts/src/tables/postscript.rs @@ -5,7 +5,6 @@ use std::fmt; mod blend; mod fd_select; mod index; -mod scale; mod stack; mod string; @@ -16,7 +15,6 @@ include!("../../generated/generated_postscript.rs"); pub use blend::BlendState; pub use index::Index; -pub use scale::{Scaler, ScalerSubfont}; pub use stack::{Number, Stack}; pub use string::{Latin1String, StringId, STANDARD_STRINGS}; diff --git a/read-fonts/src/tables/postscript/charstring.rs b/read-fonts/src/tables/postscript/charstring.rs index 4911b4cd4..f0bb8a530 100644 --- a/read-fonts/src/tables/postscript/charstring.rs +++ b/read-fonts/src/tables/postscript/charstring.rs @@ -78,186 +78,6 @@ where } } -/// 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. -pub(crate) struct ScalingSink26Dot6<'a, S> { - inner: &'a mut S, - scale: Fixed, -} - -impl<'a, S> ScalingSink26Dot6<'a, S> { - pub 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 -pub(crate) 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, -{ - pub 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() - } - } -} - /// Evaluates the given charstring and emits the resulting commands to the /// specified sink. /// diff --git a/skrifa/src/scale/cff/hint.rs b/skrifa/src/scale/cff/hint.rs new file mode 100644 index 000000000..c81572135 --- /dev/null +++ b/skrifa/src/scale/cff/hint.rs @@ -0,0 +1,49 @@ +//! CFF hinting. + +use read_fonts::{tables::postscript::dict::Blues, types::Fixed}; + +/// 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 { + // TODO +} + +impl HintState { + pub fn new(_params: &HintParams, _scale: Fixed) -> Self { + Self {} + } +} diff --git a/skrifa/src/scale/cff/mod.rs b/skrifa/src/scale/cff/mod.rs new file mode 100644 index 000000000..a90c86606 --- /dev/null +++ b/skrifa/src/scale/cff/mod.rs @@ -0,0 +1,9 @@ +//! Support for scaling CFF outlines. + +// Temporary until new scaler API is done. +#![allow(dead_code)] + +mod hint; +mod scaler; + +pub(crate) use scaler::{Scaler, Subfont}; diff --git a/read-fonts/src/tables/postscript/scale.rs b/skrifa/src/scale/cff/scaler.rs similarity index 67% rename from read-fonts/src/tables/postscript/scale.rs rename to skrifa/src/scale/cff/scaler.rs index fd6d2cf84..a47adaab7 100644 --- a/read-fonts/src/tables/postscript/scale.rs +++ b/skrifa/src/scale/cff/scaler.rs @@ -2,16 +2,22 @@ use std::ops::Range; -use super::{ - dict::{self, Blues}, - BlendState, Error, FdSelect, Index, -}; -use crate::{ - tables::{cff::Cff, cff2::Cff2, variations::ItemVariationStore}, +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, + 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 @@ -29,30 +35,9 @@ use crate::{ /// 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 with_hinting = false; -/// let subfont = scaler.subfont(subfont_index, size, coords, with_hinting)?; -/// // Scale the outline using our configured subfont and emit the -/// // result to the given pen. -/// scaler.outline(&subfont, glyph_id, coords, pen)?; -/// # Ok(()) -/// # } -/// ``` -pub struct Scaler<'a> { +pub(crate) struct Scaler<'a> { version: Version<'a>, - top_dict: ScalerTopDict<'a>, + top_dict: TopDict<'a>, units_per_em: u16, } @@ -75,13 +60,9 @@ impl<'a> Scaler<'a> { } } - pub fn from_cff( - cff1: Cff<'a>, - top_dict_index: usize, - units_per_em: u16, - ) -> Result { + 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)?; + let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; Ok(Self { version: Version::Version1(cff1), top_dict, @@ -89,9 +70,9 @@ impl<'a> Scaler<'a> { }) } - pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result { + 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)?; + let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true)?; Ok(Self { version: Version::Version2(cff2), top_dict, @@ -138,13 +119,7 @@ impl<'a> Scaler<'a> { /// /// 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], - with_hinting: bool, - ) -> Result { + 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(); @@ -173,14 +148,21 @@ impl<'a> Scaler<'a> { _ => {} } } - // TODO: convert hint params to zones if hinting is requested - let _ = with_hinting; - Ok(ScalerSubfont { + 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(Subfont { is_cff2: self.is_cff2(), index, - size, + _size: size, + scale, subrs_offset, - hint_params, + _hint_state: hint_state, store_index, }) } @@ -198,33 +180,25 @@ impl<'a> Scaler<'a> { /// The result is emitted to the specified pen. pub fn outline( &self, - subfont: &ScalerSubfont, + subfont: &Subfont, glyph_id: GlyphId, coords: &[F2Dot14], + _hint: bool, pen: &mut impl Pen, ) -> Result<(), Error> { - use super::charstring; let charstring_data = self .top_dict .charstrings .as_ref() - .ok_or(Error::MissingCharstrings)? + .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 = charstring::NopFilteringSink::new(&mut pen_sink); - let scale = if subfont.size <= 0.0 { - Fixed::ONE - } else { - // Note: we do an intermediate scale to 26.6 to ensure we - // match FreeType - Fixed::from_bits((subfont.size * 64.) as i32) - / Fixed::from_bits(self.units_per_em as i32) - }; - let mut scaling_adapter = - charstring::ScalingSink26Dot6::new(&mut simplifying_adapter, scale); - // TODO: hinting will be another sink adapter that slots in here + let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink); + let mut scaling_adapter = ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale); charstring::evaluate( charstring_data, self.global_subrs(), @@ -268,7 +242,9 @@ impl<'a> Scaler<'a> { // available. self.top_dict.private_dict_range.clone() } - .ok_or(Error::MissingPrivateDict) + .ok_or(Error::Read(ReadError::MalformedData( + "missing Private DICT in CFF table", + ))) } } @@ -287,27 +263,21 @@ enum Version<'a> { /// /// For variable fonts, this is dependent on a location in variation space. #[derive(Clone)] -pub struct ScalerSubfont { +pub(crate) struct Subfont { is_cff2: bool, index: u32, - size: f32, + _size: f32, + scale: Fixed, subrs_offset: Option, - // TODO: just capturing these for now. We'll soon compute the actual - // hinting state ("blue zones") from these values. - #[allow(dead_code)] - hint_params: HintParams, + _hint_state: HintState, store_index: u16, } -impl ScalerSubfont { +impl Subfont { 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 { @@ -334,40 +304,10 @@ impl ScalerSubfont { } } -/// Parameters used to generate the stem and counter zones for the hinting -/// algorithm. -#[derive(Clone)] -pub 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, - } - } -} - /// Entries that we parse from the Top DICT that are required to support /// charstring evaluation. #[derive(Default)] -struct ScalerTopDict<'a> { +struct TopDict<'a> { charstrings: Option>, font_dicts: Option>, fd_select: Option>, @@ -375,9 +315,9 @@ struct ScalerTopDict<'a> { var_store: Option>, } -impl<'a> ScalerTopDict<'a> { +impl<'a> TopDict<'a> { fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result { - let mut items = ScalerTopDict::default(); + let mut items = TopDict::default(); for entry in dict::entries(top_dict_data, None) { match entry? { dict::Entry::CharstringsOffset(offset) => { @@ -413,18 +353,196 @@ impl<'a> ScalerTopDict<'a> { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::FontRef; +/// 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 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)); + 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; #[test] fn read_cff_static() { @@ -438,22 +556,6 @@ mod tests { 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] @@ -468,16 +570,6 @@ mod tests { 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] @@ -521,9 +613,9 @@ mod tests { /// fonts), locations in variation space. fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { let font = FontRef::new(font_data).unwrap(); - let outlines = crate::scaler_test::parse_glyph_outlines(expected_outlines); + let outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines); let scaler = super::Scaler::new(&font).unwrap(); - let mut path = crate::scaler_test::Path { + let mut path = read_fonts::scaler_test::Path { elements: vec![], is_cff: true, }; @@ -537,7 +629,6 @@ mod tests { scaler.subfont_index(expected_outline.glyph_id), expected_outline.size, &expected_outline.coords, - false, ) .unwrap(); scaler @@ -545,6 +636,7 @@ mod tests { &subfont, expected_outline.glyph_id, &expected_outline.coords, + false, &mut path, ) .unwrap(); diff --git a/skrifa/src/scale/mod.rs b/skrifa/src/scale/mod.rs index a4360e02b..1dd5bb5a5 100644 --- a/skrifa/src/scale/mod.rs +++ b/skrifa/src/scale/mod.rs @@ -144,6 +144,7 @@ //! [lyon](https://github.com/nical/lyon) or //! [pathfinder](https://github.com/servo/pathfinder) for GPU rendering. +mod cff; mod error; mod scaler; diff --git a/skrifa/src/scale/scaler.rs b/skrifa/src/scale/scaler.rs index 65032d56f..815fc9803 100644 --- a/skrifa/src/scale/scaler.rs +++ b/skrifa/src/scale/scaler.rs @@ -1,11 +1,12 @@ -use super::{glyf, Context, Error, NormalizedCoord, Pen, Result, Size, UniqueId, VariationSetting}; +use super::{ + cff, glyf, Context, Error, NormalizedCoord, Pen, Result, Size, UniqueId, VariationSetting, +}; #[cfg(feature = "hinting")] use super::Hinting; use core::borrow::Borrow; use read_fonts::{ - tables::postscript::{Scaler as PostScriptScaler, ScalerSubfont}, types::{Fixed, GlyphId}, TableProvider, }; @@ -135,10 +136,10 @@ impl<'a> ScalerBuilder<'a> { ) { Some(Outlines::TrueType(glyf, &mut self.context.glyf_outline)) } else { - PostScriptScaler::new(font) + cff::Scaler::new(font) .ok() .and_then(|scaler| { - let first_subfont = scaler.subfont(0, size, coords, false).ok()?; + let first_subfont = scaler.subfont(0, size, coords).ok()?; Some((scaler, first_subfont)) }) .map(|(scaler, subfont)| Outlines::PostScript(scaler, subfont)) @@ -227,7 +228,7 @@ impl<'a> Scaler<'a> { #[allow(clippy::large_enum_variant)] enum Outlines<'a> { TrueType(glyf::Scaler<'a>, &'a mut glyf::Outline), - PostScript(PostScriptScaler<'a>, ScalerSubfont), + PostScript(cff::Scaler<'a>, cff::Subfont), } impl<'a> Outlines<'a> { @@ -246,9 +247,9 @@ impl<'a> Outlines<'a> { Self::PostScript(scaler, subfont) => { let subfont_index = scaler.subfont_index(glyph_id); if subfont_index != subfont.index() { - *subfont = scaler.subfont(subfont_index, size, coords, false)?; + *subfont = scaler.subfont(subfont_index, size, coords)?; } - Ok(scaler.outline(subfont, glyph_id, coords, pen)?) + Ok(scaler.outline(subfont, glyph_id, coords, false, pen)?) } } } From 9d8bac6eb823569faeb7da3559e59c45f4de8adf Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 8 Aug 2023 13:12:02 -0400 Subject: [PATCH 10/10] restore specific error variants --- skrifa/src/scale/cff/scaler.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/skrifa/src/scale/cff/scaler.rs b/skrifa/src/scale/cff/scaler.rs index a47adaab7..2d3afa29d 100644 --- a/skrifa/src/scale/cff/scaler.rs +++ b/skrifa/src/scale/cff/scaler.rs @@ -13,7 +13,7 @@ use read_fonts::{ variations::ItemVariationStore, }, types::{F2Dot14, Fixed, GlyphId, Pen}, - FontData, FontRead, ReadError, TableProvider, + FontData, FontRead, TableProvider, }; use super::hint::{HintParams, HintState}; @@ -190,9 +190,7 @@ impl<'a> Scaler<'a> { .top_dict .charstrings .as_ref() - .ok_or(Error::Read(ReadError::MalformedData( - "missing charstrings INDEX in CFF table", - )))? + .ok_or(Error::MissingCharstrings)? .get(glyph_id.to_u16() as usize)?; let subrs = subfont.subrs(self)?; let blend_state = subfont.blend_state(self, coords)?; @@ -242,9 +240,7 @@ impl<'a> Scaler<'a> { // available. self.top_dict.private_dict_range.clone() } - .ok_or(Error::Read(ReadError::MalformedData( - "missing Private DICT in CFF table", - ))) + .ok_or(Error::MissingPrivateDict) } }