diff --git a/read-fonts/src/tables/postscript.rs b/read-fonts/src/tables/postscript.rs index 2065323d4..3398c2c6d 100644 --- a/read-fonts/src/tables/postscript.rs +++ b/read-fonts/src/tables/postscript.rs @@ -34,6 +34,8 @@ pub enum Error { CharstringNestingDepthLimitExceeded, MissingSubroutines, MissingBlendState, + MissingPrivateDict, + MissingCharstrings, Read(ReadError), } @@ -98,7 +100,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..f0bb8a530 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(), ); } 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/skrifa/src/scale/cff/scaler.rs b/skrifa/src/scale/cff/scaler.rs new file mode 100644 index 000000000..2d3afa29d --- /dev/null +++ b/skrifa/src/scale/cff/scaler.rs @@ -0,0 +1,651 @@ +//! Scaler for CFF outlines. + +use std::ops::Range; + +use read_fonts::{ + tables::{ + cff::Cff, + cff2::Cff2, + postscript::{ + charstring::{self, CommandSink}, + dict, BlendState, Error, FdSelect, Index, + }, + variations::ItemVariationStore, + }, + types::{F2Dot14, Fixed, GlyphId, Pen}, + FontData, FontRead, TableProvider, +}; + +use super::hint::{HintParams, HintState}; + +/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. +/// +/// The skrifa crate provides a higher level interface for this that handles +/// caching and abstracting over the different outline formats. Consider using +/// that if detailed control over resources is not required. +/// +/// # Subfonts +/// +/// CFF tables can contain multiple logical "subfonts" which determine the +/// state required for processing some subset of glyphs. This state is +/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28) +/// operators to select an appropriate subfont for any given glyph identifier. +/// This process is exposed on this type with the +/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont +/// index for the requested glyph followed by using the +/// [`subfont`](Self::subfont) method to create an appropriately configured +/// subfont for that glyph. +pub(crate) 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) + } + } + + 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, + }) + } + + 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 subfonts. + pub fn subfont_count(&self) -> u32 { + self.top_dict + .font_dicts + .as_ref() + .map(|font_dicts| font_dicts.count()) + // All CFF fonts have at least one logical subfont. + .unwrap_or(1) + } + + /// Returns the subfont (or Font DICT) index for the given glyph + /// identifier. + pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 { + // For CFF tables, an FDSelect index will be present for CID-keyed + // fonts. Otherwise, the Top DICT will contain an entry for the + // "global" Private DICT. + // See + // + // CFF2 tables always contain a Font DICT and an FDSelect is only + // present if the size of the DICT is greater than 1. + // See + // + // In both cases, we return a subfont index of 0 when FDSelect is missing. + self.top_dict + .fd_select + .as_ref() + .and_then(|select| select.font_index(glyph_id)) + .unwrap_or(0) as u32 + } + + /// Creates a new subfont for the given index, size, normalized + /// variation coordinates and hinting state. + /// + /// The index of a subfont for a particular glyph can be retrieved with + /// the [`subfont_index`](Self::subfont_index) method. + pub fn subfont(&self, index: u32, size: f32, coords: &[F2Dot14]) -> Result { + let private_dict_range = self.private_dict_range(index)?; + let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?; + let mut hint_params = HintParams::default(); + let mut subrs_offset = None; + let mut store_index = 0; + let blend_state = self + .top_dict + .var_store + .clone() + .map(|store| BlendState::new(store, coords, store_index)) + .transpose()?; + for entry in dict::entries(private_dict_data, blend_state) { + use dict::Entry::*; + match entry? { + BlueValues(values) => hint_params.blues = values, + FamilyBlues(values) => hint_params.family_blues = values, + OtherBlues(values) => hint_params.other_blues = values, + FamilyOtherBlues(values) => hint_params.family_blues = values, + BlueScale(value) => hint_params.blue_scale = value, + BlueShift(value) => hint_params.blue_shift = value, + BlueFuzz(value) => hint_params.blue_fuzz = value, + LanguageGroup(group) => hint_params.language_group = group, + // Subrs offset is relative to the private DICT + SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset), + VariationStoreIndex(index) => store_index = index, + _ => {} + } + } + let scale = if size <= 0.0 { + Fixed::ONE + } else { + // Note: we do an intermediate scale to 26.6 to ensure we + // match FreeType + Fixed::from_bits((size * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32) + }; + let hint_state = HintState::new(&hint_params, scale); + Ok(Subfont { + is_cff2: self.is_cff2(), + index, + _size: size, + scale, + subrs_offset, + _hint_state: hint_state, + store_index, + }) + } + + /// Loads and scales an outline for the given subfont instance, glyph + /// identifier and normalized variation coordinates. + /// + /// Before calling this method, use [`subfont_index`](Self::subfont_index) + /// to retrieve the subfont index for the desired glyph and then + /// [`subfont`](Self::subfont) to create an instance of the subfont for a + /// particular size and location in variation space. + /// Creating subfont instances is not free, so this process is exposed in + /// discrete steps to allow for caching. + /// + /// The result is emitted to the specified pen. + pub fn outline( + &self, + subfont: &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); + 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) + } +} + +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, + index: u32, + _size: f32, + scale: Fixed, + subrs_offset: Option, + _hint_state: HintState, + store_index: u16, +} + +impl Subfont { + pub fn index(&self) -> u32 { + self.index + } + + /// Returns the local subroutine index. + pub fn subrs<'a>(&self, scaler: &Scaler<'a>) -> Result>, Error> { + if let Some(subrs_offset) = self.subrs_offset { + let offset_data = scaler.offset_data().as_bytes(); + let index_data = offset_data.get(subrs_offset..).unwrap_or_default(); + Ok(Some(Index::new(index_data, self.is_cff2)?)) + } else { + Ok(None) + } + } + + /// Creates a new blend state for the given normalized variation + /// coordinates. + pub fn blend_state<'a>( + &self, + scaler: &Scaler<'a>, + coords: &'a [F2Dot14], + ) -> Result>, Error> { + if let Some(var_store) = scaler.top_dict.var_store.clone() { + Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) + } else { + Ok(None) + } + } +} + +/// Entries that we parse from the Top DICT that are required to support +/// charstring evaluation. +#[derive(Default)] +struct 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 { + 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() { + 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); + } + + #[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); + } + + #[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/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..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; @@ -219,19 +220,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..815fc9803 100644 --- a/skrifa/src/scale/scaler.rs +++ b/skrifa/src/scale/scaler.rs @@ -1,4 +1,6 @@ -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; @@ -122,22 +124,30 @@ 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 + cff::Scaler::new(font) + .ok() + .and_then(|scaler| { + let first_subfont = scaler.subfont(0, size, coords).ok()?; + Some((scaler, first_subfont)) + }) + .map(|(scaler, subfont)| Outlines::PostScript(scaler, subfont)) }; Scaler { + size, coords, - outlines: Outlines { glyf }, + outlines, } } @@ -186,8 +196,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 +209,48 @@ 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)>, +// 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(cff::Scaler<'a>, cff::Subfont), } 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, subfont) => { + let subfont_index = scaler.subfont_index(glyph_id); + if subfont_index != subfont.index() { + *subfont = scaler.subfont(subfont_index, size, coords)?; + } + Ok(scaler.outline(subfont, glyph_id, coords, false, pen)?) + } } } }