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// https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#windows-platform-platform-id--3
12const WINDOWS_BMP_ENCODING: u16 = 1;
13const WINDOWS_FULL_REPERTOIRE_ENCODING: u16 = 10;
14
15// https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#unicode-platform-platform-id--0
16const UNICODE_BMP_ENCODING: u16 = 3;
17const UNICODE_FULL_REPERTOIRE_ENCODING: u16 = 4;
18
19impl CmapSubtable {
20    /// Create a new format 4 subtable
21    ///
22    /// Returns `None` if none of the input chars are in the BMP (i.e. have
23    /// codepoints <= 0xFFFF.)
24    ///
25    /// Invariants:
26    ///
27    /// - Inputs must be sorted and deduplicated.
28    /// - All `GlyphId`s must be 16-bit
29    fn create_format_4(mappings: &[(char, GlyphId)]) -> Option<Self> {
30        let mut end_code = Vec::with_capacity(mappings.len() + 1);
31        let mut start_code = Vec::with_capacity(mappings.len() + 1);
32        let mut id_deltas = Vec::with_capacity(mappings.len() + 1);
33        let mut id_range_offsets = Vec::with_capacity(mappings.len() + 1);
34        let mut glyph_ids = Vec::new();
35
36        let segments = Format4SegmentComputer::new(mappings).compute();
37        assert!(mappings.iter().all(|(_, g)| g.to_u32() <= 0xFFFF));
38        if segments.is_empty() {
39            // no chars in BMP
40            return None;
41        }
42        let add_final_segment = segments.last().is_none_or(|seg| {
43            (mappings[seg.start_ix].0, mappings[seg.end_ix].0) != ('\u{FFFF}', '\u{FFFF}')
44        });
45        let n_segments = segments.len() + add_final_segment as usize;
46        for (i, segment) in segments.into_iter().enumerate() {
47            let start = mappings[segment.start_ix].0;
48            let end = mappings[segment.end_ix].0;
49            start_code.push(start as u32 as u16);
50            end_code.push(end as u32 as u16);
51            if let Some(delta) = segment.id_delta {
52                // "The idDelta arithmetic is modulo 65536":
53                let delta = i16::try_from(delta)
54                    .unwrap_or_else(|_| delta.rem_euclid(0x10000).try_into().unwrap());
55                id_deltas.push(delta);
56                id_range_offsets.push(0u16);
57            } else {
58                // if the deltas for a range are not identical, we rely on the
59                // explicit glyph_ids array.
60                //
61                // The logic here is based on the memory layout of the table:
62                // because the glyph_id array follows the id_range_offsets array,
63                // the id_range_offsets array essentially stores a memory offset.
64                let current_n_ids = glyph_ids.len();
65                let n_following_segments = n_segments - i;
66                // number of bytes from the id_range_offset value to the glyph id
67                // for this segment, in the glyph_ids array
68                let id_range_offset = (n_following_segments + current_n_ids) * u16::RAW_BYTE_LEN;
69                id_deltas.push(0);
70                id_range_offsets.push(id_range_offset.try_into().unwrap());
71                glyph_ids.extend(
72                    mappings[segment.start_ix..=segment.end_ix]
73                        .iter()
74                        .map(|(_, gid)| u16::try_from(gid.to_u32()).expect("checked before now")),
75                )
76            }
77        }
78
79        // add the final segment:
80        if add_final_segment {
81            end_code.push(0xFFFF);
82            start_code.push(0xFFFF);
83            id_deltas.push(1);
84            id_range_offsets.push(0);
85        }
86
87        Some(Self::format_4(
88            0,
89            end_code,
90            start_code,
91            id_deltas,
92            id_range_offsets,
93            glyph_ids,
94        ))
95    }
96
97    /// Create a new format 12 `CmapSubtable` from a list of `(char, GlyphId)` pairs.
98    ///
99    /// The pairs are expected to be already sorted by chars.
100    /// In case of duplicate chars, the last one wins.
101    fn create_format_12(mappings: &[(char, GlyphId)]) -> Self {
102        let (mut char_codes, gids): (Vec<u32>, Vec<u32>) = mappings
103            .iter()
104            .map(|(cp, gid)| (*cp as u32, gid.to_u32()))
105            .unzip();
106        let cmap: HashMap<_, _> = char_codes.iter().cloned().zip(gids).collect();
107        char_codes.dedup();
108
109        // we know we have at least one non-BMP char_code > 0xFFFF so unwrap is safe
110        let mut start_char_code = *char_codes.first().unwrap();
111        let mut start_glyph_id = cmap[&start_char_code];
112        let mut last_glyph_id = start_glyph_id.wrapping_sub(1);
113        let mut last_char_code = start_char_code.wrapping_sub(1);
114        let mut groups = Vec::new();
115        for char_code in char_codes {
116            let glyph_id = cmap[&char_code];
117            if glyph_id != last_glyph_id.wrapping_add(1)
118                || char_code != last_char_code.wrapping_add(1)
119            {
120                groups.push((start_char_code, last_char_code, start_glyph_id));
121                start_char_code = char_code;
122                start_glyph_id = glyph_id;
123            }
124            last_glyph_id = glyph_id;
125            last_char_code = char_code;
126        }
127        groups.push((start_char_code, last_char_code, start_glyph_id));
128
129        let seq_map_groups = groups
130            .into_iter()
131            .map(|(start_char, end_char, gid)| SequentialMapGroup::new(start_char, end_char, gid))
132            .collect::<Vec<_>>();
133        CmapSubtable::format_12(
134            0, // 'lang' set to zero for all 'cmap' subtables whose platform IDs are other than Macintosh
135            seq_map_groups,
136        )
137    }
138}
139
140/// A conflicting Cmap definition, one char is mapped to multiple distinct GlyphIds.
141///
142/// If there are multiple conflicting mappings, one is chosen arbitrarily.
143/// gid1 is less than gid2.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct CmapConflict {
146    ch: char,
147    gid1: GlyphId,
148    gid2: GlyphId,
149}
150
151impl std::fmt::Display for CmapConflict {
152    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153        let ch32 = self.ch as u32;
154        write!(
155            f,
156            "Cannot map {:?} (U+{ch32:04X}) to two different glyph ids: {} and {}",
157            self.ch, self.gid1, self.gid2
158        )
159    }
160}
161
162impl std::error::Error for CmapConflict {}
163
164impl Cmap {
165    /// Generates a ['cmap'] that is expected to work in most modern environments.
166    ///
167    /// The input is not required to be sorted.
168    ///
169    /// This emits [format 4] and [format 12] subtables, respectively for the
170    /// Basic Multilingual Plane and Full Unicode Repertoire.
171    ///
172    /// Also see: <https://learn.microsoft.com/en-us/typography/opentype/spec/recom#cmap-table>
173    ///
174    /// [`cmap`]: https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
175    /// [format 4]: https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
176    /// [format 12]: https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-12-segmented-coverage
177    pub fn from_mappings(
178        mappings: impl IntoIterator<Item = (char, GlyphId)>,
179    ) -> Result<Cmap, CmapConflict> {
180        let mut mappings: Vec<_> = mappings.into_iter().collect();
181        mappings.sort();
182        mappings.dedup();
183        if let Some((ch, gid1, gid2)) =
184            mappings
185                .iter()
186                .zip(mappings.iter().skip(1))
187                .find_map(|((c1, g1), (c2, g2))| {
188                    (c1 == c2 && g1 != g2).then(|| (*c1, *g1.min(g2), *g1.max(g2)))
189                })
190        {
191            return Err(CmapConflict { ch, gid1, gid2 });
192        }
193
194        let mut uni_records = Vec::new(); // platform 0
195        let mut win_records = Vec::new(); // platform 3
196
197        // if there are characters in the Unicode Basic Multilingual Plane (U+0000 to U+FFFF)
198        // we need to emit format 4 subtables
199        let bmp_subtable = CmapSubtable::create_format_4(&mappings);
200        if let Some(bmp_subtable) = bmp_subtable {
201            // Absent a strong signal to do otherwise, match fontmake/fonttools
202            // Since both Windows and Unicode platform tables use the same subtable they are
203            // almost entirely byte-shared
204            // See https://github.com/googlefonts/fontmake-rs/issues/251
205            uni_records.push(EncodingRecord::new(
206                PlatformId::Unicode,
207                UNICODE_BMP_ENCODING,
208                bmp_subtable.clone(),
209            ));
210            win_records.push(EncodingRecord::new(
211                PlatformId::Windows,
212                WINDOWS_BMP_ENCODING,
213                bmp_subtable,
214            ));
215        }
216
217        // If there are any supplementary-plane characters (U+10000 to U+10FFFF) we also
218        // emit format 12 subtables
219        if mappings.iter().any(|(cp, _)| *cp > '\u{FFFF}') {
220            let full_repertoire_subtable = CmapSubtable::create_format_12(&mappings);
221            // format 12 subtables are also going to be byte-shared, just like above
222            uni_records.push(EncodingRecord::new(
223                PlatformId::Unicode,
224                UNICODE_FULL_REPERTOIRE_ENCODING,
225                full_repertoire_subtable.clone(),
226            ));
227            win_records.push(EncodingRecord::new(
228                PlatformId::Windows,
229                WINDOWS_FULL_REPERTOIRE_ENCODING,
230                full_repertoire_subtable,
231            ));
232        }
233
234        // put encoding records in order of (platform id, encoding id):
235        // - Unicode (0), BMP (3)
236        // - Unicode (0), full repertoire (4)
237        // - Windows (3), BMP (1)
238        // - Windows (3), full repertoire (10)
239        Ok(Cmap::new(
240            uni_records.into_iter().chain(win_records).collect(),
241        ))
242    }
243}
244
245// a helper for computing efficient segments for cmap format 4
246struct Format4SegmentComputer<'a> {
247    mappings: &'a [(char, GlyphId)],
248    /// The start index of the current segment, during iteration
249    seg_start: usize,
250    /// tracks whether the current segment has ordered gids
251    gids_in_order: bool,
252}
253
254#[derive(Clone, Copy, Debug)]
255struct Format4Segment {
256    // indices are into the source mappings
257    start_ix: usize,
258    end_ix: usize,
259    start_char: char,
260    end_char: char,
261    id_delta: Option<i32>,
262}
263
264impl Format4Segment {
265    fn len(&self) -> usize {
266        self.end_ix - self.start_ix + 1
267    }
268
269    // cost in bytes of this segment.
270    fn cost(&self) -> usize {
271        // a segment always costs 4 u16s (end, start, delta_id, id_range_offset)
272        const BASE_COST: usize = 4 * u16::RAW_BYTE_LEN;
273
274        if self.id_delta.is_some() {
275            BASE_COST
276        } else {
277            // and if there is not a common id_delta, we also need to add an item
278            // to the glyph_id_array for each char in the segment
279            BASE_COST + self.len() * u16::RAW_BYTE_LEN
280        }
281    }
282
283    /// `true` if we can merge other into self (other must follow self)
284    fn can_combine(&self, next: &Self) -> bool {
285        self.end_char as u32 + 1 == next.start_char as u32
286    }
287
288    /// Return `true` if we should combine this segment with the previous one.
289    ///
290    /// The case that matters here is when there is a segment with contiguous
291    /// GIDs and with a char range that is immediately adjacent to the previous
292    /// segment.
293    fn should_combine(&self, prev: &Self, next: Option<&Self>) -> bool {
294        if !prev.can_combine(self) {
295            return false;
296        }
297
298        // first we just consider the previous item. If our combined cost
299        // is lower than our separate cost, we will merge.
300        let combined_cost = prev.combine(self).cost();
301        let separate_cost = prev.cost() + self.cost();
302
303        if combined_cost < separate_cost {
304            return true;
305        }
306
307        // finally, if we are also char-contiguous with the next segment,
308        // then by construction it means if we merge now we will also merge
309        // with the next segment (since this current gid-contiguous segment
310        // is the reason we aren't all one big segment already) and so we need
311        // to also check that.
312        //
313        // Although the implementation is different, the logic is very similar in
314        // fonttools: https://github.com/fonttools/fonttools/blob/081d6a27ab8/Lib/fontTools/ttLib/tables/_c_m_a_p.py#L828
315        //
316        // As an example, consider a segment with 5 contiguous gids.
317        //
318        // This segment costs 8 bytes to encode; because the gids are contiguous
319        // we can use the `id_delta` field to represent them all.
320        //
321        // As an example, consider the following three segments:
322        //
323        // chrs [1 2] [3 4 5 6 7] [8 9]
324        // GIDs [3 1] [4 5 6 7 8] [2 9]
325        // cost   12       8       12
326        //
327        // the first and last segments each have len == 2. The GIDs are not
328        // contiguous, so they have to be encoded individually, which costs
329        // 2 bytes each. This means the total cost of these segments is 12:
330        // 8-bytes for the segment data, and 4 bytes for the gids.
331        //
332        // The middle segment has len == 5, but the GIDs are contiguous. This
333        // means that we can represent all the gids using the delta_id part of
334        // the segment, and encode the whole segment for 8 bytes.
335        //
336        // If we combine the first two segments, the new segment costs 22:
337        // 8 bytes for the segment, and 14 bytes for the 7 glyphs. This is
338        // more than the 20 bytes they cost separately.
339        //
340        // If we combine all three, though, the total cost is 26 (we add two
341        // more entries to the glyph_id array), which is better than the 32 bytes
342        // they cost separately.
343        //
344        // (note that we don't need to explicitly combine the next segment;
345        // it will happen automatically during the next loop)
346        if let Some(next) = next.filter(|next| self.can_combine(next)) {
347            let combined_cost = prev.combine(self).combine(next).cost();
348            let separate_cost = separate_cost + next.cost();
349            return combined_cost < separate_cost;
350        }
351
352        false
353    }
354
355    /// Combine this segment with one that immediately follows it.
356    ///
357    /// The caller must ensure that the two segments are contiguous.
358    fn combine(&self, next: &Format4Segment) -> Format4Segment {
359        assert_eq!(next.start_ix, self.end_ix + 1,);
360        Format4Segment {
361            start_ix: self.start_ix,
362            start_char: self.start_char,
363            end_char: next.end_char,
364            end_ix: next.end_ix,
365            id_delta: None,
366        }
367    }
368}
369
370impl<'a> Format4SegmentComputer<'a> {
371    fn new(mappings: &'a [(char, GlyphId)]) -> Self {
372        // ignore chars above BMP:
373        let mappings = mappings
374            .iter()
375            .position(|(c, _)| u16::try_from(*c as u32).is_err())
376            .map(|bad_idx| &mappings[..bad_idx])
377            .unwrap_or(mappings);
378        Self {
379            mappings,
380            seg_start: 0,
381            gids_in_order: false,
382        }
383    }
384
385    /// a convenience method called from our iter in the various cases where
386    /// we emit a segment.
387    ///
388    /// a 'seg_len' of 0 means start == end, e.g. a segment of one glyph.
389    fn make_segment(&mut self, seg_len: usize) -> Format4Segment {
390        // if start == end, we should always use a delta.
391        let use_delta = self.gids_in_order || seg_len == 0;
392        let start_ix = self.seg_start;
393        let end_ix = self.seg_start + seg_len;
394        let start_char = self.mappings[start_ix].0;
395        let end_char = self.mappings[end_ix].0;
396        let result = Format4Segment {
397            start_ix,
398            end_ix,
399            start_char,
400            end_char,
401            id_delta: self
402                .mappings
403                .get(self.seg_start)
404                .map(|(cp, gid)| gid.to_u32() as i32 - *cp as u32 as i32)
405                .filter(|_| use_delta),
406        };
407        self.seg_start += seg_len + 1;
408        self.gids_in_order = false;
409        result
410    }
411
412    /// Find the next possible segment.
413    ///
414    /// A segment _must_ be a contiguous range of chars, but we where such a range
415    /// contains subranges that are also contiguous ranges of glyph ids, we will
416    /// split those subranges into separate segments.
417    fn next_possible_segment(&mut self) -> Option<Format4Segment> {
418        if self.seg_start == self.mappings.len() {
419            return None;
420        }
421
422        let Some(((mut prev_cp, mut prev_gid), rest)) =
423            self.mappings[self.seg_start..].split_first()
424        else {
425            // if this is the last element, make a final segment
426            return Some(self.make_segment(0));
427        };
428
429        for (i, (cp, gid)) in rest.iter().enumerate() {
430            // first: all segments must be a contiguous range of codepoints
431            if *cp as u32 != prev_cp as u32 + 1 {
432                return Some(self.make_segment(i));
433            }
434            let next_gid_is_in_order = prev_gid.to_u32() + 1 == gid.to_u32();
435            if !next_gid_is_in_order {
436                // next: if prev gids were ordered but this one isn't, end prev segment
437                if self.gids_in_order {
438                    return Some(self.make_segment(i));
439                }
440            // and the funny case:
441            // if gids were not previously ordered but are now:
442            // - if i == 0, then this is the first item in a new segment;
443            //   set gids_in_order and continue
444            // - if i > 0, we need to back up one
445            } else if !self.gids_in_order {
446                if i == 0 {
447                    self.gids_in_order = true;
448                } else {
449                    return Some(self.make_segment(i - 1));
450                }
451            }
452            prev_cp = *cp;
453            prev_gid = *gid;
454        }
455
456        // if we're done looping then create the last segment:
457        let last_idx = self.mappings.len() - 1;
458        Some(self.make_segment(last_idx - self.seg_start))
459    }
460
461    /// Compute an efficient set of segments.
462    ///
463    /// - A segment is a contiguous range of chars.
464    /// - If all the chars in a segment share a common delta to their glyph ids,
465    ///   we can encode them much more efficiently
466    /// - it's possible for a contiguous range of chars to contain a subrange
467    ///   that share a common delta, where the overall range does not, e.g.
468    ///
469    ///   ```text
470    ///   [a b c d e f g]
471    ///   [9 3 6 7 8 2 1]
472    ///   ```
473    ///   (here a-g is a range containing the subrange c-e, which have a common
474    ///   delta.)
475    ///
476    /// This leads us to a reasonably intuitive algorithm: we start by greedily
477    /// splitting ranges up so we can consider all subranges with common deltas;
478    /// then we look at these one at a time, and combine them back together if
479    /// doing so saves space.
480    ///
481    /// This differs from the python, which starts from larger segments and then
482    /// subdivides them, but the overall idea is the same.
483    ///
484    /// <https://github.com/fonttools/fonttools/blob/f1d3e116d54f/Lib/fontTools/ttLib/tables/_c_m_a_p.py#L783>
485    fn compute(mut self) -> Vec<Format4Segment> {
486        let Some(first) = self.next_possible_segment() else {
487            return Default::default();
488        };
489
490        let mut result = vec![first];
491
492        // now we want to collect the segments, combining smaller segments where
493        // that leads to a size savings.
494        let mut next = self.next_possible_segment();
495
496        while let Some(current) = next.take() {
497            next = self.next_possible_segment();
498            let prev = result.last_mut().unwrap();
499            if current.should_combine(prev, next.as_ref()) {
500                *prev = prev.combine(&current);
501                continue;
502            }
503
504            result.push(current);
505        }
506        result
507    }
508}
509
510impl Cmap4 {
511    fn compute_length(&self) -> u16 {
512        // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
513        // there are always 8 u16 fields
514        const FIXED_SIZE: usize = 8 * u16::RAW_BYTE_LEN;
515        const PER_SEGMENT_LEN: usize = 4 * u16::RAW_BYTE_LEN;
516
517        let segment_len = self.end_code.len() * PER_SEGMENT_LEN;
518        let gid_len = self.glyph_id_array.len() * u16::RAW_BYTE_LEN;
519
520        (FIXED_SIZE + segment_len + gid_len)
521            .try_into()
522            .expect("cmap4 overflow")
523    }
524
525    fn compute_search_range(&self) -> u16 {
526        SearchRange::compute(self.end_code.len(), u16::RAW_BYTE_LEN).search_range
527    }
528
529    fn compute_entry_selector(&self) -> u16 {
530        SearchRange::compute(self.end_code.len(), u16::RAW_BYTE_LEN).entry_selector
531    }
532
533    fn compute_range_shift(&self) -> u16 {
534        SearchRange::compute(self.end_code.len(), u16::RAW_BYTE_LEN).range_shift
535    }
536}
537
538impl Cmap12 {
539    fn compute_length(&self) -> u32 {
540        // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-12-segmented-coverage
541        const FIXED_SIZE: usize = 2 * u16::RAW_BYTE_LEN + 3 * u32::RAW_BYTE_LEN;
542        const PER_SEGMENT_LEN: usize = 3 * u32::RAW_BYTE_LEN;
543
544        (FIXED_SIZE + PER_SEGMENT_LEN * self.groups.len())
545            .try_into()
546            .unwrap()
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use std::ops::RangeInclusive;
553
554    use font_types::GlyphId;
555    use read_fonts::{
556        tables::cmap::{Cmap, CmapSubtable, PlatformId},
557        FontData, FontRead,
558    };
559
560    use crate::{
561        dump_table,
562        tables::cmap::{
563            self as write, CmapConflict, UNICODE_BMP_ENCODING, UNICODE_FULL_REPERTOIRE_ENCODING,
564            WINDOWS_BMP_ENCODING, WINDOWS_FULL_REPERTOIRE_ENCODING,
565        },
566    };
567
568    use super::{Cmap12, SequentialMapGroup};
569
570    fn assert_generates_simple_cmap(mappings: Vec<(char, GlyphId)>) {
571        let cmap = write::Cmap::from_mappings(mappings).unwrap();
572
573        let bytes = dump_table(&cmap).unwrap();
574        let font_data = FontData::new(&bytes);
575        let cmap = Cmap::read(font_data).unwrap();
576
577        assert_eq!(
578            2,
579            cmap.encoding_records().len(),
580            "{:?}",
581            cmap.encoding_records()
582        );
583        assert_eq!(
584            vec![
585                (PlatformId::Unicode, UNICODE_BMP_ENCODING),
586                (PlatformId::Windows, WINDOWS_BMP_ENCODING)
587            ],
588            cmap.encoding_records()
589                .iter()
590                .map(|er| (er.platform_id(), er.encoding_id()))
591                .collect::<Vec<_>>()
592        );
593
594        for encoding_record in cmap.encoding_records() {
595            let CmapSubtable::Format4(cmap4) = encoding_record.subtable(font_data).unwrap() else {
596                panic!("Expected a cmap4 in {encoding_record:?}");
597            };
598
599            // The spec example says entry_selector 4 but the calculation it gives seems to yield 2 (?)
600            assert_eq!(
601                (8, 8, 2, 0),
602                (
603                    cmap4.seg_count_x2(),
604                    cmap4.search_range(),
605                    cmap4.entry_selector(),
606                    cmap4.range_shift()
607                )
608            );
609            assert_eq!(cmap4.start_code(), &[10u16, 30u16, 153u16, 0xffffu16]);
610            assert_eq!(cmap4.end_code(), &[20u16, 90u16, 480u16, 0xffffu16]);
611            // The example starts at gid 1, we're starting at 0
612            assert_eq!(cmap4.id_delta(), &[-10i16, -19i16, -81i16, 1i16]);
613            assert_eq!(cmap4.id_range_offsets(), &[0u16, 0u16, 0u16, 0u16]);
614        }
615    }
616
617    fn simple_cmap_mappings() -> Vec<(char, GlyphId)> {
618        (10..=20)
619            .chain(30..=90)
620            .chain(153..=480)
621            .enumerate()
622            .map(|(idx, codepoint)| (char::from_u32(codepoint).unwrap(), GlyphId::new(idx as u32)))
623            .collect()
624    }
625
626    // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
627    // "map characters 10-20, 30-90, and 153-480 onto a contiguous range of glyph indices"
628    #[test]
629    fn generate_simple_cmap4() {
630        let mappings = simple_cmap_mappings();
631        assert_generates_simple_cmap(mappings);
632    }
633
634    #[test]
635    fn generate_cmap4_out_of_order_input() {
636        let mut ordered = simple_cmap_mappings();
637        let mut disordered = Vec::new();
638        while !ordered.is_empty() {
639            if ordered.len() % 2 == 0 {
640                disordered.insert(0, ordered.remove(0));
641            } else {
642                disordered.push(ordered.remove(0));
643            }
644        }
645        assert_ne!(ordered, disordered);
646        assert_generates_simple_cmap(disordered);
647    }
648
649    #[test]
650    fn generate_cmap4_large_values() {
651        let mut mappings = simple_cmap_mappings();
652        // Example from Texturina.
653        let codepoint = char::from_u32(0xa78b).unwrap();
654        let gid = GlyphId::new(153);
655        mappings.push((codepoint, gid));
656
657        let cmap = write::Cmap::from_mappings(mappings).unwrap();
658
659        let bytes = dump_table(&cmap).unwrap();
660        let font_data = FontData::new(&bytes);
661        let cmap = Cmap::read(font_data).unwrap();
662        assert_eq!(cmap.map_codepoint(codepoint), Some(gid));
663    }
664
665    #[test]
666    fn bytes_are_reused() {
667        // We emit extra encoding records assuming it's cheap. Make sure.
668        let mappings = simple_cmap_mappings();
669        let cmap_both = write::Cmap::from_mappings(mappings).unwrap();
670        assert_eq!(2, cmap_both.encoding_records.len(), "{cmap_both:?}");
671
672        let bytes_for_both = dump_table(&cmap_both).unwrap().len();
673
674        for i in 0..cmap_both.encoding_records.len() {
675            let mut cmap = cmap_both.clone();
676            cmap.encoding_records.remove(i);
677            let bytes_for_one = dump_table(&cmap).unwrap().len();
678            assert_eq!(bytes_for_one + 8, bytes_for_both);
679        }
680    }
681
682    fn non_bmp_cmap_mappings() -> Vec<(char, GlyphId)> {
683        // contains four sequential map groups
684        vec![
685            // first group
686            ('\u{1f12f}', GlyphId::new(481)),
687            ('\u{1f130}', GlyphId::new(482)),
688            // char 0x1f131 skipped, starts second group
689            ('\u{1f132}', GlyphId::new(483)),
690            ('\u{1f133}', GlyphId::new(484)),
691            // gid 485 skipped, starts third group
692            ('\u{1f134}', GlyphId::new(486)),
693            // char 0x1f135 skipped, starts fourth group. identical duplicate bindings are fine
694            ('\u{1f136}', GlyphId::new(488)),
695            ('\u{1f136}', GlyphId::new(488)),
696        ]
697    }
698
699    fn bmp_and_non_bmp_cmap_mappings() -> Vec<(char, GlyphId)> {
700        let mut mappings = simple_cmap_mappings();
701        mappings.extend(non_bmp_cmap_mappings());
702        mappings
703    }
704
705    fn assert_cmap12_groups(
706        font_data: FontData,
707        cmap: &Cmap,
708        record_index: usize,
709        expected: &[(u32, u32, u32)],
710    ) {
711        let rec = &cmap.encoding_records()[record_index];
712        let CmapSubtable::Format12(subtable) = rec.subtable(font_data).unwrap() else {
713            panic!("Expected a cmap12 in {rec:?}");
714        };
715        let groups = subtable
716            .groups()
717            .iter()
718            .map(|g| (g.start_char_code(), g.end_char_code(), g.start_glyph_id()))
719            .collect::<Vec<_>>();
720        assert_eq!(groups.len(), expected.len());
721        assert_eq!(groups, expected);
722    }
723
724    #[test]
725    fn generate_cmap4_and_12() {
726        let mappings = bmp_and_non_bmp_cmap_mappings();
727
728        let cmap = write::Cmap::from_mappings(mappings).unwrap();
729
730        let bytes = dump_table(&cmap).unwrap();
731        let font_data = FontData::new(&bytes);
732        let cmap = Cmap::read(font_data).unwrap();
733
734        assert_eq!(
735            4,
736            cmap.encoding_records().len(),
737            "{:?}",
738            cmap.encoding_records()
739        );
740        assert_eq!(
741            vec![
742                (PlatformId::Unicode, UNICODE_BMP_ENCODING),
743                (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING),
744                (PlatformId::Windows, WINDOWS_BMP_ENCODING),
745                (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING)
746            ],
747            cmap.encoding_records()
748                .iter()
749                .map(|er| (er.platform_id(), er.encoding_id()))
750                .collect::<Vec<_>>()
751        );
752
753        let encoding_records = cmap.encoding_records();
754        let first_rec = &encoding_records[0];
755        assert!(
756            matches!(
757                first_rec.subtable(font_data).unwrap(),
758                CmapSubtable::Format4(_)
759            ),
760            "Expected a cmap4 in {first_rec:?}"
761        );
762
763        // (start_char_code, end_char_code, start_glyph_id)
764        let expected_groups = vec![
765            (10, 20, 0),
766            (30, 90, 11),
767            (153, 480, 72),
768            (0x1f12f, 0x1f130, 481),
769            (0x1f132, 0x1f133, 483),
770            (0x1f134, 0x1f134, 486),
771            (0x1f136, 0x1f136, 488),
772        ];
773        assert_cmap12_groups(font_data, &cmap, 1, &expected_groups);
774        assert_cmap12_groups(font_data, &cmap, 3, &expected_groups);
775    }
776
777    #[test]
778    fn generate_cmap12_only() {
779        let mappings = non_bmp_cmap_mappings();
780
781        let cmap = write::Cmap::from_mappings(mappings).unwrap();
782
783        let bytes = dump_table(&cmap).unwrap();
784        let font_data = FontData::new(&bytes);
785        let cmap = Cmap::read(font_data).unwrap();
786
787        assert_eq!(
788            2,
789            cmap.encoding_records().len(),
790            "{:?}",
791            cmap.encoding_records()
792        );
793        assert_eq!(
794            vec![
795                (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING),
796                (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING)
797            ],
798            cmap.encoding_records()
799                .iter()
800                .map(|er| (er.platform_id(), er.encoding_id()))
801                .collect::<Vec<_>>()
802        );
803
804        // (start_char_code, end_char_code, start_glyph_id)
805        let expected_groups = vec![
806            (0x1f12f, 0x1f130, 481),
807            (0x1f132, 0x1f133, 483),
808            (0x1f134, 0x1f134, 486),
809            (0x1f136, 0x1f136, 488),
810        ];
811        assert_cmap12_groups(font_data, &cmap, 0, &expected_groups);
812        assert_cmap12_groups(font_data, &cmap, 1, &expected_groups);
813    }
814
815    #[test]
816    fn multiple_mappings_fails() {
817        let mut mappings = non_bmp_cmap_mappings();
818        // add an additional mapping to a different glyphId
819        let (ch, gid1) = mappings[0];
820        let gid2 = GlyphId::new(gid1.to_u32() + 1);
821        mappings.push((ch, gid2));
822
823        let result = write::Cmap::from_mappings(mappings);
824
825        assert_eq!(result, Err(CmapConflict { ch, gid1, gid2 }))
826    }
827
828    struct MappingBuilder {
829        mappings: Vec<(char, GlyphId)>,
830        next_gid: u16,
831    }
832
833    impl Default for MappingBuilder {
834        fn default() -> Self {
835            Self {
836                mappings: Default::default(),
837                next_gid: 1,
838            }
839        }
840    }
841
842    impl MappingBuilder {
843        fn extend(mut self, range: impl IntoIterator<Item = char>) -> Self {
844            for c in range {
845                let gid = GlyphId::new(self.next_gid as _);
846                self.mappings.push((c, gid));
847                self.next_gid += 1;
848            }
849            self
850        }
851
852        // compute the segments for the mapping
853        fn compute(&mut self) -> Vec<RangeInclusive<char>> {
854            self.mappings.sort();
855            super::Format4SegmentComputer::new(&self.mappings)
856                .compute()
857                .into_iter()
858                .map(|seg| self.mappings[seg.start_ix].0..=self.mappings[seg.end_ix].0)
859                .collect()
860        }
861
862        fn build(mut self) -> Vec<(char, GlyphId)> {
863            self.mappings.sort();
864            self.mappings
865        }
866    }
867
868    #[test]
869    fn f4_segments_simple() {
870        let mut one_big_discontiguous_mapping = MappingBuilder::default().extend(('a'..='z').rev());
871        assert_eq!(one_big_discontiguous_mapping.compute(), ['a'..='z']);
872    }
873
874    #[test]
875    fn f4_segments_combine_small() {
876        let mut mapping = MappingBuilder::default()
877            // backwards so gids are not contiguous
878            .extend(['e', 'd', 'c', 'b', 'a'])
879            // these two contiguous ranges aren't worth the cost, should merge
880            // into the first and last respectively
881            .extend('f'..='g')
882            .extend('m'..='n')
883            .extend(('o'..='z').rev());
884
885        assert_eq!(mapping.compute(), ['a'..='g', 'm'..='z']);
886    }
887
888    #[test]
889    fn f4_segments_keep() {
890        let mut mapping = MappingBuilder::default()
891            .extend('a'..='m')
892            .extend(['o', 'n']);
893
894        assert_eq!(mapping.compute(), ['a'..='m', 'n'..='o']);
895    }
896
897    fn expect_f4(mapping: &[(char, GlyphId)]) -> super::Cmap4 {
898        let format4 = super::CmapSubtable::create_format_4(mapping).unwrap();
899        let super::CmapSubtable::Format4(format4) = format4 else {
900            panic!("O_o")
901        };
902        format4
903    }
904
905    // roundtrip the mapping from read-fonts
906    fn get_read_mapping(table: &super::Cmap4) -> Vec<(char, GlyphId)> {
907        let bytes = dump_table(table).unwrap();
908        let readcmap = read_fonts::tables::cmap::Cmap4::read(bytes.as_slice().into()).unwrap();
909
910        let mut mapping = readcmap
911            .iter()
912            .map(|(c, gid)| (char::from_u32(c).unwrap(), gid))
913            .collect::<Vec<_>>();
914        // cmap4 always ends with a 65535 => notdef entry. Sanity check
915        // this and then pop it to avoid messing with tests
916        assert_eq!(mapping.pop(), Some(('\u{FFFF}', GlyphId::NOTDEF)));
917        mapping
918    }
919
920    #[test]
921    fn f4_segment_len_one_uses_delta() {
922        // if a segment is length one, we should always use the delta, since it's free.
923        let mapping = MappingBuilder::default()
924            .extend(['a', 'z', '1', '9'])
925            .build();
926
927        let format4 = expect_f4(&mapping);
928        assert_eq!(format4.end_code.len(), 5); // 4 + 0xffff
929        assert!(format4.glyph_id_array.is_empty());
930        assert!(format4.id_delta.iter().all(|d| *d != 0));
931    }
932
933    #[test]
934    fn f4_efficiency() {
935        // one of these ranges should use id_delta, the other should use glyph id array
936        let mapping = MappingBuilder::default()
937            .extend('A'..='Z')
938            .extend(('a'..='z').rev())
939            .build();
940
941        let format4 = expect_f4(&mapping);
942
943        assert_eq!(
944            format4.start_code,
945            ['A' as u32 as u16, 'a' as u32 as u16, 0xffff]
946        );
947
948        assert_eq!(
949            format4.end_code,
950            ['Z' as u32 as u16, 'z' as u32 as u16, 0xffff]
951        );
952
953        assert_eq!(format4.id_delta, [-64, 0, 1]);
954        assert_eq!(format4.id_range_offsets, [0, 4, 0]);
955
956        let read_mapping = get_read_mapping(&format4);
957        assert_eq!(mapping.len(), read_mapping.len());
958        assert!(mapping == read_mapping);
959    }
960
961    #[test]
962    fn f4_kinda_real_world() {
963        // based on the first few hundred glyphs of oswald
964        let mapping = MappingBuilder::default()
965            .extend(['\r']) // CR
966            .extend('\x20'..='\x7e') // ascii space to tilde
967            .extend('\u{a0}'..='\u{ac}') // nbspace to logical not
968            .extend('\u{ae}'..='\u{17f}') // registered to long s
969            .extend(['\u{18f}', '\u{192}'])
970            .build();
971
972        let format4 = expect_f4(&mapping);
973        // we added 3 ranges + 3 individual glyphs above, + the final 0xffff
974        assert_eq!(format4.end_code.len(), 7);
975        let read_mapping = get_read_mapping(&format4);
976
977        assert_eq!(mapping.len(), read_mapping.len());
978        assert!(mapping == read_mapping);
979    }
980
981    #[test]
982    // a small ordered segment between two larger unordered segments;
983    // merging this correctly requires us to consider the next segment as well
984    fn f4_sandwich_segment() {
985        let mapping = MappingBuilder::default()
986            .extend(['\r'])
987            .extend(('\x20'..='\x27').rev()) // cost = 8*2 + 8 = 24
988            .extend('\x28'..='\x2c') // cost = 8
989            .extend(('\x2d'..='\x34').rev()) // cost = 6*2 + 8 = 20
990            // combined =
991            // (8 + 5 + 6) * 2 + 8 = 46
992            .extend('\x35'..='\x3e')
993            .build();
994
995        let format4 = expect_f4(&mapping);
996        assert_eq!(format4.end_code.len(), 4);
997    }
998
999    // test that we correctly encode array lengths exceeding u16::MAX
1000    #[test]
1001    fn cmap12_length_calculation() {
1002        let more_than_16_bits = u16::MAX as u32 + 5;
1003        let groups = (0..more_than_16_bits)
1004            .map(|i| SequentialMapGroup::new(i, i, i))
1005            .collect();
1006        let cmap12 = Cmap12::new(0, groups);
1007        let bytes = crate::dump_table(&cmap12).unwrap();
1008        let read_it_back = Cmap12::read(bytes.as_slice().into()).unwrap();
1009        assert_eq!(read_it_back.groups.len() as u32, more_than_16_bits);
1010    }
1011
1012    fn cmap4_has_a_unique_final_segment<I>(mappings: I)
1013    where
1014        I: IntoIterator<Item = (char, GlyphId)>,
1015    {
1016        let cmap = crate::tables::cmap::Cmap::from_mappings(mappings).unwrap();
1017        for record in &cmap.encoding_records {
1018            let crate::tables::cmap::CmapSubtable::Format4(cmap4) = &*record.subtable else {
1019                continue;
1020            };
1021            if !matches!((cmap4.start_code.as_slice(), cmap4.end_code.as_slice()),
1022                (&[.., before_last_start, 0xFFFF], &[.., before_last_end, 0xFFFF])
1023                    if before_last_start != 0xFFFF || before_last_end != 0xFFFF)
1024            {
1025                panic!(
1026                    "Expected cmap4 to end with a single (0xFFFF, 0xFFFF) segment, but found {:?}",
1027                    (&cmap4.start_code, &cmap4.end_code)
1028                );
1029            }
1030        }
1031    }
1032
1033    #[test]
1034    fn cmap4_final_segment_is_not_duplicated() {
1035        cmap4_has_a_unique_final_segment([('a', GlyphId::new(1)), ('\u{FFFF}', GlyphId::new(0))]);
1036    }
1037
1038    #[test]
1039    fn cmap4_final_segment_is_added_even_if_last_generated_segment_ends_with_0xffff() {
1040        cmap4_has_a_unique_final_segment([
1041            ('a', GlyphId::new(1)),
1042            ('\u{FFFE}', GlyphId::new(0)),
1043            ('\u{FFFF}', GlyphId::new(0)),
1044        ]);
1045    }
1046}