diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f4725dab9..6d67feb87 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -187,4 +187,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: fauntlet compare - run: cargo run --release -p fauntlet -- compare-glyphs --hinting-engine all --hinting-target all --exit-on-fail fauntlet/test_fonts/*.*tf + run: cargo run --release -p fauntlet -- compare-glyphs --print-paths --hinting-engine all --hinting-target all --exit-on-fail fauntlet/test_fonts/*.*tf + - name: fauntlet compare type1 + run: cargo run --release -p fauntlet -- compare-glyphs --print-paths --exit-on-fail font-test-data/test_data/type1/*.pf* + diff --git a/fauntlet/src/font/mod.rs b/fauntlet/src/font/mod.rs index b06c23976..d4b051d4c 100644 --- a/fauntlet/src/font/mod.rs +++ b/fauntlet/src/font/mod.rs @@ -7,7 +7,7 @@ use std::{ use ::freetype::{face::LoadFlag, Library}; use ::skrifa::{ outline::{HintingOptions, SmoothMode, Target}, - raw::{types::F2Dot14, FontRef, TableProvider}, + raw::{tables::postscript::font::Type1Font, types::F2Dot14, FontRef, TableProvider}, }; mod freetype; @@ -101,6 +101,7 @@ impl<'a> InstanceOptions<'a> { pub struct Font { path: PathBuf, data: SharedFontData, + pub(crate) type1: Option, count: usize, ft_library: Library, } @@ -110,11 +111,21 @@ impl Font { let path = path.as_ref().to_owned(); let file = std::fs::File::open(&path).ok()?; let data = SharedFontData(unsafe { Arc::new(memmap2::Mmap::map(&file).ok()?) }); - let count = FontRef::fonts(data.0.as_ref()).count(); + let type1 = if check_type1(&data.0) { + Some(Type1Font::new(&data.0).ok()?) + } else { + None + }; + let count = if type1.is_some() { + 1 + } else { + FontRef::fonts(data.0.as_ref()).count() + }; let _ft_library = ::freetype::Library::init().ok()?; Some(Self { path, data, + type1, count, ft_library: _ft_library, }) @@ -124,6 +135,10 @@ impl Font { &self.path } + pub fn is_type1(&self) -> bool { + self.type1.is_some() + } + pub fn count(&self) -> usize { self.count } @@ -145,7 +160,7 @@ impl Font { options: &InstanceOptions, ) -> Option<(FreeTypeInstance, SkrifaInstance<'_>)> { let ft_instance = FreeTypeInstance::new(&self.ft_library, &self.data, options)?; - let skrifa_instance = SkrifaInstance::new(&self.data, options)?; + let skrifa_instance = SkrifaInstance::new(self, options)?; Some((ft_instance, skrifa_instance)) } } @@ -158,3 +173,10 @@ impl Borrow<[u8]> for SharedFontData { self.0.as_ref() } } + +fn check_type1(data: &[u8]) -> bool { + fn check(data: &[u8]) -> bool { + data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType") + } + check(data) || data.get(6..).map(check).unwrap_or(false) +} diff --git a/fauntlet/src/font/skrifa.rs b/fauntlet/src/font/skrifa.rs index cead40bf3..695c986fa 100644 --- a/fauntlet/src/font/skrifa.rs +++ b/fauntlet/src/font/skrifa.rs @@ -1,15 +1,78 @@ -use ::skrifa::{ - outline::{DrawError, DrawSettings, HintingInstance, OutlinePen}, +use super::{InstanceOptions, SharedFontData}; +use crate::Font; +use skrifa::{ + outline::{DrawError, DrawSettings, HintingInstance, HintingOptions, OutlinePen}, prelude::{LocationRef, Size}, - raw::types::F2Dot14, - raw::{FontRef, TableProvider}, + raw::{ + tables::postscript::{ + charstring::{CommandSink, NopFilterSink, TransformSink}, + font::Type1Font, + }, + types::{F2Dot14, Fixed}, + FontRef, TableProvider, + }, GlyphId, MetadataProvider, OutlineGlyphCollection, }; -use skrifa::outline::HintingOptions; -use super::{InstanceOptions, SharedFontData}; +pub enum SkrifaInstance<'a> { + Sfnt(SkrifaSfntInstance<'a>), + Type1(SkrifaType1Instance<'a>), +} + +impl<'a> SkrifaInstance<'a> { + pub fn new(font: &'a Font, options: &InstanceOptions) -> Option { + if let Some(type1) = font.type1.as_ref() { + Some(Self::Type1(SkrifaType1Instance { + font: type1, + ppem: (options.ppem != 0.0).then_some(options.ppem), + })) + } else { + SkrifaSfntInstance::new(&font.data, options).map(Self::Sfnt) + } + } + + pub fn is_tricky(&self) -> bool { + match self { + Self::Sfnt(sfnt) => sfnt.is_tricky(), + _ => false, + } + } + + pub fn glyph_count(&self) -> u16 { + match self { + Self::Sfnt(sfnt) => sfnt.glyph_count(), + Self::Type1(type1) => type1.font.num_glyphs() as _, + } + } + + pub fn advance(&mut self, glyph_id: GlyphId) -> Option { + match self { + Self::Sfnt(sfnt) => sfnt.advance(glyph_id), + _ => None, + } + } + + pub fn hvar_and_gvar_advance_deltas(&self, glyph_id: GlyphId) -> Option<(i32, i32)> { + match self { + Self::Sfnt(sfnt) => sfnt.hvar_and_gvar_advance_deltas(glyph_id), + _ => None, + } + } + + /// Returns the scaler adjusted advance width when available. + pub fn outline( + &mut self, + glyph_id: GlyphId, + pen: &mut impl OutlinePen, + ) -> Result, DrawError> { + match self { + Self::Sfnt(sfnt) => sfnt.outline(glyph_id, pen), + Self::Type1(type1) => type1.outline(glyph_id, pen), + } + } +} -pub struct SkrifaInstance<'a> { +pub struct SkrifaSfntInstance<'a> { font: FontRef<'a>, size: Size, coords: Vec, @@ -17,7 +80,7 @@ pub struct SkrifaInstance<'a> { hinter: Option, } -impl<'a> SkrifaInstance<'a> { +impl<'a> SkrifaSfntInstance<'a> { pub fn new(data: &'a SharedFontData, options: &InstanceOptions) -> Option { let font = FontRef::from_index(data.0.as_ref(), options.index as u32).ok()?; let size = if options.ppem != 0.0 { @@ -49,7 +112,7 @@ impl<'a> SkrifaInstance<'a> { } else { None }; - Some(SkrifaInstance { + Some(Self { font, size, coords: options.coords.into(), @@ -111,3 +174,52 @@ impl<'a> SkrifaInstance<'a> { .map(|metrics| metrics.advance_width) } } + +pub struct SkrifaType1Instance<'a> { + font: &'a Type1Font, + ppem: Option, +} + +impl SkrifaType1Instance<'_> { + pub fn outline( + &mut self, + glyph_id: GlyphId, + pen: &mut impl OutlinePen, + ) -> Result, DrawError> { + let mut pen = PenCommandSink(pen); + let mut nop_filter = NopFilterSink::new(&mut pen); + let scale = self.ppem.map(|ppem| self.font.scale_for_ppem(ppem)); + let mut transformer = TransformSink::new(&mut nop_filter, self.font.matrix(), scale); + self.font + .evaluate_charstring(glyph_id, &mut transformer) + .map_err(DrawError::PostScript)?; + Ok(None) + } +} + +struct PenCommandSink<'a, P: OutlinePen>(&'a mut P); + +impl CommandSink for PenCommandSink<'_, P> { + fn move_to(&mut self, x: Fixed, y: Fixed) { + 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_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_f32(), + cy0.to_f32(), + cx1.to_f32(), + cy1.to_f32(), + x.to_f32(), + y.to_f32(), + ); + } + + fn close(&mut self) { + self.0.close() + } +} diff --git a/fauntlet/src/main.rs b/fauntlet/src/main.rs index 1b2058114..10b9b42c4 100644 --- a/fauntlet/src/main.rs +++ b/fauntlet/src/main.rs @@ -96,10 +96,10 @@ fn main() { use std::sync::atomic::{AtomicBool, Ordering}; let ok = AtomicBool::new(true); files.par_iter().for_each(|font_path| { - if print_paths { - writeln!(std::io::stdout(), "[{font_path:?}]").unwrap(); - } if let Some(mut font_data) = fauntlet::Font::new(font_path) { + if print_paths { + writeln!(std::io::stdout(), "[{font_path:?}]").unwrap(); + } for font_ix in 0..font_data.count() { for ppem in &ppem_sizes { let axis_count = font_data.axis_count(font_ix) as usize; @@ -135,7 +135,12 @@ fn main() { } } } else { - for hinting in &hinting_matrix { + let hinting_matrix = if font_data.is_type1() { + &[Hinting::None] + } else { + hinting_matrix.as_slice() + }; + for hinting in hinting_matrix { let options = InstanceOptions::new(font_ix, *ppem, &[], *hinting); if print_instances { @@ -156,6 +161,8 @@ fn main() { ) { ok.store(false, Ordering::Release); } + } else { + writeln!(std::io::stdout(), "warning: failed to load font instances for {font_path:?}").unwrap(); } } } diff --git a/read-fonts/src/tables/postscript/font.rs b/read-fonts/src/tables/postscript/font.rs index 165aebb1c..2090ca259 100644 --- a/read-fonts/src/tables/postscript/font.rs +++ b/read-fonts/src/tables/postscript/font.rs @@ -1,11 +1,12 @@ //! Model for PostScript fonts. mod cff; -#[expect(unused)] #[cfg(feature = "std")] mod type1; pub use cff::{CffFontRef, CffSubfont}; +#[cfg(feature = "std")] +pub use type1::Type1Font; use super::dict::Blues; use types::Fixed; diff --git a/read-fonts/src/tables/postscript/font/type1.rs b/read-fonts/src/tables/postscript/font/type1.rs index cb3e42b9e..3b838ce31 100644 --- a/read-fonts/src/tables/postscript/font/type1.rs +++ b/read-fonts/src/tables/postscript/font/type1.rs @@ -156,7 +156,7 @@ impl CharstringContext for Type1Font { Ok(self.subrs.get(index as u32).ok_or(ReadError::OutOfBounds)?) } - fn global_subr(&self, index: i32) -> Result<&[u8], Error> { + fn global_subr(&self, _index: i32) -> Result<&[u8], Error> { // Type1 fonts don't have global subroutines Err(Error::MissingSubroutines) } @@ -178,7 +178,7 @@ impl<'a> RawDicts<'a> { verify_header(data)?; let (base_dict, raw_private_dict) = data.split_at_checked(base_size as usize)?; // Decrypt private dict segments - let mut private_dict = decrypt( + let private_dict = decrypt( decode_pfb_binary_segments(raw_private_dict) .flat_map(|segment| segment.iter().copied()), EEXEC_SEED, @@ -196,7 +196,7 @@ impl<'a> RawDicts<'a> { // Now find the start of the private dictionary let start = find_eexec_data(data)?; let (base_dict, raw_private_dict) = data.split_at_checked(start)?; - let mut private_dict = if raw_private_dict.len() > 3 + let private_dict = if raw_private_dict.len() > 3 && raw_private_dict[..4].iter().all(|b| b.is_ascii_hexdigit()) { // Hex decode and then decrypt @@ -598,7 +598,6 @@ impl<'a> Parser<'a> { } } } - None } fn peek(&self) -> Option> { @@ -792,7 +791,7 @@ impl Parser<'_> { /// See fn read_subrs(&mut self, len_iv: i64) -> Option { let mut subrs = Subrs::default(); - let num_subrs: usize = match self.next()? { + let _: usize = match self.next()? { Token::Raw(b"[") => { // Just an empty array self.expect(Token::Raw(b"]"))?; @@ -845,7 +844,7 @@ impl Parser<'_> { /// See fn read_charstrings(&mut self, len_iv: i64) -> Option { let mut charstrings = Charstrings::default(); - let num_glyphs: usize = match self.next()? { + let _: usize = match self.next()? { Token::Int(n) => n.try_into().ok()?, _ => return None, }; @@ -1594,7 +1593,7 @@ mod tests { break; } } - let mut charstrings = charstrings.unwrap(); + let charstrings = charstrings.unwrap(); assert_eq!(charstrings.num_glyphs(), 9); assert!(charstrings.orig_notdef_index.is_none()); let expected_names = [ @@ -1612,7 +1611,7 @@ mod tests { .map(|idx| charstrings.name(idx).unwrap()) .collect::>(); assert_eq!(names, expected_names); - /// Prefix (up to 8 bytes), extracted from FreeType + // Prefix (up to 8 bytes), extracted from FreeType let expected_charstrings_prefix: [&[u8]; 9] = [ &[139, 248, 236, 13, 14], &[177, 249, 173, 13, 139, 4, 247, 183],