//! 1:1 port of `dart-pdf/packages/pdf_graphics/test/cff_test.dart` //! (tranche 1: fixture-built font — structure, encoding, outlines, widths). //! //! `build_test_cff_font` is a line-for-line port of dart's //! `buildTestCffFont()`: glyph 0 = .notdef, glyph 1 = an 800x800 square at //! the origin mapped to code 65 ('A'), advance 660 via nominalWidthX 600 + //! leading operand 60. use nigig_pdf_graphics::cff::CffFont; use nigig_pdf_graphics::path::PdfPathSegment; fn u8(v: u8) -> Vec { vec![v] } fn u16(v: u16) -> Vec { vec![(v >> 8) as u8, (v & 0xff) as u8] } /// CFF DICT 5-byte integer (operator 29). fn int5(v: i32) -> Vec { vec![ 29, ((v >> 24) & 0xff) as u8, ((v >> 16) & 0xff) as u8, ((v >> 8) & 0xff) as u8, (v & 0xff) as u8, ] } fn join(parts: Vec>) -> Vec { parts.into_iter().flatten().collect() } fn index(items: Vec>) -> Vec { if items.is_empty() { return u16(0); } let mut out = u16(items.len() as u16); out.push(2); // offSize 2 let mut offset: u16 = 1; out.extend(u16(offset)); for item in &items { offset += item.len() as u16; out.extend(u16(offset)); } for item in items { out.extend(item); } out } fn build_test_cff_font() -> Vec { // charstrings: .notdef = endchar; square = 60 width, then contour. let notdef = vec![14u8]; let square = vec![ 139 + 60, // width delta 60 over nominalWidthX 139, 139, 21, // 0 0 rmoveto 249, 180, 6, // 800 hlineto 249, 180, 7, // 800 vlineto 253, 180, 6, // -800 hlineto 14, // endchar ]; let header = vec![1u8, 0, 4, 2]; let name_index = index(vec![b"Test".to_vec()]); let string_index = u16(0); let gsubr_index = u16(0); // encoding: format 0, one code: 65 -> gid 1. let encoding = vec![0u8, 1, 65]; let charstrings_index = index(vec![notdef, square]); // private dict: defaultWidthX 500 (20), nominalWidthX 600 (21). let private_dict = join(vec![int5(500), u8(20), int5(600), u8(21)]); // top dict (fixed-size operands so offsets are computable): // CharStrings (17), Encoding (16), Private (18). let top_dict_with = |charstrings_at: i32, encoding_at: i32, private_at: i32| -> Vec { join(vec![ int5(charstrings_at), u8(17), int5(encoding_at), u8(16), int5(private_dict.len() as i32), int5(private_at), u8(18), ]) }; let top_dict_size = top_dict_with(0, 0, 0).len(); let top_dict_index_size = index(vec![vec![0u8; top_dict_size]]).len(); let fixed_prefix = header.len() + name_index.len() + top_dict_index_size + string_index.len() + gsubr_index.len(); let encoding_at = fixed_prefix; let charstrings_at = encoding_at + encoding.len(); let private_at = charstrings_at + charstrings_index.len(); join(vec![ header, name_index, index(vec![top_dict_with( charstrings_at as i32, encoding_at as i32, private_at as i32, )]), string_index, gsubr_index, encoding, charstrings_index, private_dict, ]) } fn test_font() -> CffFont { CffFont::parse(&build_test_cff_font()).expect("test CFF parses") } #[test] fn parses_the_cff_structure() { let font = test_font(); assert_eq!(font.num_glyphs(), 2); assert!(!font.is_cid_keyed()); } #[test] fn encoding_maps_character_codes_to_glyphs() { let font = test_font(); assert_eq!(font.gid_for_code(65), 1); assert_eq!(font.gid_for_code(66), 0); } #[test] fn charstrings_interpret_to_em_unit_outlines() { let font = test_font(); let square = font.outline_for_glyph(1).expect("square outline"); let segs = square.segments(); let first = &segs[0]; assert!( matches!(first, PdfPathSegment::MoveTo { x, y } if x.abs() < 1e-9 && y.abs() < 1e-9), "first must be MoveTo(0,0), got {first:?}" ); // 800 font units at the default 0.001 matrix = 0.8 em. let lines: Vec<(f64, f64)> = segs .iter() .filter_map(|s| match s { PdfPathSegment::LineTo { x, y } => Some((*x, *y)), _ => None, }) .collect(); assert_eq!(lines.len(), 3, "square has 3 lines, got {segs:?}"); assert!((lines[0].0 - 0.8).abs() < 1e-9, "got {lines:?}"); assert!((lines[1].1 - 0.8).abs() < 1e-9, "got {lines:?}"); assert!( matches!(segs.last(), Some(PdfPathSegment::Close)), "last must be Close, got {segs:?}" ); } #[test] fn width_comes_from_the_leading_charstring_operand() { let font = test_font(); font.outline_for_glyph(1); // nominalWidthX 600 + operand 60 = 660 units = 0.66 em. let advance = font.advance_for_glyph(1).expect("advance"); assert!((advance - 0.66).abs() < 1e-9, "got {advance}"); } #[test] fn empty_glyphs_return_no_outline_but_a_default_width() { let font = test_font(); assert!(font.outline_for_glyph(0).is_none()); // .notdef has no width operand: defaultWidthX 500. let advance = font.advance_for_glyph(0).expect("advance"); assert!((advance - 0.5).abs() < 1e-9, "got {advance}"); } #[test] fn garbage_input_parses_to_none() { assert!(CffFont::parse(b"not a font").is_none()); } // ---------- tranche 2: seac composition (corpus) + OpenType CFF ---------- use nigig_pdf_cos::{PdfDict, PdfObj}; use nigig_pdf_document::PdfDocument; use nigig_pdf_graphics::cff::OpenTypeCffFont; /// The dart-pdf checkout this port tracks; corpus PDFs are read from it. /// Tests that need a corpus file skip (with a notice) when it is absent so /// the suite stays green on machines without the checkout. const DART_CORPUS: &str = "/Users/aok/Projects/rustdev/CratesCode/dart-pdf/test_corpora/pdfjs"; fn corpus(relative: &str) -> Option> { let path = format!("{DART_CORPUS}/{relative}"); match std::fs::read(&path) { Ok(bytes) => Some(bytes), Err(_) => { eprintln!("SKIP (no dart-pdf checkout): {path}"); None } } } fn cff_from_resources( doc: &mut PdfDocument, resources: &PdfDict, font_name: &str, ) -> Option { let fonts = doc.resolve(resources.get("Font")?).ok()?; let pdf_font = doc.resolve(fonts.as_dict()?.get(font_name)?).ok()?; let descriptor = doc.resolve(pdf_font.as_dict()?.get("FontDescriptor")?).ok()?; let font_file = descriptor.as_dict()?.get("FontFile3")?.clone(); let data = doc.resolve_stream(&font_file).ok()?; CffFont::parse(&data) } fn page_resources(doc: &mut PdfDocument) -> Option { let page_ref = doc.page_object_ref(0)?; let page_obj = doc.resolve_ref(page_ref).ok()?; let resources = doc.resolve(page_obj.as_dict()?.get("Resources")?).ok()?; resources.as_dict().cloned() } fn cff_from_page_font(path: &str, font_name: &str) -> Option { let bytes = corpus(path)?; let mut doc = PdfDocument::parse(&bytes).ok()?; let resources = page_resources(&mut doc)?; cff_from_resources(&mut doc, &resources, font_name) } fn cff_from_form_font(path: &str, form_name: &str, font_name: &str) -> Option { let bytes = corpus(path)?; let mut doc = PdfDocument::parse(&bytes).ok()?; let resources = page_resources(&mut doc)?; let xobjects = doc.resolve(resources.get("XObject")?).ok()?; let form = doc.resolve(xobjects.as_dict()?.get(form_name)?).ok()?; let form_resources = doc.resolve(form.as_stream()?.dict.get("Resources")?).ok()?; cff_from_resources(&mut doc, form_resources.as_dict()?, font_name) } #[test] fn endchar_seac_composes_accented_glyphs() { let Some(cff) = cff_from_form_font("endchar.pdf", "Fm0", "T1_0") else { return; }; let e = cff.outline_for_glyph(cff.gid_for_name("E")).expect("E"); let acute = cff .outline_for_glyph(cff.gid_for_name("acute")) .expect("acute"); let eacute = cff .outline_for_glyph(cff.gid_for_name("Eacute")) .expect("Eacute"); assert!( eacute.segments().len() > e.segments().len() + acute.segments().len() - 2, "Eacute should compose E + acute" ); } #[test] fn endchar_seac_composes_tilde_accents() { let Some(cff) = cff_from_page_font("glyph_accent.pdf", "F1") else { return; }; let a = cff.outline_for_glyph(cff.gid_for_name("a")).expect("a"); let tilde = cff .outline_for_glyph(cff.gid_for_name("tilde")) .expect("tilde"); let atilde = cff .outline_for_glyph(cff.gid_for_name("atilde")) .expect("atilde"); assert!( atilde.segments().len() > a.segments().len() + tilde.segments().len() - 2, "atilde should compose a + tilde" ); } // --- OpenType CFF container builders (port of _testOpenTypeCff etc.) --- fn u16be(v: u16) -> [u8; 2] { [(v >> 8) as u8, (v & 0xff) as u8] } fn u32be(v: u32) -> [u8; 4] { [ (v >> 24) as u8, ((v >> 16) & 0xff) as u8, ((v >> 8) & 0xff) as u8, (v & 0xff) as u8, ] } fn flat(parts: Vec<&[u8]>) -> Vec { parts.concat() } /// Port of dart `_testCmaps`: one subtable per Unicode cmap format. fn test_cmaps() -> Vec> { let mut format0 = flat(vec![&u16be(0), &u16be(262), &u16be(0)]); let mut glyphs = vec![0u8; 256]; glyphs[0x41] = 1; format0.extend(glyphs); // format 4: two segments; 0x42 maps through glyphIdArray[0] = 2 with // idDelta -1, yielding glyph 1. let format4 = flat(vec![ &u16be(4), &u16be(34), &u16be(0), &u16be(4), &u16be(4), &u16be(1), &u16be(0), &u16be(0x42), &u16be(0xffff), &u16be(0), &u16be(0x42), &u16be(0xffff), &u16be(0xffff), &u16be(1), &u16be(4), &u16be(0), &u16be(2), ]); let format6 = flat(vec![ &u16be(6), &u16be(12), &u16be(0), &u16be(0x400), &u16be(1), &u16be(1), ]); let format12 = flat(vec![ &u16be(12), &u16be(0), &u32be(28), &u32be(0), &u32be(1), &u32be(0x1f600), &u32be(0x1f600), &u32be(1), ]); vec![format0, format4, format6, format12] } fn cmap_table(subtables: &[Vec]) -> Vec { let records_len = 4 + subtables.len() * 8; let mut offset = records_len; let mut out = flat(vec![&u16be(0), &u16be(subtables.len() as u16)]); for (index, sub) in subtables.iter().enumerate() { out.extend(u16be(0)); out.extend(u16be(index as u16)); out.extend(u32be(offset as u32)); offset += sub.len(); } for sub in subtables { out.extend(sub); } out } fn test_opentype_cff(face_offset: usize, cmaps: Option>>) -> Vec { let cff = build_test_cff_font(); let cmap = cmap_table(&cmaps.unwrap_or_else(test_cmaps)); const DIR_LEN: usize = 12 + 2 * 16; let cff_offset = face_offset + DIR_LEN; let cmap_offset = cff_offset + cff.len(); let mut out = flat(vec![ &u32be(0x4f54544f), // OTTO &u16be(2), &u16be(0), &u16be(0), &u16be(0), b"CFF ", &u32be(0), &u32be(cff_offset as u32), &u32be(cff.len() as u32), b"cmap", &u32be(0), &u32be(cmap_offset as u32), &u32be(cmap.len() as u32), ]); out.extend(cff); out.extend(cmap); out } fn test_opentype_cff_collection() -> Vec { const FIRST_OFFSET: usize = 20; let first = test_opentype_cff(FIRST_OFFSET, None); let second_offset = FIRST_OFFSET + first.len(); let second = test_opentype_cff(second_offset, None); let mut out = flat(vec![ b"ttcf", &u32be(0x00010000), &u32be(2), &u32be(FIRST_OFFSET as u32), &u32be(second_offset as u32), ]); out.extend(first); out.extend(second); out } #[test] fn opentype_cff_reads_unicode_cmap_formats_and_collections() { let bytes = test_opentype_cff(0, None); let face = OpenTypeCffFont::parse(&bytes, 0).expect("OT CFF face"); assert_eq!(face.num_glyphs(), 2); assert_eq!(face.gid_for_unicode(0x41), 1, "format 0"); assert_eq!(face.gid_for_unicode(0x42), 1, "format 4"); assert_eq!(face.gid_for_unicode(0x400), 1, "format 6"); assert_eq!(face.gid_for_unicode(0x1f600), 1, "format 12"); assert_eq!(face.gid_for_unicode(0x43), 0); assert!(face.outline_for_glyph(1).is_some()); let advance = face.advance_for_glyph(1).expect("advance"); assert!((advance - 0.66).abs() < 1e-9, "got {advance}"); let collection = test_opentype_cff_collection(); assert!(OpenTypeCffFont::parse(&collection, 1).is_some()); assert!(OpenTypeCffFont::parse(&collection, -1).is_none()); assert!(OpenTypeCffFont::parse(&collection, 2).is_none()); } #[test] fn opentype_cff_rejects_malformed_or_incomplete_containers() { let plain = test_opentype_cff(0, None); assert!(OpenTypeCffFont::parse(&plain, 1).is_none()); assert!(OpenTypeCffFont::parse(b"not an sfnt", 0).is_none()); let mut truetype = plain.clone(); truetype[0..4].copy_from_slice(&[0, 1, 0, 0]); assert!(OpenTypeCffFont::parse(&truetype, 0).is_none()); assert!(OpenTypeCffFont::parse(&test_opentype_cff(0, Some(vec![])), 0).is_none()); } #[test] fn opentype_cff_system_face_keeps_unicode_cmap() { // Port of the macOS-only dart test; runs only when the Hiragino face is // present, otherwise skips with a notice (no system-font dependency). let path = "/System/Library/Fonts/ヒラギノ明朝 ProN.ttc"; let Ok(bytes) = std::fs::read(path) else { eprintln!("SKIP (no system face): {path}"); return; }; let face = OpenTypeCffFont::parse(&bytes, 0).expect("system face"); let glyph = face.gid_for_unicode('日' as u32); assert!(glyph > 0); assert!(face.outline_for_glyph(glyph).is_some()); assert!(face.advance_for_glyph(glyph).unwrap_or(0.0) > 0.0); }