diff --git a/crates/fonts-build/src/cmap.rs b/crates/fonts-build/src/cmap.rs index b817b7ef..c8f972c5 100644 --- a/crates/fonts-build/src/cmap.rs +++ b/crates/fonts-build/src/cmap.rs @@ -1,6 +1,11 @@ //! Just enough of an OpenType reader to ask which characters a face maps: the `cmap` table's //! Unicode subtables in formats 4 and 12, in a single font or one face of a collection. +/// The most distinct Unicode subtables read from one face. Real fonts have one to three; a cmap +/// may list 65535 encoding records, and each would make [`Cmap::ranges`] and [`Cmap::covers`] walk +/// its subtable again. +const MAX_SUBTABLES: usize = 16; + /// The Unicode character maps of one face. pub(crate) struct Cmap<'a> { subtables: Vec<&'a [u8]>, @@ -38,8 +43,15 @@ impl<'a> Cmap<'a> { Some(12) => u32_at(rest, 4).and_then(|len| usize::try_from(len).ok()), _ => None, }; - if let Some(subtable) = len.and_then(|len| rest.get(..len)) { + // Encoding records often share one subtable (Unicode and Windows naming the same + // format 4 table); each is read once. + if let Some(subtable) = len.and_then(|len| rest.get(..len)) + && !subtables.iter().any(|t: &&[u8]| t.as_ptr() == subtable.as_ptr() && t.len() == subtable.len()) + { subtables.push(subtable); + if subtables.len() == MAX_SUBTABLES { + break; + } } } (!subtables.is_empty()).then_some(Self { subtables }) diff --git a/crates/fonts-build/src/tests.rs b/crates/fonts-build/src/tests.rs index f614dc6e..24019116 100644 --- a/crates/fonts-build/src/tests.rs +++ b/crates/fonts-build/src/tests.rs @@ -384,6 +384,62 @@ fn cmap_reads_each_subtable_within_its_declared_length() { assert!(cmap.covers('A') && !cmap.covers('\u{20000}')); } +/// A font whose cmap has one Windows full-repertoire encoding record per item of `records`, each +/// naming that index of `subtables`: format 12 subtables of `(first, last)` groups. +fn shared_cmap_font(records: &[usize], subtables: &[&[(u32, u32)]]) -> Vec { + let be32 = |v: &mut Vec, x: u32| v.extend_from_slice(&x.to_be_bytes()); + let mut tables: Vec> = Vec::new(); + for groups in subtables { + let mut t = vec![0, 12, 0, 0]; + be32(&mut t, 16 + 12 * u32::try_from(groups.len()).unwrap()); + be32(&mut t, 0); + be32(&mut t, u32::try_from(groups.len()).unwrap()); + for &(first, last) in *groups { + [first, last, 1].into_iter().for_each(|x| be32(&mut t, x)); + } + tables.push(t); + } + let mut cmap = vec![0, 0]; + cmap.extend_from_slice(&u16::try_from(records.len()).unwrap().to_be_bytes()); + let mut offsets = Vec::new(); + let mut at = 4 + 8 * u32::try_from(records.len()).unwrap(); + for t in &tables { + offsets.push(at); + at += u32::try_from(t.len()).unwrap(); + } + for &r in records { + cmap.extend_from_slice(&[0, 3, 0, 10]); + be32(&mut cmap, offsets[r]); + } + tables.iter().for_each(|t| cmap.extend(t)); + let mut font = vec![0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; + font.extend_from_slice(b"cmap"); + for x in [0, 28, u32::try_from(cmap.len()).unwrap()] { + be32(&mut font, x); + } + font.extend(cmap); + font +} + +#[test] +fn cmap_reads_a_shared_subtable_once() { + // 65535 encoding records naming one subtable of 2000 one-character groups: read once, not + // 131 million spans. + let groups: Vec<(u32, u32)> = (0..2000).map(|i| (0x1_0000 + 2 * i, 0x1_0000 + 2 * i)).collect(); + let font = shared_cmap_font(&vec![0; usize::from(u16::MAX)], &[&groups]); + let started = std::time::Instant::now(); + let cmap = Cmap::read(&font, 0).unwrap(); + assert_eq!(cmap.ranges(), groups); + assert!(cmap.covers('\u{10000}') && !cmap.covers('\u{10001}')); + assert!(started.elapsed() < std::time::Duration::from_secs(10), "{:?}", started.elapsed()); + + // Past MAX_SUBTABLES distinct subtables, the rest are not read. + let one: Vec<[(u32, u32); 1]> = (0..20).map(|i| [(0x4E00 + i, 0x4E00 + i)]).collect(); + let subtables: Vec<&[(u32, u32)]> = one.iter().map(|g| &g[..]).collect(); + let font = shared_cmap_font(&(0..20).collect::>(), &subtables); + assert_eq!(Cmap::read(&font, 0).unwrap().ranges(), [(0x4E00, 0x4E0F)]); +} + /// A craft-fonts checkout in `scratch` with these `(family, scripts, role, font)` faces, all /// Regular. fn checkout(scratch: &Scratch, faces: &[(&str, &str, &str, Vec)]) -> PathBuf {