From ebdf70c95dceb666269343b86ac9efbc097e51a6 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 10 Oct 2026 00:28:32 -0600 Subject: [PATCH] [skera] Support identity CFF charsets --- skera/src/cff/subset.rs | 8 +++++ skera/src/lib.rs | 4 +++ skera/src/main.rs | 7 +++++ skera/tests/cff_test.rs | 69 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/skera/src/cff/subset.rs b/skera/src/cff/subset.rs index 3ceaf8b67..621b48060 100644 --- a/skera/src/cff/subset.rs +++ b/skera/src/cff/subset.rs @@ -397,6 +397,10 @@ pub(super) fn assemble( let mut strings = Vec::new(); let mut sid_map = BTreeMap::new(); let mut codes = vec![0]; + let identity_charset = source.cid + && plan + .subset_flags + .contains(SubsetFlags::SUBSET_FLAGS_CFF_IDENTITY_CHARSET); if !F::CFF2 { let cs = Charset::new( FontData::new(source.font.data()), @@ -405,6 +409,10 @@ pub(super) fn assemble( ) .ok_or(Error)?; for &old in old_gids.iter().skip(1) { + if identity_charset { + codes.push(codes.len()); + continue; + } let code = cs .string_id(GlyphId::new(old as u32)) .ok_or(Error)? diff --git a/skera/src/lib.rs b/skera/src/lib.rs index 9714e3f22..d1650f308 100644 --- a/skera/src/lib.rs +++ b/skera/src/lib.rs @@ -278,6 +278,10 @@ impl SubsetFlags { /// If set do not pull mirrored versions of input codepoints into the subset. pub const SUBSET_FLAGS_NO_BIDI_CLOSURE: Self = Self(0x0800); + /// If set CID-keyed CFF charsets use sequential CIDs equal to the output + /// glyph IDs. Name-keyed CFF and CFF2 fonts are unaffected. + pub const SUBSET_FLAGS_CFF_IDENTITY_CHARSET: Self = Self(0x8000); + /// Returns `true` if all of the flags in `other` are contained within `self`. #[inline] pub const fn contains(&self, other: Self) -> bool { diff --git a/skera/src/main.rs b/skera/src/main.rs index ded50210b..5496be0b5 100644 --- a/skera/src/main.rs +++ b/skera/src/main.rs @@ -110,6 +110,10 @@ struct Args { #[arg(long)] optimize: bool, + /// Emit an identity CFF charset (CID = output GID) for CID-keyed CFF + #[arg(long)] + cff_identity_charset: bool, + ///run subsetter N times #[arg(short, long)] num_iterations: Option, @@ -342,5 +346,8 @@ fn parse_subset_flags(args: &Args) -> SubsetFlags { if args.optimize { flags |= SubsetFlags::SUBSET_FLAGS_OPTIMIZE_IUP_DELTAS; } + if args.cff_identity_charset { + flags |= SubsetFlags::SUBSET_FLAGS_CFF_IDENTITY_CHARSET; + } flags } diff --git a/skera/tests/cff_test.rs b/skera/tests/cff_test.rs index 4bd1830c0..9a1372ec3 100644 --- a/skera/tests/cff_test.rs +++ b/skera/tests/cff_test.rs @@ -123,6 +123,75 @@ fn retained_gid_holes_and_notdef_are_empty() { } } +#[test] +fn identity_charsets_use_output_gids_only_for_cid_keyed_cff() { + for file in [ + "SourceHanSans-Regular_subset.otf", + "SourceSansPro-Regular.otf", + "AdobeVFPrototype.otf", + ] { + let source = std::fs::read(format!("test-data/fonts/{file}")).unwrap(); + let font = FontRef::new(&source).unwrap(); + for retain in [false, true] { + let flags = SubsetFlags::SUBSET_FLAGS_NOTDEF_OUTLINE + | if retain { + SubsetFlags::SUBSET_FLAGS_RETAIN_GIDS + } else { + SubsetFlags::default() + }; + let dense_plan = plan(&font, flags, "U+0041,U+0043,U+0061,U+4E00"); + let identity_plan = plan( + &font, + flags | SubsetFlags::SUBSET_FLAGS_CFF_IDENTITY_CHARSET, + "U+0041,U+0043,U+0061,U+4E00", + ); + let ordinary = subset_font(&font, &dense_plan).unwrap(); + let identity = subset_font(&font, &identity_plan).unwrap(); + let ordinary = FontRef::new(&ordinary).unwrap(); + let identity = FontRef::new(&identity).unwrap(); + let tag = if identity.cff2().is_ok() { + Tag::new(b"CFF2") + } else { + Tag::new(b"CFF ") + }; + let a_data = ordinary.data_for_tag(tag).unwrap().as_bytes(); + let b_data = identity.data_for_tag(tag).unwrap().as_bytes(); + let a = CffFontRef::new(a_data, 0, None).unwrap(); + let b = CffFontRef::new(b_data, 0, None).unwrap(); + assert_eq!(a.num_glyphs(), b.num_glyphs()); + if tag == Tag::new(b"CFF ") && b.is_cid() { + for gid in 0..b.num_glyphs() { + let gid = GlyphId::new(gid); + assert_eq!( + b.charset().unwrap().string_id(gid).unwrap().to_u16() as u32, + gid.to_u32() + ); + assert_eq!( + a.charstrings().get(gid.to_u32() as usize), + b.charstrings().get(gid.to_u32() as usize) + ); + assert_eq!(a.subfont_index(gid), b.subfont_index(gid)); + } + assert!( + identity_plan + .old_to_new_glyph_mapping() + .any(|(old, new)| old != new) + || retain + ); + } else { + assert_eq!(a_data, b_data, "{file}"); + } + for tag in [b"cmap", b"hmtx", b"GPOS", b"GSUB"] { + let tag = Tag::new(tag); + assert_eq!( + ordinary.data_for_tag(tag).map(|d| d.as_bytes()), + identity.data_for_tag(tag).map(|d| d.as_bytes()) + ); + } + } + } +} + #[test] fn cff2_subsetting_preserves_variation_locations() { use write_fonts::types::F2Dot14;