//! Subsetting a real TrueType font. //! //! The unit tests in `subset.rs` cover refusals and edge cases with //! hand-built table stubs. They cannot catch the failure that actually //! happened: a subset that is *structurally valid* — correct table //! directory, checksums, every glyph decoding — and whose glyphs are in the //! wrong place. Writing a zero left side bearing shifted every outline //! horizontally, and `hmtx` was indexed by new glyph id against the //! original font, so glyphs took other glyphs' metrics. //! //! So these tests assert **geometry against the source font**, not that the //! subset parses. //! //! Skipped when the system font is absent rather than failing, because the //! font is a property of the machine and not of this crate. CI installs //! `fonts-dejavu-core`. //! //! See `REVIEWS/adr/0019-pdf-document-creation.md`. use std::collections::BTreeSet; use nigig_pdf_graphics::sfnt::SfntFont; use nigig_pdf_graphics::subset::{glyph_index, subset_truetype, SubsetError}; const FONT: &str = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; fn font_bytes() -> Option> { std::fs::read(FONT).ok() } /// Read a glyph's advance and left side bearing straight from a font's /// `hmtx`, independently of the subsetter. fn metrics_of(data: &[u8], gid: u16) -> (u16, i16) { let font = SfntFont::parse(data).expect("parses"); let metrics = font.metrics().expect("metrics"); ( font.advance(gid, &metrics).expect("advance"), font.left_side_bearing(gid, &metrics).expect("lsb"), ) } #[test] fn a_subset_keeps_only_the_glyphs_asked_for() { let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "Hello".chars().collect(); let subset = subset_truetype(&data, &chars).expect("subsets"); // H, e, l, o plus .notdef. No composites are involved here, so the // count is exact. assert_eq!( subset.glyph_map.len(), 5, "expected .notdef plus four distinct letters, got {:?}", subset.to_unicode ); assert!( subset.data.len() < data.len() / 50, "a five-glyph subset of a 750 KB font should be tiny, got {} bytes", subset.data.len() ); // .notdef must survive: a TrueType font without glyph 0 is invalid. assert_eq!(subset.glyph_map.get(&0), Some(&0)); } #[test] fn every_subset_glyph_keeps_its_advance_and_bearing() { // This is the regression. The outlines were intact and every letter // was drawn shifted left by its own left side bearing, because the // rebuilt hmtx wrote a zero bearing and read advances by the new glyph // id from the *original* font. let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "HWdelor,!".chars().collect(); let subset = subset_truetype(&data, &chars).expect("subsets"); for (&old_gid, &new_gid) in &subset.glyph_map { let expected = metrics_of(&data, old_gid); let actual = metrics_of(&subset.data, new_gid); assert_eq!( expected, actual, "glyph {old_gid} -> {new_gid}: expected (advance, lsb) {expected:?}, got {actual:?}" ); } } #[test] fn a_composite_glyph_brings_its_components_with_it() { // "é" is a composite of "e" and an accent. Subsetting without // following the reference leaves a glyph that renders as nothing. let Some(data) = font_bytes() else { return }; let mut chars = BTreeSet::new(); chars.insert('é'); let subset = subset_truetype(&data, &chars).expect("subsets"); assert!( subset.glyph_map.len() > 2, "é is composite, so its components must be kept too; got {:?}", subset.glyph_map ); // Each component named inside the subset must exist in the subset. let font = SfntFont::parse(&subset.data).expect("subset parses"); let maxp = font.table(b"maxp").expect("maxp"); let num_glyphs = u16::from_be_bytes([maxp[4], maxp[5]]); assert_eq!(num_glyphs as usize, subset.glyph_map.len()); } #[test] fn widths_are_in_pdf_thousandths_and_match_the_source() { let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "Hi".chars().collect(); let subset = subset_truetype(&data, &chars).expect("subsets"); let source = SfntFont::parse(&data).expect("parses"); let metrics = source.metrics().expect("metrics"); let scale = 1000.0 / metrics.units_per_em as f64; for (&old_gid, &new_gid) in &subset.glyph_map { let expected = source.advance(old_gid, &metrics).expect("advance") as f64 * scale; let actual = subset.widths[new_gid as usize]; assert!( (expected - actual).abs() < 0.05, "glyph {new_gid}: expected width {expected:.2}, got {actual:.2}" ); } // A width of zero for a visible letter is the silent-empty failure. let h = subset.glyph_for('H').expect("H is in the subset"); assert!( subset.widths[h as usize] > 100.0, "a capital H cannot be {} units wide", subset.widths[h as usize] ); } #[test] fn identity_h_encoding_is_two_bytes_per_glyph() { let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "AB".chars().collect(); let subset = subset_truetype(&data, &chars).expect("subsets"); let encoded = subset.encode("AB"); assert_eq!(encoded.len(), 4, "two glyphs, two bytes each"); let a = subset.glyph_for('A').expect("A present"); let b = subset.glyph_for('B').expect("B present"); assert_eq!( encoded, vec![(a >> 8) as u8, a as u8, (b >> 8) as u8, b as u8] ); } #[test] fn a_character_outside_the_subset_is_dropped_not_rendered_as_notdef() { let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "A".chars().collect(); let subset = subset_truetype(&data, &chars).expect("subsets"); assert!(subset.glyph_for('Z').is_none()); // "AZA" keeps only the two A's rather than emitting .notdef boxes. assert_eq!(subset.encode("AZA").len(), 4); } #[test] fn text_width_sums_the_glyph_advances() { let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "il".chars().collect(); let subset = subset_truetype(&data, &chars).expect("subsets"); let one = subset.text_width("i", 12.0); let two = subset.text_width("ii", 12.0); assert!(one > 0.0, "a glyph must have a width"); assert!( (two - one * 2.0).abs() < 1e-9, "two of the same glyph must be twice as wide" ); // Scaling with the point size. assert!((subset.text_width("i", 24.0) - one * 2.0).abs() < 1e-9); } #[test] fn the_subset_is_byte_identical_between_runs() { // A generated PDF that differs run to run cannot be diffed in review // or cached. The subset tag is derived from the glyph set for exactly // this reason. let Some(data) = font_bytes() else { return }; let chars: BTreeSet = "Reproducible".chars().collect(); let first = subset_truetype(&data, &chars).expect("subsets"); let second = subset_truetype(&data, &chars).expect("subsets"); assert_eq!(first.data, second.data); } #[test] fn glyph_lookup_finds_a_known_character() { let Some(data) = font_bytes() else { return }; let font = SfntFont::parse(&data).expect("parses"); // Assert against a value read from the font, not a hardcoded id. let a = glyph_index(&font, 'A').expect("A must be in DejaVu Sans"); let b = glyph_index(&font, 'B').expect("B must be in DejaVu Sans"); assert_ne!(a, 0, "A must not map to .notdef"); assert_ne!(a, b, "different characters need different glyphs"); // A character DejaVu Sans does not contain. assert!(glyph_index(&font, '\u{10FFFF}').is_none()); } #[test] fn subsetting_a_cff_font_is_refused_rather_than_silently_wrong() { // Nothing on this machine is guaranteed to be CFF, so this drives the // refusal through the public entry point with a synthetic OTTO header. let mut data = Vec::new(); data.extend_from_slice(b"OTTO"); data.extend_from_slice(&1u16.to_be_bytes()); data.extend_from_slice(&[0; 6]); data.extend_from_slice(b"CFF "); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&28u32.to_be_bytes()); data.extend_from_slice(&4u32.to_be_bytes()); data.extend_from_slice(&[0; 4]); assert_eq!( subset_truetype(&data, &BTreeSet::new()).unwrap_err(), SubsetError::CffNotSupported ); }