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// We write the UVS tables inline, immediately after the record array, rather
551// than as offset targets that the packer lays out.
552//
553// This is because of the 'length' field, which has to cover the UVS tables
554// as well as the header: fonttools slices the subtable by it before
555// decompiling, and ots requires it to be exact. write-fonts can't promise
556// that for offset targets: the packer is free to reorder subtables, and
557// identical subtables are deduplicated. In practice the default sort emits a
558// table's children directly after it, and cmap's 32-bit offsets never trigger
559// the fallback sort, so it would happen to work out; but writing the tables
560// ourselves makes the layout (and so 'length') correct by construction.
561impl FontWrite for Cmap14 {
562    fn write_into(&self, writer: &mut TableWriter) {
563        14u16.write_into(writer);
564        self.compute_length().write_into(writer);
565        u32::try_from(self.var_selector.len())
566            .unwrap()
567            .write_into(writer);
568
569        // offsets are relative to the start of this subtable; the UVS tables
570        // follow the record array, in record order, default before non-default
571        let mut next_offset = Self::HEADER_LEN + self.var_selector.len() * Self::RECORD_LEN;
572        let mut write_offset = |writer: &mut TableWriter, len: Option<usize>| match len {
573            Some(len) => {
574                (next_offset as u32).write_into(writer);
575                next_offset += len;
576            }
577            None => 0u32.write_into(writer),
578        };
579        for record in &self.var_selector {
580            record.var_selector.write_into(writer);
581            write_offset(
582                writer,
583                record.default_uvs.as_ref().map(DefaultUvs::compute_len),
584            );
585            write_offset(
586                writer,
587                record
588                    .non_default_uvs
589                    .as_ref()
590                    .map(NonDefaultUvs::compute_len),
591            );
592        }
593        for record in &self.var_selector {
594            if let Some(uvs) = record.default_uvs.as_ref() {
595                uvs.write_into(writer);
596            }
597            if let Some(uvs) = record.non_default_uvs.as_ref() {
598                uvs.write_into(writer);
599            }
600        }
601    }
602
603    fn table_type(&self) -> TableType {
604        TableType::Named("Cmap14")
605    }
606}
607
608impl Cmap14 {
609    // format, length, numVarSelectorRecords
610    const HEADER_LEN: usize = u16::RAW_BYTE_LEN + 2 * u32::RAW_BYTE_LEN;
611    // varSelector, defaultUVSOffset, nonDefaultUVSOffset
612    const RECORD_LEN: usize = Uint24::RAW_BYTE_LEN + 2 * u32::RAW_BYTE_LEN;
613
614    fn compute_length(&self) -> u32 {
615        // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-14-unicode-variation-sequences
616        let uvs_len: usize = self
617            .var_selector
618            .iter()
619            .map(|record| {
620                record
621                    .default_uvs
622                    .as_ref()
623                    .map(DefaultUvs::compute_len)
624                    .unwrap_or(0)
625                    + record
626                        .non_default_uvs
627                        .as_ref()
628                        .map(NonDefaultUvs::compute_len)
629                        .unwrap_or(0)
630            })
631            .sum();
632
633        (Self::HEADER_LEN + self.var_selector.len() * Self::RECORD_LEN + uvs_len)
634            .try_into()
635            .expect("cmap14 overflow")
636    }
637}
638
639impl DefaultUvs {
640    fn compute_len(&self) -> usize {
641        // numUnicodeValueRanges, then (startUnicodeValue, additionalCount) per range
642        u32::RAW_BYTE_LEN + self.ranges.len() * (Uint24::RAW_BYTE_LEN + u8::RAW_BYTE_LEN)
643    }
644}
645
646impl NonDefaultUvs {
647    fn compute_len(&self) -> usize {
648        // numUVSMappings, then (unicodeValue, glyphID) per mapping
649        u32::RAW_BYTE_LEN + self.uvs_mapping.len() * (Uint24::RAW_BYTE_LEN + u16::RAW_BYTE_LEN)
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use std::ops::RangeInclusive;
656
657    use font_types::{GlyphId, Uint24};
658    use read_fonts::{
659        tables::cmap::{Cmap, CmapSubtable, PlatformId},
660        FontData, FontRead, TableProvider,
661    };
662
663    use crate::{
664        dump_table,
665        from_obj::ToOwnedTable,
666        tables::cmap::{
667            self as write, CmapConflict, UNICODE_BMP_ENCODING, UNICODE_FULL_REPERTOIRE_ENCODING,
668            WINDOWS_BMP_ENCODING, WINDOWS_FULL_REPERTOIRE_ENCODING,
669        },
670    };
671
672    use super::{Cmap12, SequentialMapGroup};
673
674    fn assert_generates_simple_cmap(mappings: Vec<(char, GlyphId)>) {
675        let cmap = write::Cmap::from_mappings(mappings).unwrap();
676
677        let bytes = dump_table(&cmap).unwrap();
678        let font_data = FontData::new(&bytes);
679        let cmap = Cmap::read(font_data).unwrap();
680
681        assert_eq!(
682            2,
683            cmap.encoding_records().len(),
684            "{:?}",
685            cmap.encoding_records()
686        );
687        assert_eq!(
688            vec![
689                (PlatformId::Unicode, UNICODE_BMP_ENCODING),
690                (PlatformId::Windows, WINDOWS_BMP_ENCODING)
691            ],
692            cmap.encoding_records()
693                .iter()
694                .map(|er| (er.platform_id(), er.encoding_id()))
695                .collect::<Vec<_>>()
696        );
697
698        for encoding_record in cmap.encoding_records() {
699            let CmapSubtable::Format4(cmap4) = encoding_record.subtable(font_data).unwrap() else {
700                panic!("Expected a cmap4 in {encoding_record:?}");
701            };
702
703            // The spec example says entry_selector 4 but the calculation it gives seems to yield 2 (?)
704            assert_eq!(
705                (8, 8, 2, 0),
706                (
707                    cmap4.seg_count_x2(),
708                    cmap4.search_range(),
709                    cmap4.entry_selector(),
710                    cmap4.range_shift()
711                )
712            );
713            assert_eq!(cmap4.start_code(), &[10u16, 30u16, 153u16, 0xffffu16]);
714            assert_eq!(cmap4.end_code(), &[20u16, 90u16, 480u16, 0xffffu16]);
715            // The example starts at gid 1, we're starting at 0
716            assert_eq!(cmap4.id_delta(), &[-10i16, -19i16, -81i16, 1i16]);
717            assert_eq!(cmap4.id_range_offsets(), &[0u16, 0u16, 0u16, 0u16]);
718        }
719    }
720
721    fn simple_cmap_mappings() -> Vec<(char, GlyphId)> {
722        (10..=20)
723            .chain(30..=90)
724            .chain(153..=480)
725            .enumerate()
726            .map(|(idx, codepoint)| (char::from_u32(codepoint).unwrap(), GlyphId::new(idx as u32)))
727            .collect()
728    }
729
730    // https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
731    // "map characters 10-20, 30-90, and 153-480 onto a contiguous range of glyph indices"
732    #[test]
733    fn generate_simple_cmap4() {
734        let mappings = simple_cmap_mappings();
735        assert_generates_simple_cmap(mappings);
736    }
737
738    #[test]
739    fn generate_cmap4_out_of_order_input() {
740        let mut ordered = simple_cmap_mappings();
741        let mut disordered = Vec::new();
742        while !ordered.is_empty() {
743            if ordered.len() % 2 == 0 {
744                disordered.insert(0, ordered.remove(0));
745            } else {
746                disordered.push(ordered.remove(0));
747            }
748        }
749        assert_ne!(ordered, disordered);
750        assert_generates_simple_cmap(disordered);
751    }
752
753    #[test]
754    fn generate_cmap4_large_values() {
755        let mut mappings = simple_cmap_mappings();
756        // Example from Texturina.
757        let codepoint = char::from_u32(0xa78b).unwrap();
758        let gid = GlyphId::new(153);
759        mappings.push((codepoint, gid));
760
761        let cmap = write::Cmap::from_mappings(mappings).unwrap();
762
763        let bytes = dump_table(&cmap).unwrap();
764        let font_data = FontData::new(&bytes);
765        let cmap = Cmap::read(font_data).unwrap();
766        assert_eq!(cmap.map_codepoint(codepoint), Some(gid));
767    }
768
769    #[test]
770    fn bytes_are_reused() {
771        // We emit extra encoding records assuming it's cheap. Make sure.
772        let mappings = simple_cmap_mappings();
773        let cmap_both = write::Cmap::from_mappings(mappings).unwrap();
774        assert_eq!(2, cmap_both.encoding_records.len(), "{cmap_both:?}");
775
776        let bytes_for_both = dump_table(&cmap_both).unwrap().len();
777
778        for i in 0..cmap_both.encoding_records.len() {
779            let mut cmap = cmap_both.clone();
780            cmap.encoding_records.remove(i);
781            let bytes_for_one = dump_table(&cmap).unwrap().len();
782            assert_eq!(bytes_for_one + 8, bytes_for_both);
783        }
784    }
785
786    fn non_bmp_cmap_mappings() -> Vec<(char, GlyphId)> {
787        // contains four sequential map groups
788        vec![
789            // first group
790            ('\u{1f12f}', GlyphId::new(481)),
791            ('\u{1f130}', GlyphId::new(482)),
792            // char 0x1f131 skipped, starts second group
793            ('\u{1f132}', GlyphId::new(483)),
794            ('\u{1f133}', GlyphId::new(484)),
795            // gid 485 skipped, starts third group
796            ('\u{1f134}', GlyphId::new(486)),
797            // char 0x1f135 skipped, starts fourth group. identical duplicate bindings are fine
798            ('\u{1f136}', GlyphId::new(488)),
799            ('\u{1f136}', GlyphId::new(488)),
800        ]
801    }
802
803    fn bmp_and_non_bmp_cmap_mappings() -> Vec<(char, GlyphId)> {
804        let mut mappings = simple_cmap_mappings();
805        mappings.extend(non_bmp_cmap_mappings());
806        mappings
807    }
808
809    fn assert_cmap12_groups(
810        font_data: FontData,
811        cmap: &Cmap,
812        record_index: usize,
813        expected: &[(u32, u32, u32)],
814    ) {
815        let rec = &cmap.encoding_records()[record_index];
816        let CmapSubtable::Format12(subtable) = rec.subtable(font_data).unwrap() else {
817            panic!("Expected a cmap12 in {rec:?}");
818        };
819        let groups = subtable
820            .groups()
821            .iter()
822            .map(|g| (g.start_char_code(), g.end_char_code(), g.start_glyph_id()))
823            .collect::<Vec<_>>();
824        assert_eq!(groups.len(), expected.len());
825        assert_eq!(groups, expected);
826    }
827
828    #[test]
829    fn generate_cmap4_and_12() {
830        let mappings = bmp_and_non_bmp_cmap_mappings();
831
832        let cmap = write::Cmap::from_mappings(mappings).unwrap();
833
834        let bytes = dump_table(&cmap).unwrap();
835        let font_data = FontData::new(&bytes);
836        let cmap = Cmap::read(font_data).unwrap();
837
838        assert_eq!(
839            4,
840            cmap.encoding_records().len(),
841            "{:?}",
842            cmap.encoding_records()
843        );
844        assert_eq!(
845            vec![
846                (PlatformId::Unicode, UNICODE_BMP_ENCODING),
847                (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING),
848                (PlatformId::Windows, WINDOWS_BMP_ENCODING),
849                (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING)
850            ],
851            cmap.encoding_records()
852                .iter()
853                .map(|er| (er.platform_id(), er.encoding_id()))
854                .collect::<Vec<_>>()
855        );
856
857        let encoding_records = cmap.encoding_records();
858        let first_rec = &encoding_records[0];
859        assert!(
860            matches!(
861                first_rec.subtable(font_data).unwrap(),
862                CmapSubtable::Format4(_)
863            ),
864            "Expected a cmap4 in {first_rec:?}"
865        );
866
867        // (start_char_code, end_char_code, start_glyph_id)
868        let expected_groups = vec![
869            (10, 20, 0),
870            (30, 90, 11),
871            (153, 480, 72),
872            (0x1f12f, 0x1f130, 481),
873            (0x1f132, 0x1f133, 483),
874            (0x1f134, 0x1f134, 486),
875            (0x1f136, 0x1f136, 488),
876        ];
877        assert_cmap12_groups(font_data, &cmap, 1, &expected_groups);
878        assert_cmap12_groups(font_data, &cmap, 3, &expected_groups);
879    }
880
881    #[test]
882    fn generate_cmap12_only() {
883        let mappings = non_bmp_cmap_mappings();
884
885        let cmap = write::Cmap::from_mappings(mappings).unwrap();
886
887        let bytes = dump_table(&cmap).unwrap();
888        let font_data = FontData::new(&bytes);
889        let cmap = Cmap::read(font_data).unwrap();
890
891        assert_eq!(
892            2,
893            cmap.encoding_records().len(),
894            "{:?}",
895            cmap.encoding_records()
896        );
897        assert_eq!(
898            vec![
899                (PlatformId::Unicode, UNICODE_FULL_REPERTOIRE_ENCODING),
900                (PlatformId::Windows, WINDOWS_FULL_REPERTOIRE_ENCODING)
901            ],
902            cmap.encoding_records()
903                .iter()
904                .map(|er| (er.platform_id(), er.encoding_id()))
905                .collect::<Vec<_>>()
906        );
907
908        // (start_char_code, end_char_code, start_glyph_id)
909        let expected_groups = vec![
910            (0x1f12f, 0x1f130, 481),
911            (0x1f132, 0x1f133, 483),
912            (0x1f134, 0x1f134, 486),
913            (0x1f136, 0x1f136, 488),
914        ];
915        assert_cmap12_groups(font_data, &cmap, 0, &expected_groups);
916        assert_cmap12_groups(font_data, &cmap, 1, &expected_groups);
917    }
918
919    #[test]
920    fn multiple_mappings_fails() {
921        let mut mappings = non_bmp_cmap_mappings();
922        // add an additional mapping to a different glyphId
923        let (ch, gid1) = mappings[0];
924        let gid2 = GlyphId::new(gid1.to_u32() + 1);
925        mappings.push((ch, gid2));
926
927        let result = write::Cmap::from_mappings(mappings);
928
929        assert_eq!(result, Err(CmapConflict { ch, gid1, gid2 }))
930    }
931
932    struct MappingBuilder {
933        mappings: Vec<(char, GlyphId)>,
934        next_gid: u16,
935    }
936
937    impl Default for MappingBuilder {
938        fn default() -> Self {
939            Self {
940                mappings: Default::default(),
941                next_gid: 1,
942            }
943        }
944    }
945
946    impl MappingBuilder {
947        fn extend(mut self, range: impl IntoIterator<Item = char>) -> Self {
948            for c in range {
949                let gid = GlyphId::new(self.next_gid as _);
950                self.mappings.push((c, gid));
951                self.next_gid += 1;
952            }
953            self
954        }
955
956        // compute the segments for the mapping
957        fn compute(&mut self) -> Vec<RangeInclusive<char>> {
958            self.mappings.sort();
959            super::Format4SegmentComputer::new(&self.mappings)
960                .compute()
961                .into_iter()
962                .map(|seg| self.mappings[seg.start_ix].0..=self.mappings[seg.end_ix].0)
963                .collect()
964        }
965
966        fn build(mut self) -> Vec<(char, GlyphId)> {
967            self.mappings.sort();
968            self.mappings
969        }
970    }
971
972    #[test]
973    fn f4_segments_simple() {
974        let mut one_big_discontiguous_mapping = MappingBuilder::default().extend(('a'..='z').rev());
975        assert_eq!(one_big_discontiguous_mapping.compute(), ['a'..='z']);
976    }
977
978    #[test]
979    fn f4_segments_combine_small() {
980        let mut mapping = MappingBuilder::default()
981            // backwards so gids are not contiguous
982            .extend(['e', 'd', 'c', 'b', 'a'])
983            // these two contiguous ranges aren't worth the cost, should merge
984            // into the first and last respectively
985            .extend('f'..='g')
986            .extend('m'..='n')
987            .extend(('o'..='z').rev());
988
989        assert_eq!(mapping.compute(), ['a'..='g', 'm'..='z']);
990    }
991
992    #[test]
993    fn f4_segments_keep() {
994        let mut mapping = MappingBuilder::default()
995            .extend('a'..='m')
996            .extend(['o', 'n']);
997
998        assert_eq!(mapping.compute(), ['a'..='m', 'n'..='o']);
999    }
1000
1001    fn expect_f4(mapping: &[(char, GlyphId)]) -> super::Cmap4 {
1002        let format4 = super::CmapSubtable::create_format_4(mapping).unwrap();
1003        let super::CmapSubtable::Format4(format4) = format4 else {
1004            panic!("O_o")
1005        };
1006        format4
1007    }
1008
1009    // roundtrip the mapping from read-fonts
1010    fn get_read_mapping(table: &super::Cmap4) -> Vec<(char, GlyphId)> {
1011        let bytes = dump_table(table).unwrap();
1012        let readcmap = read_fonts::tables::cmap::Cmap4::read(bytes.as_slice().into()).unwrap();
1013
1014        let mut mapping = readcmap
1015            .iter()
1016            .map(|(c, gid)| (char::from_u32(c).unwrap(), gid))
1017            .collect::<Vec<_>>();
1018        // cmap4 always ends with a 65535 => notdef entry. Sanity check
1019        // this and then pop it to avoid messing with tests
1020        assert_eq!(mapping.pop(), Some(('\u{FFFF}', GlyphId::NOTDEF)));
1021        mapping
1022    }
1023
1024    #[test]
1025    fn f4_segment_len_one_uses_delta() {
1026        // if a segment is length one, we should always use the delta, since it's free.
1027        let mapping = MappingBuilder::default()
1028            .extend(['a', 'z', '1', '9'])
1029            .build();
1030
1031        let format4 = expect_f4(&mapping);
1032        assert_eq!(format4.end_code.len(), 5); // 4 + 0xffff
1033        assert!(format4.glyph_id_array.is_empty());
1034        assert!(format4.id_delta.iter().all(|d| *d != 0));
1035    }
1036
1037    #[test]
1038    fn f4_efficiency() {
1039        // one of these ranges should use id_delta, the other should use glyph id array
1040        let mapping = MappingBuilder::default()
1041            .extend('A'..='Z')
1042            .extend(('a'..='z').rev())
1043            .build();
1044
1045        let format4 = expect_f4(&mapping);
1046
1047        assert_eq!(
1048            format4.start_code,
1049            ['A' as u32 as u16, 'a' as u32 as u16, 0xffff]
1050        );
1051
1052        assert_eq!(
1053            format4.end_code,
1054            ['Z' as u32 as u16, 'z' as u32 as u16, 0xffff]
1055        );
1056
1057        assert_eq!(format4.id_delta, [-64, 0, 1]);
1058        assert_eq!(format4.id_range_offsets, [0, 4, 0]);
1059
1060        let read_mapping = get_read_mapping(&format4);
1061        assert_eq!(mapping.len(), read_mapping.len());
1062        assert!(mapping == read_mapping);
1063    }
1064
1065    #[test]
1066    fn f4_kinda_real_world() {
1067        // based on the first few hundred glyphs of oswald
1068        let mapping = MappingBuilder::default()
1069            .extend(['\r']) // CR
1070            .extend('\x20'..='\x7e') // ascii space to tilde
1071            .extend('\u{a0}'..='\u{ac}') // nbspace to logical not
1072            .extend('\u{ae}'..='\u{17f}') // registered to long s
1073            .extend(['\u{18f}', '\u{192}'])
1074            .build();
1075
1076        let format4 = expect_f4(&mapping);
1077        // we added 3 ranges + 3 individual glyphs above, + the final 0xffff
1078        assert_eq!(format4.end_code.len(), 7);
1079        let read_mapping = get_read_mapping(&format4);
1080
1081        assert_eq!(mapping.len(), read_mapping.len());
1082        assert!(mapping == read_mapping);
1083    }
1084
1085    #[test]
1086    // a small ordered segment between two larger unordered segments;
1087    // merging this correctly requires us to consider the next segment as well
1088    fn f4_sandwich_segment() {
1089        let mapping = MappingBuilder::default()
1090            .extend(['\r'])
1091            .extend(('\x20'..='\x27').rev()) // cost = 8*2 + 8 = 24
1092            .extend('\x28'..='\x2c') // cost = 8
1093            .extend(('\x2d'..='\x34').rev()) // cost = 6*2 + 8 = 20
1094            // combined =
1095            // (8 + 5 + 6) * 2 + 8 = 46
1096            .extend('\x35'..='\x3e')
1097            .build();
1098
1099        let format4 = expect_f4(&mapping);
1100        assert_eq!(format4.end_code.len(), 4);
1101    }
1102
1103    // test that we correctly encode array lengths exceeding u16::MAX
1104    #[test]
1105    fn cmap12_length_calculation() {
1106        let more_than_16_bits = u16::MAX as u32 + 5;
1107        let groups = (0..more_than_16_bits)
1108            .map(|i| SequentialMapGroup::new(i, i, i))
1109            .collect();
1110        let cmap12 = Cmap12::new(0, groups);
1111        let bytes = crate::dump_table(&cmap12).unwrap();
1112        let read_it_back = Cmap12::read(bytes.as_slice().into()).unwrap();
1113        assert_eq!(read_it_back.groups.len() as u32, more_than_16_bits);
1114    }
1115
1116    #[test]
1117    fn cmap14_round_trip() {
1118        let font = read_fonts::FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
1119        let (_, cmap14) = font.cmap().unwrap().uvs_subtable().unwrap();
1120        let owned: write::Cmap14 = cmap14.to_owned_table();
1121        let bytes = dump_table(&owned).unwrap();
1122        assert_eq!(
1123            bytes,
1124            &cmap14.offset_data().as_bytes()[..cmap14.length() as usize]
1125        );
1126    }
1127
1128    fn default_uvs() -> write::DefaultUvs {
1129        write::DefaultUvs::new(1, vec![write::UnicodeRange::new(Uint24::new(0x4E00), 2)])
1130    }
1131
1132    fn non_default_uvs() -> write::NonDefaultUvs {
1133        write::NonDefaultUvs::new(1, vec![write::UvsMapping::new(Uint24::new(0x4E08), 25)])
1134    }
1135
1136    // returns the bytes and the offsets written in each record, checking that
1137    // the written 'length' matches the bytes
1138    fn dump_cmap14(cmap14: &write::Cmap14) -> (Vec<u8>, Vec<u32>) {
1139        let bytes = dump_table(cmap14).unwrap();
1140        let read = read_fonts::tables::cmap::Cmap14::read(bytes.as_slice().into()).unwrap();
1141        assert_eq!(read.length() as usize, bytes.len());
1142        let offsets = read
1143            .var_selector()
1144            .iter()
1145            .flat_map(|rec| [rec.default_uvs_offset(), rec.non_default_uvs_offset()])
1146            .map(|off| off.offset().to_u32())
1147            .collect();
1148        (bytes, offsets)
1149    }
1150
1151    // if we wrote the UVS tables as offset targets, the packer would dedup these
1152    #[test]
1153    fn cmap14_identical_uvs_tables_are_not_shared() {
1154        let cmap14 = write::Cmap14::new(vec![
1155            write::VariationSelector::new(
1156                Uint24::new(0xE0100),
1157                Some(default_uvs()),
1158                Some(non_default_uvs()),
1159            ),
1160            write::VariationSelector::new(
1161                Uint24::new(0xE0101),
1162                Some(default_uvs()),
1163                Some(non_default_uvs()),
1164            ),
1165        ]);
1166        let (bytes, offsets) = dump_cmap14(&cmap14);
1167        // 10 byte header + 2 * 11 byte records = 32, then a default table (8)
1168        // and a non-default table (9) per record
1169        assert_eq!(offsets, [32, 40, 49, 57]);
1170        assert_eq!(bytes.len(), 66);
1171    }
1172
1173    #[test]
1174    fn cmap14_null_offsets() {
1175        let cmap14 = write::Cmap14::new(vec![
1176            write::VariationSelector::new(Uint24::new(0xE0100), None, Some(non_default_uvs())),
1177            write::VariationSelector::new(Uint24::new(0xE0101), Some(default_uvs()), None),
1178        ]);
1179        let (bytes, offsets) = dump_cmap14(&cmap14);
1180        // missing tables take no space, and the tables are written in record order
1181        assert_eq!(offsets, [0, 32, 41, 0]);
1182        let tail = [
1183            dump_table(&non_default_uvs()).unwrap(),
1184            dump_table(&default_uvs()).unwrap(),
1185        ]
1186        .concat();
1187        assert_eq!(&bytes[32..], tail);
1188    }
1189
1190    fn cmap4_has_a_unique_final_segment<I>(mappings: I)
1191    where
1192        I: IntoIterator<Item = (char, GlyphId)>,
1193    {
1194        let cmap = crate::tables::cmap::Cmap::from_mappings(mappings).unwrap();
1195        for record in &cmap.encoding_records {
1196            let crate::tables::cmap::CmapSubtable::Format4(cmap4) = &*record.subtable else {
1197                continue;
1198            };
1199            if !matches!((cmap4.start_code.as_slice(), cmap4.end_code.as_slice()),
1200                (&[.., before_last_start, 0xFFFF], &[.., before_last_end, 0xFFFF])
1201                    if before_last_start != 0xFFFF || before_last_end != 0xFFFF)
1202            {
1203                panic!(
1204                    "Expected cmap4 to end with a single (0xFFFF, 0xFFFF) segment, but found {:?}",
1205                    (&cmap4.start_code, &cmap4.end_code)
1206                );
1207            }
1208        }
1209    }
1210
1211    #[test]
1212    fn cmap4_final_segment_is_not_duplicated() {
1213        cmap4_has_a_unique_final_segment([('a', GlyphId::new(1)), ('\u{FFFF}', GlyphId::new(0))]);
1214    }
1215
1216    #[test]
1217    fn cmap4_final_segment_is_added_even_if_last_generated_segment_ends_with_0xffff() {
1218        cmap4_has_a_unique_final_segment([
1219            ('a', GlyphId::new(1)),
1220            ('\u{FFFE}', GlyphId::new(0)),
1221            ('\u{FFFF}', GlyphId::new(0)),
1222        ]);
1223    }
1224}