Skip to main content

write_fonts/tables/
cmap.rs

1//! the [cmap] table
2//!
3//! [cmap]: https://docs.microsoft.com/en-us/typography/opentype/spec/cmap
4
5include!("../../generated/generated_cmap.rs");
6
7use std::collections::HashMap;
8
9use crate::search_range::SearchRange;
10
11#[cfg(test)]
12mod dmap_tests {
13    use super::*;
14    use crate::{dump_table, FontBuilder};
15    use read_fonts::{FontRef, TableProvider};
16
17    #[test]
18    fn dmap_roundtrip() {
19        let dmap = Dmap::new(vec![EncodingRecord::new(
20            PlatformId::Windows,
21            10,
22            CmapSubtable::format_12(0, vec![SequentialMapGroup::new(65, 65, 70000)]),
23        )]);
24        let bytes = dump_table(&dmap).unwrap();
25        assert_eq!(Dmap::read(FontData::new(&bytes)).unwrap(), dmap);
26        let mut builder = FontBuilder::new();
27        builder.add_table(&dmap).unwrap();
28        let bytes = builder.build();
29        let font = FontRef::new(&bytes).unwrap();
30        assert_eq!(
31            font.dmap().unwrap().as_cmap().map_codepoint('A'),
32            Some(GlyphId::new(70000))
33        );
34        assert!(font.cmap().is_err());
35    }
36}
37
38// https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#windows-platform-platform-id--3
39const WINDOWS_BMP_ENCODING: u16 = 1;
40const WINDOWS_FULL_REPERTOIRE_ENCODING: u16 = 10;
41
42// https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#unicode-platform-platform-id--0
43const UNICODE_BMP_ENCODING: u16 = 3;
44const UNICODE_FULL_REPERTOIRE_ENCODING: u16 = 4;
45
46/// The reason we can't build a format 4 subtable for a set of mappings.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48enum Format4Error {
49    /// None of the chars are in the BMP.
50    NoBmpMappings,
51    /// A BMP char is mapped to a glyph id that doesn't fit in 16 bits.
52    GlyphIdOverflow { ch: char, gid: GlyphId },
53    /// The subtable's length, in bytes, doesn't fit in 16 bits.
54    LengthOverflow(usize),
55}
56
57impl std::fmt::Display for Format4Error {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::NoBmpMappings => write!(f, "there are no chars in the BMP"),
61            Self::GlyphIdOverflow { ch, gid } => write!(
62                f,
63                "U+{:04X} is mapped to {gid}, which is above the format 4 maximum of 65535",
64                *ch as u32
65            ),
66            Self::LengthOverflow(len) => write!(
67                f,
68                "length of {len} bytes is above the format 4 maximum of 65535"
69            ),
70        }
71    }
72}
73
74impl CmapSubtable {
75    /// Create a new format 4 subtable
76    ///
77    /// Returns an error if none of the input chars are in the BMP (i.e. have
78    /// codepoints <= 0xFFFF), or if format 4 can't represent all of the BMP
79    /// mappings.
80    ///
81    /// Invariants:
82    ///
83    /// - Inputs must be sorted and deduplicated.
84    fn create_format_4(mappings: &[(char, GlyphId)]) -> Result<Self, Format4Error> {
85        let mut end_code = Vec::with_capacity(mappings.len() + 1);
86        let mut start_code = Vec::with_capacity(mappings.len() + 1);
87        let mut id_deltas = Vec::with_capacity(mappings.len() + 1);
88        let mut id_range_offsets = Vec::with_capacity(mappings.len() + 1);
89        let mut glyph_ids = Vec::new();
90
91        // format 4 only has 16-bit glyph ids
92        if let Some((ch, gid)) = mappings
93            .iter()
94            .take_while(|(ch, _)| *ch <= '\u{FFFF}')
95            .find(|(_, gid)| gid.to_u32() > 0xFFFF)
96        {
97            return Err(Format4Error::GlyphIdOverflow { ch: *ch, gid: *gid });
98        }
99        let segments = compute_format4_segments(mappings);
100        if segments.is_empty() {
101            return Err(Format4Error::NoBmpMappings);
102        }
103        let add_final_segment = segments.last().is_none_or(|seg| {
104            (mappings[seg.start_ix].0, mappings[seg.end_ix].0) != ('\u{FFFF}', '\u{FFFF}')
105        });
106        let n_segments = segments.len() + add_final_segment as usize;
107        let n_glyph_ids = segments
108            .iter()
109            .filter(|seg| seg.id_delta.is_none())
110            .map(Format4Segment::len)
111            .sum();
112        Cmap4::checked_length(n_segments, n_glyph_ids)?;
113        for (i, segment) in segments.into_iter().enumerate() {
114            let start = mappings[segment.start_ix].0;
115            let end = mappings[segment.end_ix].0;
116            start_code.push(start as u32 as u16);
117            end_code.push(end as u32 as u16);
118            if let Some(delta) = segment.id_delta {
119                // "The idDelta arithmetic is modulo 65536":
120                id_deltas.push(delta as i16);
121                id_range_offsets.push(0u16);
122            } else {
123                // if the deltas for a range are not identical, we rely on the
124                // explicit glyph_ids array.
125                //
126                // The logic here is based on the memory layout of the table:
127                // because the glyph_id array follows the id_range_offsets array,
128                // the id_range_offsets array essentially stores a memory offset.
129                let current_n_ids = glyph_ids.len();
130                let n_following_segments = n_segments - i;
131                // number of bytes from the id_range_offset value to the glyph id
132                // for this segment, in the glyph_ids array
133                let id_range_offset = (n_following_segments + current_n_ids) * u16::RAW_BYTE_LEN;
134                id_deltas.push(0);
135                // this is an offset within the subtable, so it fits in a u16
136                // because the subtable's length does
137                id_range_offsets.push(
138                    id_range_offset
139                        .try_into()
140                        .expect("length checked before now"),
141                );
142                glyph_ids.extend(
143                    mappings[segment.start_ix..=segment.end_ix]
144                        .iter()
145                        .map(|(_, gid)| u16::try_from(gid.to_u32()).expect("checked before now")),
146                )
147            }
148        }
149
150        // add the final segment:
151        if add_final_segment {
152            end_code.push(0xFFFF);
153            start_code.push(0xFFFF);
154            id_deltas.push(1);
155            id_range_offsets.push(0);
156        }
157
158        Ok(Self::format_4(
159            0,
160            end_code,
161            start_code,
162            id_deltas,
163            id_range_offsets,
164            glyph_ids,
165        ))
166    }
167
168    /// Create a new format 12 `CmapSubtable` from a list of `(char, GlyphId)` pairs.
169    ///
170    /// The pairs are expected to be already sorted by chars.
171    /// In case of duplicate chars, the last one wins.
172    fn create_format_12(mappings: &[(char, GlyphId)]) -> Self {
173        let (mut char_codes, gids): (Vec<u32>, Vec<u32>) = mappings
174            .iter()
175            .map(|(cp, gid)| (*cp as u32, gid.to_u32()))
176            .unzip();
177        let cmap: HashMap<_, _> = char_codes.iter().cloned().zip(gids).collect();
178        char_codes.dedup();
179
180        // we only create a format 12 subtable when there are mappings, so unwrap is safe
181        let mut start_char_code = *char_codes.first().unwrap();
182        let mut start_glyph_id = cmap[&start_char_code];
183        let mut last_glyph_id = start_glyph_id.wrapping_sub(1);
184        let mut last_char_code = start_char_code.wrapping_sub(1);
185        let mut groups = Vec::new();
186        for char_code in char_codes {
187            let glyph_id = cmap[&char_code];
188            if glyph_id != last_glyph_id.wrapping_add(1)
189                || char_code != last_char_code.wrapping_add(1)
190            {
191                groups.push((start_char_code, last_char_code, start_glyph_id));
192                start_char_code = char_code;
193                start_glyph_id = glyph_id;
194            }
195            last_glyph_id = glyph_id;
196            last_char_code = char_code;
197        }
198        groups.push((start_char_code, last_char_code, start_glyph_id));
199
200        let seq_map_groups = groups
201            .into_iter()
202            .map(|(start_char, end_char, gid)| SequentialMapGroup::new(start_char, end_char, gid))
203            .collect::<Vec<_>>();
204        CmapSubtable::format_12(
205            0, // 'lang' set to zero for all 'cmap' subtables whose platform IDs are other than Macintosh
206            seq_map_groups,
207        )
208    }
209}
210
211/// A conflicting Cmap definition, one char is mapped to multiple distinct GlyphIds.
212///
213/// If there are multiple conflicting mappings, one is chosen arbitrarily.
214/// gid1 is less than gid2.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct CmapConflict {
217    ch: char,
218    gid1: GlyphId,
219    gid2: GlyphId,
220}
221
222impl std::fmt::Display for CmapConflict {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        let ch32 = self.ch as u32;
225        write!(
226            f,
227            "Cannot map {:?} (U+{ch32:04X}) to two different glyph ids: {} and {}",
228            self.ch, self.gid1, self.gid2
229        )
230    }
231}
232
233impl std::error::Error for CmapConflict {}
234
235impl Cmap {
236    /// Generates a ['cmap'] that is expected to work in most modern environments.
237    ///
238    /// The input is not required to be sorted.
239    ///
240    /// This emits [format 4] and [format 12] subtables, respectively for the
241    /// Basic Multilingual Plane and Full Unicode Repertoire.
242    ///
243    /// The format 4 subtable is omitted if it can't represent every mapping
244    /// in the BMP: that is, if it would be longer than 65535 bytes, or if a
245    /// BMP char is mapped to a glyph id above 65535. In that case we log a
246    /// warning, and the format 12 subtable contains all of the mappings, and
247    /// is emitted even if every char is in the BMP. The spec [doesn't require] a format 4 alongside a
248    /// format 12, and says that if both are present their BMP mappings should
249    /// be identical, which rules out a partial format 4.
250    ///
251    /// Also see: <https://learn.microsoft.com/en-us/typography/opentype/spec/recom#cmap-table>
252    ///
253    /// [`cmap`]: https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
254    /// [format 4]: https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
255    /// [format 12]: https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-12-segmented-coverage
256    /// [doesn't require]: https://learn.microsoft.com/en-us/typography/opentype/spec/recom#cmap-table
257    pub fn from_mappings(
258        mappings: impl IntoIterator<Item = (char, GlyphId)>,
259    ) -> Result<Cmap, CmapConflict> {
260        let mut mappings: Vec<_> = mappings.into_iter().collect();
261        mappings.sort();
262        mappings.dedup();
263        if let Some((ch, gid1, gid2)) =
264            mappings
265                .iter()
266                .zip(mappings.iter().skip(1))
267                .find_map(|((c1, g1), (c2, g2))| {
268                    (c1 == c2 && g1 != g2).then(|| (*c1, *g1.min(g2), *g1.max(g2)))
269                })
270        {
271            return Err(CmapConflict { ch, gid1, gid2 });
272        }
273
274        let mut uni_records = Vec::new(); // platform 0
275        let mut win_records = Vec::new(); // platform 3
276
277        // if there are characters in the Unicode Basic Multilingual Plane (U+0000 to U+FFFF)
278        // we need to emit format 4 subtables, unless format 4 can't represent them
279        let bmp_subtable = CmapSubtable::create_format_4(&mappings)
280            .inspect_err(|e| log::warn!("omitting cmap format 4 subtable: {e}"))
281            .ok();
282        let has_format_4 = bmp_subtable.is_some();
283        if let Some(bmp_subtable) = bmp_subtable {
284            // Absent a strong signal to do otherwise, match fontmake/fonttools
285            // Since both Windows and Unicode platform tables use the same subtable they are
286            // almost entirely byte-shared
287            // See https://github.com/googlefonts/fontmake-rs/issues/251
288            uni_records.push(EncodingRecord::new(
289                PlatformId::Unicode,
290                UNICODE_BMP_ENCODING,
291                bmp_subtable.clone(),
292            ));
293            win_records.push(EncodingRecord::new(
294                PlatformId::Windows,
295                WINDOWS_BMP_ENCODING,
296                bmp_subtable,
297            ));
298        }
299
300        // If there are any supplementary-plane characters (U+10000 to U+10FFFF) we also
301        // emit format 12 subtables; and if there's no format 4 then format 12 has to
302        // cover the BMP as well
303        if !mappings.is_empty()
304            && (!has_format_4 || mappings.iter().any(|(cp, _)| *cp > '\u{FFFF}'))
305        {
306            let full_repertoire_subtable = CmapSubtable::create_format_12(&mappings);
307            // format 12 subtables are also going to be byte-shared, just like above
308            uni_records.push(EncodingRecord::new(
309                PlatformId::Unicode,
310                UNICODE_FULL_REPERTOIRE_ENCODING,
311                full_repertoire_subtable.clone(),
312            ));
313            win_records.push(EncodingRecord::new(
314                PlatformId::Windows,
315                WINDOWS_FULL_REPERTOIRE_ENCODING,
316                full_repertoire_subtable,
317            ));
318        }
319
320        // put encoding records in order of (platform id, encoding id):
321        // - Unicode (0), BMP (3)
322        // - Unicode (0), full repertoire (4)
323        // - Windows (3), BMP (1)
324        // - Windows (3), full repertoire (10)
325        Ok(Cmap::new(
326            uni_records.into_iter().chain(win_records).collect(),
327        ))
328    }
329}
330
331// a segment of a format 4 subtable
332#[derive(Clone, Copy, Debug)]
333struct Format4Segment {
334    // indices are into the source mappings
335    start_ix: usize,
336    end_ix: usize,
337    id_delta: Option<i32>,
338}
339
340impl Format4Segment {
341    fn len(&self) -> usize {
342        self.end_ix - self.start_ix + 1
343    }
344}
345
346/// Compute the segments for a format 4 subtable, using as few bytes as possible.
347///
348/// A segment maps a contiguous range of chars. It costs 8 bytes (end code,
349/// start code, idDelta and idRangeOffset), and if its glyph ids aren't also
350/// contiguous, it needs a glyphIdArray entry for each char, at 2 bytes each.
351///
352/// We split the mappings into runs of contiguous chars, then find the
353/// cheapest segments for each run with [`push_cheapest_segments`]. So
354/// segments never span a gap in the chars. A glyphIdArray segment could, by
355/// mapping the missing chars to glyph 0, but we don't try that.
356///
357/// Chars above the BMP are ignored.
358fn compute_format4_segments(mappings: &[(char, GlyphId)]) -> Vec<Format4Segment> {
359    // ignore chars above BMP:
360    let mappings = mappings
361        .iter()
362        .position(|(c, _)| u16::try_from(*c as u32).is_err())
363        .map(|bad_idx| &mappings[..bad_idx])
364        .unwrap_or(mappings);
365    let mut segments = Vec::new();
366    let mut run_start = 0;
367    for run in mappings.chunk_by(|(c1, _), (c2, _)| *c1 as u32 + 1 == *c2 as u32) {
368        push_cheapest_segments(run, run_start, &mut segments);
369        run_start += run.len();
370    }
371    segments
372}
373
374/// Append the cheapest segments for `run`, whose chars are contiguous.
375///
376/// `offset` is the index of the start of `run` in the full list of mappings.
377///
378/// Let `best[k]` be the cost of the cheapest segments for `run[..k]`. The
379/// last of those segments covers `run[j..k]` for some `j`, so `best[k]` is
380/// the minimum over `j` of `best[j]` plus the cost of that segment. Trying
381/// every `j` would be quadratic, but two observations make it linear:
382///
383/// - `best` never decreases, since dropping the last mapping from a set of
384///   segments never makes them more expensive. So the cheapest idDelta
385///   segment ending at `k` is the longest one, which starts where the
386///   current run of contiguous glyph ids does.
387/// - A glyphIdArray segment costs 2 bytes per mapping wherever it starts,
388///   so the cheapest one ending at `k` starts at the `j` with the lowest
389///   `best[j] - 2 * j`. We track that as we go.
390fn push_cheapest_segments(
391    run: &[(char, GlyphId)],
392    offset: usize,
393    segments: &mut Vec<Format4Segment>,
394) {
395    // a segment always costs 4 u16s (end, start, id_delta, id_range_offset)
396    const SEGMENT_COST: usize = 4 * u16::RAW_BYTE_LEN;
397    // and a glyphIdArray segment also costs a u16 for each char
398    const GLYPH_ID_COST: usize = u16::RAW_BYTE_LEN;
399
400    // best[k] is the cost of the cheapest segments for run[..k]
401    let mut best = vec![0; run.len() + 1];
402    // the start of the last of those segments, and whether it uses idDelta
403    let mut last_segment = vec![(0, false); run.len() + 1];
404    // the start of the current run of contiguous glyph ids
405    let mut delta_start = 0;
406    // the start of the cheapest glyphIdArray segment ending at k
407    let mut array_start = 0;
408    for k in 1..=run.len() {
409        if k > 1 && run[k - 2].1.to_u32() + 1 != run[k - 1].1.to_u32() {
410            delta_start = k - 1;
411        }
412        let delta_cost = best[delta_start] + SEGMENT_COST;
413        let array_cost = best[array_start] + SEGMENT_COST + (k - array_start) * GLYPH_ID_COST;
414        // on a tie, prefer idDelta
415        (best[k], last_segment[k]) = if delta_cost <= array_cost {
416            (delta_cost, (delta_start, true))
417        } else {
418            (array_cost, (array_start, false))
419        };
420        // best[k] - 2 * k < best[array_start] - 2 * array_start, without
421        // going negative
422        if best[k] < best[array_start] + (k - array_start) * GLYPH_ID_COST {
423            array_start = k;
424        }
425    }
426
427    // walk back from the end of the run to recover the segments
428    let first_new = segments.len();
429    let mut end = run.len();
430    while end > 0 {
431        let (start, use_delta) = last_segment[end];
432        let (ch, gid) = run[start];
433        segments.push(Format4Segment {
434            start_ix: offset + start,
435            end_ix: offset + end - 1,
436            id_delta: use_delta.then(|| gid.to_u32() as i32 - ch as u32 as i32),
437        });
438        end = start;
439    }
440    segments[first_new..].reverse();
441}
442
443impl Cmap4 {
444    /// Compute the length of a subtable with the given number of segments and glyph ids.
445    ///
446    /// Returns an error if the length doesn't fit in the 16-bit length field.
447    fn checked_length(n_segments: usize, n_glyph_ids: usize) -> Result<u16, Format4Error> {
448        // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
449        // there are always 8 u16 fields
450        const FIXED_SIZE: usize = 8 * u16::RAW_BYTE_LEN;
451        const PER_SEGMENT_LEN: usize = 4 * u16::RAW_BYTE_LEN;
452
453        let segment_len = n_segments * PER_SEGMENT_LEN;
454        let gid_len = n_glyph_ids * u16::RAW_BYTE_LEN;
455
456        let length = FIXED_SIZE + segment_len + gid_len;
457        length
458            .try_into()
459            .map_err(|_| Format4Error::LengthOverflow(length))
460    }
461
462    fn compute_length(&self) -> u16 {
463        // validation reports a length that doesn't fit
464        Self::checked_length(self.end_code.len(), self.glyph_id_array.len())
465            .expect("cmap4 overflow")
466    }
467
468    fn check_length(&self, ctx: &mut ValidationCtx) {
469        if let Err(e) = Self::checked_length(self.end_code.len(), self.glyph_id_array.len()) {
470            ctx.report(e);
471        }
472    }
473
474    fn compute_search_range(&self) -> u16 {
475        SearchRange::compute(self.end_code.len(), u16::RAW_BYTE_LEN).search_range
476    }
477
478    fn compute_entry_selector(&self) -> u16 {
479        SearchRange::compute(self.end_code.len(), u16::RAW_BYTE_LEN).entry_selector
480    }
481
482    fn compute_range_shift(&self) -> u16 {
483        SearchRange::compute(self.end_code.len(), u16::RAW_BYTE_LEN).range_shift
484    }
485}
486
487impl Cmap12 {
488    fn compute_length(&self) -> u32 {
489        // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-12-segmented-coverage
490        const FIXED_SIZE: usize = 2 * u16::RAW_BYTE_LEN + 3 * u32::RAW_BYTE_LEN;
491        const PER_SEGMENT_LEN: usize = 3 * u32::RAW_BYTE_LEN;
492
493        (FIXED_SIZE + PER_SEGMENT_LEN * self.groups.len())
494            .try_into()
495            .unwrap()
496    }
497}
498
499// We write the UVS tables inline, immediately after the record array, rather
500// than as offset targets that the packer lays out.
501//
502// This is because of the 'length' field, which has to cover the UVS tables
503// as well as the header: fonttools slices the subtable by it before
504// decompiling, and ots requires it to be exact. write-fonts can't promise
505// that for offset targets: the packer is free to reorder subtables, and
506// identical subtables are deduplicated. In practice the default sort emits a
507// table's children directly after it, and cmap's 32-bit offsets never trigger
508// the fallback sort, so it would happen to work out; but writing the tables
509// ourselves makes the layout (and so 'length') correct by construction.
510macro_rules! impl_uvs_writer {
511    ($cmap:ident, $format:literal) => {
512        impl FontWrite for $cmap {
513            fn write_into(&self, writer: &mut TableWriter) {
514                ($format as u16).write_into(writer);
515                self.compute_length().write_into(writer);
516                u32::try_from(self.var_selector.len())
517                    .unwrap()
518                    .write_into(writer);
519
520                // offsets are relative to the start of this subtable; the UVS tables
521                // follow the record array, in record order, default before non-default
522                let mut next_offset = Self::HEADER_LEN + self.var_selector.len() * Self::RECORD_LEN;
523                let mut write_offset = |writer: &mut TableWriter, len: Option<usize>| match len {
524                    Some(len) => {
525                        (next_offset as u32).write_into(writer);
526                        next_offset += len;
527                    }
528                    None => 0u32.write_into(writer),
529                };
530                for record in &self.var_selector {
531                    record.var_selector.write_into(writer);
532                    write_offset(
533                        writer,
534                        record.default_uvs.as_ref().map(DefaultUvs::compute_len),
535                    );
536                    write_offset(
537                        writer,
538                        record.non_default_uvs.as_ref().map(|uvs| uvs.compute_len()),
539                    );
540                }
541                for record in &self.var_selector {
542                    if let Some(uvs) = record.default_uvs.as_ref() {
543                        uvs.write_into(writer);
544                    }
545                    if let Some(uvs) = record.non_default_uvs.as_ref() {
546                        uvs.write_into(writer);
547                    }
548                }
549            }
550
551            fn table_type(&self) -> TableType {
552                TableType::Named(stringify!($cmap))
553            }
554        }
555
556        impl $cmap {
557            // format, length, numVarSelectorRecords
558            const HEADER_LEN: usize = u16::RAW_BYTE_LEN + 2 * u32::RAW_BYTE_LEN;
559            // varSelector, defaultUVSOffset, nonDefaultUVSOffset
560            const RECORD_LEN: usize = Uint24::RAW_BYTE_LEN + 2 * u32::RAW_BYTE_LEN;
561
562            fn compute_length(&self) -> u32 {
563                let uvs_len: usize = self
564                    .var_selector
565                    .iter()
566                    .map(|record| {
567                        record
568                            .default_uvs
569                            .as_ref()
570                            .map(DefaultUvs::compute_len)
571                            .unwrap_or(0)
572                            + record
573                                .non_default_uvs
574                                .as_ref()
575                                .map(|uvs| uvs.compute_len())
576                                .unwrap_or(0)
577                    })
578                    .sum();
579
580                (Self::HEADER_LEN + self.var_selector.len() * Self::RECORD_LEN + uvs_len)
581                    .try_into()
582                    .expect("UVS subtable overflow")
583            }
584        }
585    };
586}
587
588impl_uvs_writer!(Cmap14, 14);
589impl_uvs_writer!(Cmap15, 15);
590
591impl DefaultUvs {
592    fn compute_len(&self) -> usize {
593        // numUnicodeValueRanges, then (startUnicodeValue, additionalCount) per range
594        u32::RAW_BYTE_LEN + self.ranges.len() * (Uint24::RAW_BYTE_LEN + u8::RAW_BYTE_LEN)
595    }
596}
597
598impl NonDefaultUvs {
599    fn compute_len(&self) -> usize {
600        // numUVSMappings, then (unicodeValue, glyphID) per mapping
601        u32::RAW_BYTE_LEN + self.uvs_mapping.len() * (Uint24::RAW_BYTE_LEN + u16::RAW_BYTE_LEN)
602    }
603}
604
605impl NonDefaultUvs24 {
606    fn compute_len(&self) -> usize {
607        u32::RAW_BYTE_LEN + self.uvs_mapping.len() * (2 * Uint24::RAW_BYTE_LEN)
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use std::ops::RangeInclusive;
614
615    use font_types::{GlyphId, Uint24};
616    use read_fonts::{
617        tables::cmap::{Cmap, CmapSubtable, PlatformId},
618        FontData, FontRead, TableProvider,
619    };
620
621    use crate::{
622        dump_table,
623        from_obj::ToOwnedTable,
624        tables::cmap::{
625            self as write, CmapConflict, UNICODE_BMP_ENCODING, UNICODE_FULL_REPERTOIRE_ENCODING,
626            WINDOWS_BMP_ENCODING, WINDOWS_FULL_REPERTOIRE_ENCODING,
627        },
628    };
629
630    use super::{Cmap12, SequentialMapGroup};
631
632    fn assert_generates_simple_cmap(mappings: Vec<(char, GlyphId)>) {
633        let cmap = write::Cmap::from_mappings(mappings).unwrap();
634
635        let bytes = dump_table(&cmap).unwrap();
636        let font_data = FontData::new(&bytes);
637        let cmap = Cmap::read(font_data).unwrap();
638
639        assert_eq!(
640            2,
641            cmap.encoding_records().len(),
642            "{:?}",
643            cmap.encoding_records()
644        );
645        assert_eq!(
646            vec![
647                (PlatformId::Unicode, UNICODE_BMP_ENCODING),
648                (PlatformId::Windows, WINDOWS_BMP_ENCODING)
649            ],
650            cmap.encoding_records()
651                .iter()
652                .map(|er| (er.platform_id(), er.encoding_id()))
653                .collect::<Vec<_>>()
654        );
655
656        for encoding_record in cmap.encoding_records() {
657            let CmapSubtable::Format4(cmap4) = encoding_record.subtable(font_data).unwrap() else {
658                panic!("Expected a cmap4 in {encoding_record:?}");
659            };
660
661            // The spec example says entry_selector 4 but the calculation it gives seems to yield 2 (?)
662            assert_eq!(
663                (8, 8, 2, 0),
664                (
665                    cmap4.seg_count_x2(),
666                    cmap4.search_range(),
667                    cmap4.entry_selector(),
668                    cmap4.range_shift()
669                )
670            );
671            assert_eq!(cmap4.start_code(), &[10u16, 30u16, 153u16, 0xffffu16]);
672            assert_eq!(cmap4.end_code(), &[20u16, 90u16, 480u16, 0xffffu16]);
673            // The example starts at gid 1, we're starting at 0
674            assert_eq!(cmap4.id_delta(), &[-10i16, -19i16, -81i16, 1i16]);
675            assert_eq!(cmap4.id_range_offsets(), &[0u16, 0u16, 0u16, 0u16]);
676        }
677    }
678
679    fn simple_cmap_mappings() -> Vec<(char, GlyphId)> {
680        (10..=20)
681            .chain(30..=90)
682            .chain(153..=480)
683            .enumerate()
684            .map(|(idx, codepoint)| (char::from_u32(codepoint).unwrap(), GlyphId::new(idx as u32)))
685            .collect()
686    }
687
688    // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
689    // "map characters 10-20, 30-90, and 153-480 onto a contiguous range of glyph indices"
690    #[test]
691    fn generate_simple_cmap4() {
692        let mappings = simple_cmap_mappings();
693        assert_generates_simple_cmap(mappings);
694    }
695
696    #[test]
697    fn generate_cmap4_out_of_order_input() {
698        let mut ordered = simple_cmap_mappings();
699        let mut disordered = Vec::new();
700        while !ordered.is_empty() {
701            if ordered.len() % 2 == 0 {
702                disordered.insert(0, ordered.remove(0));
703            } else {
704                disordered.push(ordered.remove(0));
705            }
706        }
707        assert_ne!(ordered, disordered);
708        assert_generates_simple_cmap(disordered);
709    }
710
711    #[test]
712    fn generate_cmap4_large_values() {
713        let mut mappings = simple_cmap_mappings();
714        // Example from Texturina.
715        let codepoint = char::from_u32(0xa78b).unwrap();
716        let gid = GlyphId::new(153);
717        mappings.push((codepoint, gid));
718
719        let cmap = write::Cmap::from_mappings(mappings).unwrap();
720
721        let bytes = dump_table(&cmap).unwrap();
722        let font_data = FontData::new(&bytes);
723        let cmap = Cmap::read(font_data).unwrap();
724        assert_eq!(cmap.map_codepoint(codepoint), Some(gid));
725    }
726
727    #[test]
728    fn generate_cmap4_large_positive_delta() {
729        let mut mappings = simple_cmap_mappings();
730        // Example from Cactus Classical Serif: a low codepoint mapped to a
731        // glyph more than 32767 ids above it.
732        let codepoint = '\u{2026}';
733        let gid = GlyphId::new(58562);
734        mappings.push((codepoint, gid));
735
736        let cmap = write::Cmap::from_mappings(mappings).unwrap();
737
738        let bytes = dump_table(&cmap).unwrap();
739        let font_data = FontData::new(&bytes);
740        let cmap = Cmap::read(font_data).unwrap();
741        assert_eq!(cmap.map_codepoint(codepoint), Some(gid));
742    }
743
744    #[test]
745    fn bytes_are_reused() {
746        // We emit extra encoding records assuming it's cheap. Make sure.
747        let mappings = simple_cmap_mappings();
748        let cmap_both = write::Cmap::from_mappings(mappings).unwrap();
749        assert_eq!(2, cmap_both.encoding_records.len(), "{cmap_both:?}");
750
751        let bytes_for_both = dump_table(&cmap_both).unwrap().len();
752
753        for i in 0..cmap_both.encoding_records.len() {
754            let mut cmap = cmap_both.clone();
755            cmap.encoding_records.remove(i);
756            let bytes_for_one = dump_table(&cmap).unwrap().len();
757            assert_eq!(bytes_for_one + 8, bytes_for_both);
758        }
759    }
760
761    fn non_bmp_cmap_mappings() -> Vec<(char, GlyphId)> {
762        // contains four sequential map groups
763        vec![
764            // first group
765            ('\u{1f12f}', GlyphId::new(481)),
766            ('\u{1f130}', GlyphId::new(482)),
767            // char 0x1f131 skipped, starts second group
768            ('\u{1f132}', GlyphId::new(483)),
769            ('\u{1f133}', GlyphId::new(484)),
770            // gid 485 skipped, starts third group
771            ('\u{1f134}', GlyphId::new(486)),
772            // char 0x1f135 skipped, starts fourth group. identical duplicate bindings are fine
773            ('\u{1f136}', GlyphId::new(488)),
774            ('\u{1f136}', GlyphId::new(488)),
775        ]
776    }
777
778    fn bmp_and_non_bmp_cmap_mappings() -> Vec<(char, GlyphId)> {
779        let mut mappings = simple_cmap_mappings();
780        mappings.extend(non_bmp_cmap_mappings());
781        mappings
782    }
783
784    fn assert_cmap12_groups(
785        font_data: FontData,
786        cmap: &Cmap,
787        record_index: usize,
788        expected: &[(u32, u32, u32)],
789    ) {
790        let rec = &cmap.encoding_records()[record_index];
791        let CmapSubtable::Format12(subtable) = rec.subtable(font_data).unwrap() else {
792            panic!("Expected a cmap12 in {rec:?}");
793        };
794        let groups = subtable
795            .groups()
796            .iter()
797            .map(|g| (g.start_char_code(), g.end_char_code(), g.start_glyph_id()))
798            .collect::<Vec<_>>();
799        assert_eq!(groups.len(), expected.len());
800        assert_eq!(groups, expected);
801    }
802
803    #[test]
804    fn generate_cmap4_and_12() {
805        let mappings = bmp_and_non_bmp_cmap_mappings();
806
807        let cmap = write::Cmap::from_mappings(mappings).unwrap();
808
809        let bytes = dump_table(&cmap).unwrap();
810        let font_data = FontData::new(&bytes);
811        let cmap = Cmap::read(font_data).unwrap();
812
813        assert_eq!(
814            4,
815            cmap.encoding_records().len(),
816            "{:?}",
817            cmap.encoding_records()
818        );
819        assert_eq!(
820            vec![
821                (PlatformId::Unicode, UNICODE_BMP_ENCODING),
822                (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING),
823                (PlatformId::Windows, WINDOWS_BMP_ENCODING),
824                (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING)
825            ],
826            cmap.encoding_records()
827                .iter()
828                .map(|er| (er.platform_id(), er.encoding_id()))
829                .collect::<Vec<_>>()
830        );
831
832        let encoding_records = cmap.encoding_records();
833        let first_rec = &encoding_records[0];
834        assert!(
835            matches!(
836                first_rec.subtable(font_data).unwrap(),
837                CmapSubtable::Format4(_)
838            ),
839            "Expected a cmap4 in {first_rec:?}"
840        );
841
842        // (start_char_code, end_char_code, start_glyph_id)
843        let expected_groups = vec![
844            (10, 20, 0),
845            (30, 90, 11),
846            (153, 480, 72),
847            (0x1f12f, 0x1f130, 481),
848            (0x1f132, 0x1f133, 483),
849            (0x1f134, 0x1f134, 486),
850            (0x1f136, 0x1f136, 488),
851        ];
852        assert_cmap12_groups(font_data, &cmap, 1, &expected_groups);
853        assert_cmap12_groups(font_data, &cmap, 3, &expected_groups);
854    }
855
856    #[test]
857    fn generate_cmap12_only() {
858        let mappings = non_bmp_cmap_mappings();
859
860        let cmap = write::Cmap::from_mappings(mappings).unwrap();
861
862        let bytes = dump_table(&cmap).unwrap();
863        let font_data = FontData::new(&bytes);
864        let cmap = Cmap::read(font_data).unwrap();
865
866        assert_eq!(
867            2,
868            cmap.encoding_records().len(),
869            "{:?}",
870            cmap.encoding_records()
871        );
872        assert_eq!(
873            vec![
874                (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING),
875                (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING)
876            ],
877            cmap.encoding_records()
878                .iter()
879                .map(|er| (er.platform_id(), er.encoding_id()))
880                .collect::<Vec<_>>()
881        );
882
883        // (start_char_code, end_char_code, start_glyph_id)
884        let expected_groups = vec![
885            (0x1f12f, 0x1f130, 481),
886            (0x1f132, 0x1f133, 483),
887            (0x1f134, 0x1f134, 486),
888            (0x1f136, 0x1f136, 488),
889        ];
890        assert_cmap12_groups(font_data, &cmap, 0, &expected_groups);
891        assert_cmap12_groups(font_data, &cmap, 1, &expected_groups);
892    }
893
894    #[test]
895    fn multiple_mappings_fails() {
896        let mut mappings = non_bmp_cmap_mappings();
897        // add an additional mapping to a different glyphId
898        let (ch, gid1) = mappings[0];
899        let gid2 = GlyphId::new(gid1.to_u32() + 1);
900        mappings.push((ch, gid2));
901
902        let result = write::Cmap::from_mappings(mappings);
903
904        assert_eq!(result, Err(CmapConflict { ch, gid1, gid2 }))
905    }
906
907    struct MappingBuilder {
908        mappings: Vec<(char, GlyphId)>,
909        next_gid: u16,
910    }
911
912    impl Default for MappingBuilder {
913        fn default() -> Self {
914            Self {
915                mappings: Default::default(),
916                next_gid: 1,
917            }
918        }
919    }
920
921    impl MappingBuilder {
922        fn extend(mut self, range: impl IntoIterator<Item = char>) -> Self {
923            for c in range {
924                let gid = GlyphId::new(self.next_gid as _);
925                self.mappings.push((c, gid));
926                self.next_gid += 1;
927            }
928            self
929        }
930
931        // compute the segments for the mapping
932        fn compute(&mut self) -> Vec<RangeInclusive<char>> {
933            self.mappings.sort();
934            super::compute_format4_segments(&self.mappings)
935                .into_iter()
936                .map(|seg| self.mappings[seg.start_ix].0..=self.mappings[seg.end_ix].0)
937                .collect()
938        }
939
940        fn build(mut self) -> Vec<(char, GlyphId)> {
941            self.mappings.sort();
942            self.mappings
943        }
944    }
945
946    #[test]
947    fn f4_segments_simple() {
948        let mut one_big_discontiguous_mapping = MappingBuilder::default().extend(('a'..='z').rev());
949        assert_eq!(one_big_discontiguous_mapping.compute(), ['a'..='z']);
950    }
951
952    #[test]
953    fn f4_segments_combine_small() {
954        let mut mapping = MappingBuilder::default()
955            // backwards so gids are not contiguous
956            .extend(['e', 'd', 'c', 'b', 'a'])
957            // these two contiguous ranges aren't worth the cost, should merge
958            // into the first and last respectively
959            .extend('f'..='g')
960            .extend('m'..='n')
961            .extend(('o'..='z').rev());
962
963        assert_eq!(mapping.compute(), ['a'..='g', 'm'..='z']);
964    }
965
966    #[test]
967    fn f4_segments_keep() {
968        let mut mapping = MappingBuilder::default()
969            .extend('a'..='m')
970            .extend(['o', 'n']);
971
972        assert_eq!(mapping.compute(), ['a'..='m', 'n'..='o']);
973    }
974
975    fn expect_f4(mapping: &[(char, GlyphId)]) -> super::Cmap4 {
976        let format4 = super::CmapSubtable::create_format_4(mapping).unwrap();
977        let super::CmapSubtable::Format4(format4) = format4 else {
978            panic!("O_o")
979        };
980        format4
981    }
982
983    // roundtrip the mapping from read-fonts
984    fn get_read_mapping(table: &super::Cmap4) -> Vec<(char, GlyphId)> {
985        let bytes = dump_table(table).unwrap();
986        let readcmap = read_fonts::tables::cmap::Cmap4::read(bytes.as_slice().into()).unwrap();
987
988        let mut mapping = readcmap
989            .iter()
990            .map(|(c, gid)| (char::from_u32(c).unwrap(), gid))
991            .collect::<Vec<_>>();
992        // cmap4 always ends with a 65535 => notdef entry. Sanity check
993        // this and then pop it to avoid messing with tests
994        assert_eq!(mapping.pop(), Some(('\u{FFFF}', GlyphId::NOTDEF)));
995        mapping
996    }
997
998    #[test]
999    fn f4_segment_len_one_uses_delta() {
1000        // if a segment is length one, we should always use the delta, since it's free.
1001        let mapping = MappingBuilder::default()
1002            .extend(['a', 'z', '1', '9'])
1003            .build();
1004
1005        let format4 = expect_f4(&mapping);
1006        assert_eq!(format4.end_code.len(), 5); // 4 + 0xffff
1007        assert!(format4.glyph_id_array.is_empty());
1008        assert!(format4.id_delta.iter().all(|d| *d != 0));
1009    }
1010
1011    #[test]
1012    fn f4_efficiency() {
1013        // one of these ranges should use id_delta, the other should use glyph id array
1014        let mapping = MappingBuilder::default()
1015            .extend('A'..='Z')
1016            .extend(('a'..='z').rev())
1017            .build();
1018
1019        let format4 = expect_f4(&mapping);
1020
1021        assert_eq!(
1022            format4.start_code,
1023            ['A' as u32 as u16, 'a' as u32 as u16, 0xffff]
1024        );
1025
1026        assert_eq!(
1027            format4.end_code,
1028            ['Z' as u32 as u16, 'z' as u32 as u16, 0xffff]
1029        );
1030
1031        assert_eq!(format4.id_delta, [-64, 0, 1]);
1032        assert_eq!(format4.id_range_offsets, [0, 4, 0]);
1033
1034        let read_mapping = get_read_mapping(&format4);
1035        assert_eq!(mapping.len(), read_mapping.len());
1036        assert!(mapping == read_mapping);
1037    }
1038
1039    #[test]
1040    fn f4_kinda_real_world() {
1041        // based on the first few hundred glyphs of oswald
1042        let mapping = MappingBuilder::default()
1043            .extend(['\r']) // CR
1044            .extend('\x20'..='\x7e') // ascii space to tilde
1045            .extend('\u{a0}'..='\u{ac}') // nbspace to logical not
1046            .extend('\u{ae}'..='\u{17f}') // registered to long s
1047            .extend(['\u{18f}', '\u{192}'])
1048            .build();
1049
1050        let format4 = expect_f4(&mapping);
1051        // we added 3 ranges + 3 individual glyphs above, + the final 0xffff
1052        assert_eq!(format4.end_code.len(), 7);
1053        let read_mapping = get_read_mapping(&format4);
1054
1055        assert_eq!(mapping.len(), read_mapping.len());
1056        assert!(mapping == read_mapping);
1057    }
1058
1059    #[test]
1060    // a small ordered segment between two larger unordered segments;
1061    // merging this correctly requires us to consider the next segment as well
1062    fn f4_sandwich_segment() {
1063        let mapping = MappingBuilder::default()
1064            .extend(['\r'])
1065            .extend(('\x20'..='\x27').rev()) // cost = 8*2 + 8 = 24
1066            .extend('\x28'..='\x2c') // cost = 8
1067            .extend(('\x2d'..='\x34').rev()) // cost = 6*2 + 8 = 20
1068            // combined =
1069            // (8 + 5 + 6) * 2 + 8 = 46
1070            .extend('\x35'..='\x3e')
1071            .build();
1072
1073        let format4 = expect_f4(&mapping);
1074        assert_eq!(format4.end_code.len(), 4);
1075    }
1076
1077    #[test]
1078    // ASCII from Adamina. In 0x3A..=0x7E the longest run of contiguous glyph
1079    // ids is 5, which costs more to split out than it saves. Merging any two
1080    // neighbouring segments doesn't help either, so a greedy merge (which we
1081    // used to do) leaves 9 segments where one is cheapest.
1082    fn f4_segments_adamina() {
1083        #[rustfmt::skip]
1084        let gids = [
1085            // space to slash
1086            175, 144, 150, 146, 180, 196, 198, 151, 159, 160, 138, 185, 143, 163, 147, 153,
1087            // digits
1088            120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
1089            // colon to at
1090            142, 152, 191, 189, 190, 148, 197,
1091            // A to Z
1092            1, 9, 10, 12, 14, 19, 20, 21, 22, 27, 28, 29, 30, 31, 33, 41, 43, 44, 45, 46, 47,
1093            52, 53, 54, 55, 57,
1094            // left bracket to grave
1095            157, 139, 158, 206, 154, 212,
1096            // a to z
1097            58, 66, 67, 69, 71, 76, 77, 78, 79, 85, 86, 87, 88, 89, 91, 99, 101, 102, 103, 105,
1098            106, 111, 112, 113, 114, 117,
1099            // left brace to tilde
1100            155, 204, 156, 193,
1101        ];
1102        let mappings = ('\x20'..='\x7e').zip(gids.map(GlyphId::new)).collect();
1103        let mut mapping = MappingBuilder {
1104            mappings,
1105            ..Default::default()
1106        };
1107
1108        assert_eq!(
1109            mapping.compute(),
1110            ['\x20'..='\x2f', '\x30'..='\x39', '\x3a'..='\x7e']
1111        );
1112    }
1113
1114    /// The cost of the cheapest segments for `mappings`, trying every split.
1115    ///
1116    /// This excludes the header and the final segment.
1117    fn cheapest_segments_cost(mappings: &[(char, GlyphId)]) -> usize {
1118        mappings
1119            .chunk_by(|(c1, _), (c2, _)| *c1 as u32 + 1 == *c2 as u32)
1120            .map(|run| {
1121                // best[k] is the cost of the cheapest segments for run[..k]
1122                let mut best = vec![0; run.len() + 1];
1123                for end in 1..=run.len() {
1124                    let cheapest = (0..end)
1125                        .map(|start| {
1126                            let segment = &run[start..end];
1127                            let use_delta = segment
1128                                .windows(2)
1129                                .all(|pair| pair[0].1.to_u32() + 1 == pair[1].1.to_u32());
1130                            let glyph_ids_cost = if use_delta { 0 } else { 2 * segment.len() };
1131                            best[start] + 8 + glyph_ids_cost
1132                        })
1133                        .min()
1134                        .unwrap();
1135                    best[end] = cheapest;
1136                }
1137                best[run.len()]
1138            })
1139            .sum()
1140    }
1141
1142    #[test]
1143    fn f4_segments_are_cheapest() {
1144        use rand::{rngs::StdRng, RngExt, SeedableRng};
1145
1146        // the header and the final segment
1147        const FIXED_COST: usize = 16 + 8;
1148
1149        let mut rng = StdRng::seed_from_u64(4);
1150        for _ in 0..500 {
1151            // how often each glyph id follows on from the previous one
1152            let p_contiguous = rng.random_range(0.0..1.0);
1153            let mut mappings = Vec::new();
1154            let (mut ch, mut gid) = (0x20, 1);
1155            for _ in 0..rng.random_range(1..40) {
1156                mappings.push((char::from_u32(ch).unwrap(), GlyphId::new(gid)));
1157                ch += if rng.random_bool(0.05) { 2 } else { 1 };
1158                gid = if rng.random_bool(p_contiguous) {
1159                    gid + 1
1160                } else {
1161                    rng.random_range(1..100)
1162                };
1163            }
1164
1165            let format4 = expect_f4(&mappings);
1166            assert_eq!(get_read_mapping(&format4), mappings);
1167            assert_eq!(
1168                dump_table(&format4).unwrap().len(),
1169                FIXED_COST + cheapest_segments_cost(&mappings),
1170                "{mappings:?}"
1171            );
1172        }
1173    }
1174
1175    // test that we correctly encode array lengths exceeding u16::MAX
1176    #[test]
1177    fn cmap12_length_calculation() {
1178        let more_than_16_bits = u16::MAX as u32 + 5;
1179        let groups = (0..more_than_16_bits)
1180            .map(|i| SequentialMapGroup::new(i, i, i))
1181            .collect();
1182        let cmap12 = Cmap12::new(0, groups);
1183        let bytes = crate::dump_table(&cmap12).unwrap();
1184        let read_it_back = Cmap12::read(bytes.as_slice().into()).unwrap();
1185        assert_eq!(read_it_back.groups.len() as u32, more_than_16_bits);
1186    }
1187
1188    #[test]
1189    fn cmap14_round_trip() {
1190        let font = read_fonts::FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
1191        let cmap = font.cmap().unwrap();
1192        let cmap14 = cmap
1193            .encoding_records()
1194            .iter()
1195            .find_map(|record| match record.subtable(cmap.offset_data()) {
1196                Ok(read_fonts::tables::cmap::CmapSubtable::Format14(table)) => Some(table),
1197                _ => None,
1198            })
1199            .unwrap();
1200        let owned: write::Cmap14 = cmap14.to_owned_table();
1201        let bytes = dump_table(&owned).unwrap();
1202        assert_eq!(
1203            bytes,
1204            &cmap14.offset_data().as_bytes()[..cmap14.length() as usize]
1205        );
1206    }
1207
1208    #[test]
1209    fn cmap15_round_trip() {
1210        use font_test_data::cmap::format15;
1211        let bytes = format15(0xe0100, &[65], &[(66, 65536), (67, 70000), (68, 0xffffff)]);
1212        let read = read_fonts::tables::cmap::Cmap15::read(bytes.as_slice().into()).unwrap();
1213        let owned: write::Cmap15 = read.to_owned_table();
1214        assert_eq!(dump_table(&owned).unwrap(), bytes);
1215        let cmap = write::Cmap::new(vec![write::EncodingRecord::new(
1216            PlatformId::Unicode,
1217            5,
1218            owned.into(),
1219        )]);
1220        let bytes = dump_table(&cmap).unwrap();
1221        let dmap = write::Dmap::new(cmap.encoding_records.clone());
1222        assert_eq!(dump_table(&dmap).unwrap(), bytes);
1223        let cmap = Cmap::read(bytes.as_slice().into()).unwrap();
1224        let (_, subtable) = cmap.uvs_subtable().unwrap();
1225        assert_eq!(
1226            subtable.map_variant(67u32, 0xe0100u32),
1227            Some(read_fonts::tables::cmap::MapVariant::Variant(GlyphId::new(
1228                70000
1229            )))
1230        );
1231    }
1232
1233    #[test]
1234    fn cmap15_offsets_and_record_widths() {
1235        let non_default = write::NonDefaultUvs24::new(vec![write::UvsMapping24::new(
1236            Uint24::new(0x4e08),
1237            Uint24::new(70000),
1238        )]);
1239        let cmap = write::Cmap15::new(vec![
1240            write::VariationSelector15::new(
1241                Uint24::new(0xe0100),
1242                Some(default_uvs()),
1243                Some(non_default.clone()),
1244            ),
1245            write::VariationSelector15::new(
1246                Uint24::new(0xe0101),
1247                Some(default_uvs()),
1248                Some(non_default.clone()),
1249            ),
1250            write::VariationSelector15::new(Uint24::new(0xe0102), None, Some(non_default)),
1251            write::VariationSelector15::new(Uint24::new(0xe0103), Some(default_uvs()), None),
1252        ]);
1253        let bytes = dump_table(&cmap).unwrap();
1254        let read = read_fonts::tables::cmap::Cmap15::read(bytes.as_slice().into()).unwrap();
1255        // 10-byte header + four 11-byte records, then 8-byte default and
1256        // 10-byte non-default UVS tables. Identical targets are not shared.
1257        let offsets: Vec<_> = read
1258            .var_selector()
1259            .iter()
1260            .flat_map(|record| {
1261                [
1262                    record.default_uvs_offset().offset().to_u32(),
1263                    record.non_default_uvs_offset().offset().to_u32(),
1264                ]
1265            })
1266            .collect();
1267        assert_eq!(offsets, [54, 62, 72, 80, 0, 90, 100, 0]);
1268        assert_eq!(read.length(), 108);
1269        assert_eq!(bytes.len(), 108);
1270        for selector in 0xe0100..=0xe0102u32 {
1271            assert_eq!(
1272                read_fonts::tables::cmap::UvsSubtable::from(read.clone())
1273                    .map_variant(0x4e08u32, selector),
1274                Some(read_fonts::tables::cmap::MapVariant::Variant(GlyphId::new(
1275                    70000
1276                )))
1277            );
1278        }
1279        let owned: write::Cmap15 = read.to_owned_table();
1280        assert_eq!(dump_table(&owned).unwrap(), bytes);
1281    }
1282
1283    fn default_uvs() -> write::DefaultUvs {
1284        write::DefaultUvs::new(1, vec![write::UnicodeRange::new(Uint24::new(0x4E00), 2)])
1285    }
1286
1287    fn non_default_uvs() -> write::NonDefaultUvs {
1288        write::NonDefaultUvs::new(1, vec![write::UvsMapping::new(Uint24::new(0x4E08), 25)])
1289    }
1290
1291    // returns the bytes and the offsets written in each record, checking that
1292    // the written 'length' matches the bytes
1293    fn dump_cmap14(cmap14: &write::Cmap14) -> (Vec<u8>, Vec<u32>) {
1294        let bytes = dump_table(cmap14).unwrap();
1295        let read = read_fonts::tables::cmap::Cmap14::read(bytes.as_slice().into()).unwrap();
1296        assert_eq!(read.length() as usize, bytes.len());
1297        let offsets = read
1298            .var_selector()
1299            .iter()
1300            .flat_map(|rec| [rec.default_uvs_offset(), rec.non_default_uvs_offset()])
1301            .map(|off| off.offset().to_u32())
1302            .collect();
1303        (bytes, offsets)
1304    }
1305
1306    // if we wrote the UVS tables as offset targets, the packer would dedup these
1307    #[test]
1308    fn cmap14_identical_uvs_tables_are_not_shared() {
1309        let cmap14 = write::Cmap14::new(vec![
1310            write::VariationSelector::new(
1311                Uint24::new(0xE0100),
1312                Some(default_uvs()),
1313                Some(non_default_uvs()),
1314            ),
1315            write::VariationSelector::new(
1316                Uint24::new(0xE0101),
1317                Some(default_uvs()),
1318                Some(non_default_uvs()),
1319            ),
1320        ]);
1321        let (bytes, offsets) = dump_cmap14(&cmap14);
1322        // 10 byte header + 2 * 11 byte records = 32, then a default table (8)
1323        // and a non-default table (9) per record
1324        assert_eq!(offsets, [32, 40, 49, 57]);
1325        assert_eq!(bytes.len(), 66);
1326    }
1327
1328    #[test]
1329    fn cmap14_null_offsets() {
1330        let cmap14 = write::Cmap14::new(vec![
1331            write::VariationSelector::new(Uint24::new(0xE0100), None, Some(non_default_uvs())),
1332            write::VariationSelector::new(Uint24::new(0xE0101), Some(default_uvs()), None),
1333        ]);
1334        let (bytes, offsets) = dump_cmap14(&cmap14);
1335        // missing tables take no space, and the tables are written in record order
1336        assert_eq!(offsets, [0, 32, 41, 0]);
1337        let tail = [
1338            dump_table(&non_default_uvs()).unwrap(),
1339            dump_table(&default_uvs()).unwrap(),
1340        ]
1341        .concat();
1342        assert_eq!(&bytes[32..], tail);
1343    }
1344
1345    fn cmap4_has_a_unique_final_segment<I>(mappings: I)
1346    where
1347        I: IntoIterator<Item = (char, GlyphId)>,
1348    {
1349        let cmap = crate::tables::cmap::Cmap::from_mappings(mappings).unwrap();
1350        for record in &cmap.encoding_records {
1351            let crate::tables::cmap::CmapSubtable::Format4(cmap4) = &*record.subtable else {
1352                continue;
1353            };
1354            if !matches!((cmap4.start_code.as_slice(), cmap4.end_code.as_slice()),
1355                (&[.., before_last_start, 0xFFFF], &[.., before_last_end, 0xFFFF])
1356                    if before_last_start != 0xFFFF || before_last_end != 0xFFFF)
1357            {
1358                panic!(
1359                    "Expected cmap4 to end with a single (0xFFFF, 0xFFFF) segment, but found {:?}",
1360                    (&cmap4.start_code, &cmap4.end_code)
1361                );
1362            }
1363        }
1364    }
1365
1366    #[test]
1367    fn cmap4_final_segment_is_not_duplicated() {
1368        cmap4_has_a_unique_final_segment([('a', GlyphId::new(1)), ('\u{FFFF}', GlyphId::new(0))]);
1369    }
1370
1371    #[test]
1372    fn cmap4_final_segment_is_added_even_if_last_generated_segment_ends_with_0xffff() {
1373        cmap4_has_a_unique_final_segment([
1374            ('a', GlyphId::new(1)),
1375            ('\u{FFFE}', GlyphId::new(0)),
1376            ('\u{FFFF}', GlyphId::new(0)),
1377        ]);
1378    }
1379
1380    type RecordKey = (PlatformId, u16, u16);
1381
1382    // write the cmap and read it back, returning the (platform, encoding, format)
1383    // of each encoding record along with the mappings in its subtable
1384    fn read_back(cmap: &write::Cmap) -> Vec<(RecordKey, Vec<(char, GlyphId)>)> {
1385        let bytes = dump_table(cmap).unwrap();
1386        let font_data = FontData::new(&bytes);
1387        let cmap = Cmap::read(font_data).unwrap();
1388        cmap.encoding_records()
1389            .iter()
1390            .map(|rec| {
1391                let subtable = rec.subtable(font_data).unwrap();
1392                let mut mappings = subtable
1393                    .iter()
1394                    .map(|(c, gid)| (char::from_u32(c).unwrap(), gid))
1395                    .collect::<Vec<_>>();
1396                if subtable.format() == 4 {
1397                    // cmap4 always ends with a 65535 => notdef entry
1398                    assert_eq!(mappings.pop(), Some(('\u{FFFF}', GlyphId::NOTDEF)));
1399                }
1400                let key = (rec.platform_id(), rec.encoding_id(), subtable.format());
1401                (key, mappings)
1402            })
1403            .collect()
1404    }
1405
1406    // assert that we emit only format 12, containing all the mappings
1407    fn assert_format_12_only(mut mappings: Vec<(char, GlyphId)>) {
1408        let cmap = write::Cmap::from_mappings(mappings.clone()).unwrap();
1409        mappings.sort();
1410        mappings.dedup();
1411        assert_eq!(
1412            read_back(&cmap),
1413            [
1414                (
1415                    (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING, 12),
1416                    mappings.clone()
1417                ),
1418                (
1419                    (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING, 12),
1420                    mappings
1421                ),
1422            ]
1423        );
1424    }
1425
1426    // one contiguous run of BMP chars with reversed glyph ids, which has to
1427    // be stored in the glyph id array, and is too long for format 4
1428    fn too_long_for_format_4() -> MappingBuilder {
1429        MappingBuilder::default().extend(('\u{1000}'..='\u{AC3F}').rev())
1430    }
1431
1432    #[test]
1433    fn omit_format_4_if_too_long() {
1434        let mappings = too_long_for_format_4().build();
1435        assert_eq!(mappings.len(), 40_000);
1436        assert_eq!(
1437            super::CmapSubtable::create_format_4(&mappings),
1438            // two segments, and a glyph id for each mapping
1439            Err(super::Format4Error::LengthOverflow(16 + 2 * 8 + 40_000 * 2))
1440        );
1441        assert_format_12_only(mappings);
1442    }
1443
1444    #[test]
1445    fn omit_format_4_if_too_long_with_supplementary_chars() {
1446        let mappings = too_long_for_format_4()
1447            .extend(['\u{1F600}', '\u{1F602}', '\u{20000}'])
1448            .build();
1449        assert_format_12_only(mappings);
1450    }
1451
1452    #[test]
1453    fn omit_format_4_if_bmp_glyph_id_too_large() {
1454        let mut mappings = simple_cmap_mappings();
1455        let big_gid = ('\u{4E00}', GlyphId::new(70_000));
1456        mappings.push(big_gid);
1457        mappings.sort();
1458        assert_eq!(
1459            super::CmapSubtable::create_format_4(&mappings),
1460            Err(super::Format4Error::GlyphIdOverflow {
1461                ch: big_gid.0,
1462                gid: big_gid.1
1463            })
1464        );
1465        assert_format_12_only(mappings);
1466    }
1467
1468    #[test]
1469    fn empty_mappings() {
1470        let cmap = write::Cmap::from_mappings([]).unwrap();
1471        assert!(read_back(&cmap).is_empty());
1472    }
1473
1474    #[test]
1475    fn large_supplementary_glyph_id_keeps_format_4() {
1476        let mut mappings = bmp_and_non_bmp_cmap_mappings();
1477        mappings.push(('\u{20000}', GlyphId::new(70_000)));
1478        mappings.sort();
1479        mappings.dedup();
1480        let bmp_mappings = simple_cmap_mappings();
1481
1482        let cmap = write::Cmap::from_mappings(mappings.clone()).unwrap();
1483        assert_eq!(
1484            read_back(&cmap),
1485            [
1486                (
1487                    (PlatformId::Unicode, UNICODE_BMP_ENCODING, 4),
1488                    bmp_mappings.clone()
1489                ),
1490                (
1491                    (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING, 12),
1492                    mappings.clone()
1493                ),
1494                ((PlatformId::Windows, WINDOWS_BMP_ENCODING, 4), bmp_mappings),
1495                (
1496                    (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING, 12),
1497                    mappings
1498                ),
1499            ]
1500        );
1501    }
1502
1503    #[test]
1504    fn oversized_cmap4_fails_validation() {
1505        // a segment covering U+0000..=U+7FFF using the glyph id array, then
1506        // the final segment
1507        let cmap4 = write::Cmap4::new(
1508            0,
1509            vec![0x7FFF, 0xFFFF],
1510            vec![0, 0xFFFF],
1511            vec![0, 1],
1512            vec![4, 0],
1513            (1..=0x8000).collect(),
1514        );
1515        assert!(matches!(
1516            dump_table(&cmap4),
1517            Err(crate::error::Error::ValidationFailed(_))
1518        ));
1519
1520        let cmap = write::Cmap::new(vec![write::EncodingRecord::new(
1521            PlatformId::Windows,
1522            WINDOWS_BMP_ENCODING,
1523            cmap4.into(),
1524        )]);
1525        assert!(matches!(
1526            dump_table(&cmap),
1527            Err(crate::error::Error::ValidationFailed(_))
1528        ));
1529    }
1530}