1
use std::collections::BTreeMap;
2
use std::marker::PhantomData;
3

            
4
use crate::big5::big5_to_unicode;
5
use crate::binary::read::ReadScope;
6
use crate::error::ParseError;
7
use crate::font::Encoding;
8
use crate::macroman::{char_to_macroman, is_macroman, macroman_to_char};
9
use crate::subset::{CmapTarget, SubsetGlyphs};
10
use crate::tables::cmap::{owned, Cmap, EncodingId, PlatformId, SequentialMapGroup};
11
use crate::tables::os2::{self, Os2};
12
use crate::tables::{cmap, FontTableProvider};
13
use crate::tag;
14

            
15
pub struct MappingsToKeep<T> {
16
    mappings: BTreeMap<Character, u16>,
17
    plane: CharExistence,
18
    _ids: PhantomData<T>,
19
}
20

            
21
pub enum OldIds {}
22
pub enum NewIds {}
23

            
24
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]
25
enum CharExistence {
26
    /// Can be encoded in [MacRoman](https://en.wikipedia.org/wiki/Mac_OS_Roman)
27
    MacRoman = 1,
28
    /// Unicode Plane 0
29
    BasicMultilingualPlane = 2,
30
    /// Unicode Plane 1 onwards
31
    AstralPlane = 3,
32
    /// Exists outside Unicode
33
    DivinePlane = 4,
34
}
35

            
36
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
37
enum Character {
38
    Unicode(char),
39
    Symbol(u32),
40
}
41

            
42
/// The strategy to use to generate a cmap table for the subset font
43
#[allow(unused)]
44
pub(crate) enum CmapStrategy {
45
    /// Build a cmap table by filtering existing mappings
46
    Generate(MappingsToKeep<OldIds>), // FIXME: Rename
47
    /// Use the supplied Mac Roman cmap table
48
    MacRomanSupplied(Box<[u8; 256]>),
49
    /// Omit the cmap table
50
    Omit,
51
}
52

            
53
#[derive(Debug)]
54
struct CmapSubtableFormat4Segment<'a> {
55
    start: u32,
56
    end: u32,
57
    glyph_ids: &'a mut Vec<u16>,
58
    consecutive_glyph_ids: bool,
59
}
60

            
61
impl Character {
62
    fn new(ch: u32, encoding: Encoding) -> Option<Self> {
63
        match encoding {
64
            Encoding::Unicode => std::char::from_u32(ch).map(Character::Unicode),
65
            Encoding::Symbol => Some(Character::Symbol(ch)),
66
            Encoding::AppleRoman => macroman_to_char(ch as u8).map(Character::Unicode),
67
            Encoding::Big5 => u16::try_from(ch)
68
                .ok()
69
                .and_then(big5_to_unicode)
70
                .map(Character::Unicode),
71
        }
72
    }
73

            
74
    fn existence(self) -> CharExistence {
75
        match self {
76
            Character::Unicode(ch) if is_macroman(ch) => CharExistence::MacRoman,
77
            Character::Unicode(ch) if ch <= '\u{FFFF}' => CharExistence::BasicMultilingualPlane,
78
            Character::Unicode(_) => CharExistence::AstralPlane,
79
            Character::Symbol(_) => CharExistence::DivinePlane,
80
        }
81
    }
82

            
83
    fn as_u32(self) -> u32 {
84
        match self {
85
            Character::Unicode(ch) => ch as u32,
86
            Character::Symbol(ch) => ch,
87
        }
88
    }
89
}
90

            
91
impl From<char> for Character {
92
    fn from(ch: char) -> Self {
93
        Character::Unicode(ch)
94
    }
95
}
96

            
97
impl<'a> CmapSubtableFormat4Segment<'a> {
98
    fn new(start: u32, gid: u16, glyph_ids: &'a mut Vec<u16>) -> Self {
99
        glyph_ids.clear();
100
        glyph_ids.push(gid);
101
        CmapSubtableFormat4Segment {
102
            start,
103
            end: start,
104
            glyph_ids,
105
            consecutive_glyph_ids: true,
106
        }
107
    }
108

            
109
    fn add(&mut self, ch: u32, gid: u16) -> bool {
110
        // -1 because the next consecutive character introduces no gap
111
        let gap = ch.saturating_sub(self.end).saturating_sub(1);
112
        let should_remain_compact = self.consecutive_glyph_ids && self.glyph_ids.len() >= 4;
113

            
114
        if gap > 0 && should_remain_compact {
115
            // Each new segment costs 8 bytes so if the gap will introduce a non-consecutive glyph
116
            // id and the current segment contains 4 of more entries it's better to start a new
117
            // segment and allow this one to continue to use the compact representation.
118
            false
119
        } else if gap < 4 {
120
            // Each gap entry is two bytes in the glyph id array, if the gap is less than four
121
            // characters then it's worth adding to this segment, otherwise it's better to create
122
            // a new segment (which costs 8 bytes).
123

            
124
            // Gaps need to be mapped to .notdef (glyph id 0)
125
            if gap == 0 {
126
                // NOTE(unwrap): glyph_ids is never empty
127
                let prev = self.glyph_ids.last().copied().unwrap();
128
                self.consecutive_glyph_ids &= (prev + 1) == gid;
129
            } else {
130
                self.glyph_ids.extend(std::iter::repeat_n(0, gap as usize));
131
                // if there's a gap then the glyph ids can't be consecutive
132
                self.consecutive_glyph_ids = false;
133
            }
134
            self.glyph_ids.push(gid);
135
            self.end = ch;
136
            true
137
        } else {
138
            false
139
        }
140
    }
141
}
142

            
143
impl owned::CmapSubtableFormat4 {
144
    fn from_mappings(
145
        mappings: &MappingsToKeep<NewIds>,
146
    ) -> Result<owned::CmapSubtableFormat4, ParseError> {
147
        let mut table = owned::CmapSubtableFormat4 {
148
            language: 0,
149
            end_codes: Vec::new(),
150
            start_codes: Vec::new(),
151
            id_deltas: Vec::new(),
152
            id_range_offsets: Vec::new(),
153
            glyph_id_array: Vec::new(),
154
        };
155

            
156
        // Group the mappings into contiguous ranges, there can be holes in the ranges
157
        let mut glyph_ids = Vec::new();
158
        let mut id_range_offset_fixup_indices = Vec::new();
159
        if let Some((start, gid)) = mappings.iter().next() {
160
            let mut segment = CmapSubtableFormat4Segment::new(start.as_u32(), gid, &mut glyph_ids);
161
            for (ch, gid) in mappings.iter().skip(1) {
162
                if !segment.add(ch.as_u32(), gid) {
163
                    table.add_segment(segment, &mut id_range_offset_fixup_indices);
164
                    segment = CmapSubtableFormat4Segment::new(ch.as_u32(), gid, &mut glyph_ids);
165
                }
166
            }
167

            
168
            // Add final range
169
            table.add_segment(segment, &mut id_range_offset_fixup_indices);
170
        }
171

            
172
        // Final start code and endCode values must be 0xFFFF. This segment need not contain any
173
        // valid mappings. (It can just map the single character code 0xFFFF to missingGlyph).
174
        // However, the segment must be present.
175
        //
176
        // — https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
177
        let segment = CmapSubtableFormat4Segment::new(0xFFFF, 0, &mut glyph_ids);
178
        table.add_segment(segment, &mut id_range_offset_fixup_indices);
179

            
180
        // Fix up the id_range_offsets now that all segments have been added
181
        let num_segments = table.end_codes.len();
182
        for index in id_range_offset_fixup_indices {
183
            let id_range_offset = &mut table.id_range_offsets[index];
184
            let count = num_segments + usize::from(*id_range_offset) - index;
185
            // ×2 because we need to skip over `count` 16-bit values
186
            *id_range_offset = u16::try_from(2 * count).map_err(|_| ParseError::LimitExceeded)?;
187
        }
188

            
189
        Ok(table)
190
    }
191

            
192
    fn add_segment(
193
        &mut self,
194
        segment: CmapSubtableFormat4Segment<'_>,
195
        id_range_offset_fixups: &mut Vec<usize>,
196
    ) {
197
        self.start_codes.push(segment.start as u16);
198
        self.end_codes.push(segment.end as u16);
199

            
200
        // If the segment contains contiguous range of glyph ids then we can just store
201
        // an id delta for the entire range.
202
        if segment.consecutive_glyph_ids {
203
            // NOTE(unwrap): safe as segments will always contain at least one char->glyph mapping
204
            let first_glyph_id = *segment.glyph_ids.first().unwrap();
205

            
206
            // NOTE: casting start to i32 is safe as format 4 can only hold Unicode BMP chars,
207
            // which are 16-bit values. Casting the result to i16 is safe because the calculation
208
            // is modulo 0x10000 (65536), which limits the value to ±0x10000.
209
            self.id_deltas
210
                .push((i32::from(first_glyph_id) - segment.start as i32 % 0x10000) as i16);
211
            self.id_range_offsets.push(0);
212
        } else {
213
            // Glyph ids are not consecutive so store them in the glyph id array via id range
214
            // offsets
215
            self.id_deltas.push(0);
216
            // NOTE: The id range offset value will be fixed up in a later pass
217
            id_range_offset_fixups.push(self.id_range_offsets.len());
218
            // NOTE: casting should be safe as num_glyphs in a font is u16
219
            self.id_range_offsets.push(self.glyph_id_array.len() as u16);
220
            self.glyph_id_array.extend_from_slice(segment.glyph_ids);
221
        }
222
    }
223
}
224

            
225
impl owned::CmapSubtableFormat12 {
226
    fn from_mappings(mappings: &MappingsToKeep<NewIds>) -> owned::CmapSubtableFormat12 {
227
        let mut segments = Vec::new();
228
        if let Some((start, gid)) = mappings.iter().next() {
229
            let mut segment = SequentialMapGroup {
230
                start_char_code: start.as_u32(),
231
                end_char_code: start.as_u32(),
232
                start_glyph_id: u32::from(gid),
233
            };
234
            let mut prev_gid = gid;
235
            for (ch, gid) in mappings.iter().skip(1) {
236
                if ch.as_u32() == segment.end_char_code + 1 && gid == prev_gid + 1 {
237
                    segment.end_char_code += 1
238
                } else {
239
                    segments.push(segment);
240
                    segment = SequentialMapGroup {
241
                        start_char_code: ch.as_u32(),
242
                        end_char_code: ch.as_u32(),
243
                        start_glyph_id: u32::from(gid),
244
                    };
245
                }
246
                prev_gid = gid;
247
            }
248
            segments.push(segment);
249
        }
250

            
251
        owned::CmapSubtableFormat12 {
252
            language: 0,
253
            groups: segments,
254
        }
255
    }
256
}
257

            
258
impl owned::EncodingRecord {
259
    pub fn from_mappings(mappings: &MappingsToKeep<NewIds>) -> Result<Self, ParseError> {
260
        match mappings.plane() {
261
            CharExistence::MacRoman => {
262
                // The language field must be set to zero for all 'cmap' subtables whose platform
263
                // IDs are other than Macintosh (platform ID 1). For 'cmap' subtables whose
264
                // platform IDs are Macintosh, set this field to the Macintosh language ID of the
265
                // 'cmap' subtable plus one, or to zero if the 'cmap' subtable is not
266
                // language-specific. For example, a Mac OS Turkish 'cmap' subtable must set this
267
                // field to 18, since the Macintosh language ID for Turkish is 17. A Mac OS Roman
268
                // 'cmap' subtable must set this field to 0, since Mac OS Roman is not a
269
                // language-specific encoding.
270
                //
271
                // — https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#use-of-the-language-field-in-cmap-subtables
272
                let mut glyph_id_array = [0; 256];
273
                for (ch, gid) in mappings.iter() {
274
                    let ch_mac = match ch {
275
                        // NOTE(unwrap): Safe as we verified all chars with `is_macroman` earlier
276
                        Character::Unicode(unicode) => {
277
                            usize::from(char_to_macroman(unicode).unwrap())
278
                        }
279
                        Character::Symbol(_) => unreachable!("symbol in mac roman"),
280
                    };
281
                    // Cast is safe as we determined that all chars are valid in Mac Roman
282
                    glyph_id_array[ch_mac] = gid as u8;
283
                }
284
                let sub_table = owned::CmapSubtable::Format0 {
285
                    language: 0,
286
                    glyph_id_array: Box::new(glyph_id_array),
287
                };
288
                Ok(owned::EncodingRecord {
289
                    platform_id: PlatformId::MACINTOSH,
290
                    encoding_id: EncodingId::MACINTOSH_APPLE_ROMAN,
291
                    sub_table,
292
                })
293
            }
294
            CharExistence::BasicMultilingualPlane => {
295
                let sub_table = cmap::owned::CmapSubtable::Format4(
296
                    owned::CmapSubtableFormat4::from_mappings(mappings)?,
297
                );
298
                Ok(owned::EncodingRecord {
299
                    platform_id: PlatformId::UNICODE,
300
                    encoding_id: EncodingId::UNICODE_BMP,
301
                    sub_table,
302
                })
303
            }
304
            CharExistence::AstralPlane => {
305
                let sub_table = cmap::owned::CmapSubtable::Format12(
306
                    owned::CmapSubtableFormat12::from_mappings(mappings),
307
                );
308
                Ok(owned::EncodingRecord {
309
                    platform_id: PlatformId::UNICODE,
310
                    encoding_id: EncodingId::UNICODE_FULL,
311
                    sub_table,
312
                })
313
            }
314
            CharExistence::DivinePlane => {
315
                let sub_table = cmap::owned::CmapSubtable::Format4(
316
                    owned::CmapSubtableFormat4::from_mappings(mappings)?,
317
                );
318
                Ok(owned::EncodingRecord {
319
                    platform_id: PlatformId::WINDOWS,
320
                    encoding_id: EncodingId::WINDOWS_SYMBOL,
321
                    sub_table,
322
                })
323
            }
324
        }
325
    }
326
}
327

            
328
impl<T> MappingsToKeep<T> {
329
    fn iter(&self) -> impl Iterator<Item = (Character, u16)> + '_ {
330
        self.mappings.iter().map(|(&ch, &gid)| (ch, gid))
331
    }
332

            
333
    fn plane(&self) -> CharExistence {
334
        self.plane
335
    }
336
}
337

            
338
impl MappingsToKeep<OldIds> {
339
    pub(crate) fn new(
340
        provider: &impl FontTableProvider,
341
        glyph_ids: &[u16],
342
        target: CmapTarget,
343
    ) -> Result<Self, ParseError> {
344
        let cmap_data = provider.read_table_data(tag::CMAP)?;
345
        let cmap0 = ReadScope::new(&cmap_data).read::<Cmap<'_>>()?;
346
        let (encoding, cmap_sub_table) =
347
            crate::font::read_cmap_subtable(&cmap0)?.ok_or(ParseError::UnsuitableCmap)?;
348

            
349
        // Special case handling of a Symbol cmap targeting MacRoman
350
        //
351
        // This exists to handle the case where MacRoman characters were successfully mapped to
352
        // glyphs via a cmap sub-table with Symbol encoding (see `legacy_symbol_char_code` in
353
        // `Font`). We need to perform the inverse operation when we're targeting a MacRoman
354
        // encoded cmap sub-table in the subset font.
355
        let symbol_first_char = if encoding == Encoding::Symbol && target == CmapTarget::MacRoman {
356
            Some(
357
                provider
358
                    .table_data(tag::OS_2)?
359
                    .map(|data| ReadScope::new(&data).read_dep::<Os2>(data.len()))
360
                    .transpose()?
361
                    .map(|os2| os2.us_first_char_index)
362
                    .unwrap_or(0x20),
363
            )
364
        } else {
365
            None
366
        };
367

            
368
        // Collect cmap mappings for the selected glyph ids
369
        let mut mappings_to_keep = BTreeMap::new();
370
        let mut plane = if target == CmapTarget::Unicode {
371
            CharExistence::BasicMultilingualPlane
372
        } else {
373
            CharExistence::MacRoman
374
        };
375

            
376
        // Process all the mappings and select the ones we want to keep
377
        cmap_sub_table.mappings_fn(|ch, gid| {
378
            if gid != 0 && glyph_ids.contains(&gid) {
379
                // We want to keep this mapping, determine the plane it lives on
380
                // If `symbol_first_char` is set then that indicates we're targeting a MacRoman
381
                // cmap sub-table with a source Windows Symbol encoded cmap sub-table. Perform
382
                // a mapping from symbol char code to unicode (inverse of what was done when
383
                // mapping glyphs).
384
                let output_char = symbol_first_char
385
                    .and_then(|first| legacy_symbol_char_code_to_unicode(ch, first))
386
                    .map(|uni| Some(Character::from(uni)))
387
                    .unwrap_or_else(|| Character::new(ch, encoding));
388
                let output_char = match output_char {
389
                    Some(ch) => ch,
390
                    None => return,
391
                };
392

            
393
                match target {
394
                    CmapTarget::MacRoman => {
395
                        // Only keep if it's MacRoman compatible
396
                        if output_char.existence() <= CharExistence::MacRoman {
397
                            mappings_to_keep.insert(output_char, gid);
398
                        }
399
                    }
400
                    CmapTarget::Unicode | CmapTarget::Unrestricted => {
401
                        if output_char.existence() > plane {
402
                            plane = output_char.existence();
403
                        }
404
                        mappings_to_keep.insert(output_char, gid);
405
                    }
406
                }
407
            }
408
        })?;
409

            
410
        if mappings_to_keep.len() <= usize::from(u16::MAX) {
411
            Ok(MappingsToKeep {
412
                mappings: mappings_to_keep,
413
                plane,
414
                _ids: PhantomData,
415
            })
416
        } else {
417
            Err(ParseError::LimitExceeded)
418
        }
419
    }
420

            
421
    /// Update the glyph ids to be ids in the new subset font
422
    pub(crate) fn update_to_new_ids(
423
        mut self,
424
        subset_glyphs: &impl SubsetGlyphs,
425
    ) -> MappingsToKeep<NewIds> {
426
        self.mappings
427
            .iter_mut()
428
            .for_each(|(_ch, gid)| *gid = subset_glyphs.new_id(*gid));
429
        MappingsToKeep {
430
            mappings: self.mappings,
431
            plane: self.plane,
432
            _ids: PhantomData,
433
        }
434
    }
435

            
436
    // Calculate new first and last Unicode codepoints
437
    pub(crate) fn first_last_codepoints(&self) -> (u32, u32) {
438
        if self.mappings.is_empty() {
439
            (0, 0) // No mappings, use 0 for both
440
        } else {
441
            self.iter().fold((u32::MAX, 0_u32), |(min, max), (ch, _)| {
442
                let code = ch.as_u32();
443
                (min.min(code), max.max(code))
444
            })
445
        }
446
    }
447

            
448
    // Compute the new OS/2 ulUnicodeRange bitmask
449
    pub(crate) fn unicode_bitmask(&self) -> u128 {
450
        self.iter().fold(0, |mask, (ch, _)| {
451
            mask | os2::unicode_range_mask(ch.as_u32())
452
        })
453
    }
454
}
455

            
456
// See also legacy_symbol_char_code in Font and the explanation there
457
fn legacy_symbol_char_code_to_unicode(ch: u32, first_char: u16) -> Option<char> {
458
    let char_code0 = if (0xF000..=0xF0FF).contains(&ch) {
459
        ch
460
    } else {
461
        ch + 0xF000
462
    };
463
    std::char::from_u32((char_code0 + 0x20) - u32::from(first_char)) // Perform subtraction last to avoid underflow.
464
}
465

            
466
#[cfg(test)]
467
mod tests {
468
    use crate::tables::OpenTypeFont;
469
    use crate::tests::read_fixture;
470

            
471
    use super::*;
472

            
473
    #[test]
474
    fn test_character_existence() {
475
        assert_eq!(Character::Unicode('a').existence(), CharExistence::MacRoman);
476
        assert_eq!(
477
            Character::Unicode('ռ').existence(),
478
            CharExistence::BasicMultilingualPlane
479
        );
480
        assert_eq!(
481
            Character::Unicode('🦀').existence(),
482
            CharExistence::AstralPlane
483
        );
484
    }
485

            
486
    #[test]
487
    fn test_format4_subtable() {
488
        let mappings = MappingsToKeep {
489
            mappings: vec![
490
                (Character::Unicode('a'), 1),
491
                (Character::Unicode('b'), 2),
492
                (Character::Unicode('i'), 4),
493
                (Character::Unicode('j'), 3),
494
            ]
495
            .into_iter()
496
            .collect(),
497
            plane: CharExistence::MacRoman,
498
            _ids: PhantomData,
499
        };
500
        let sub_table = owned::CmapSubtableFormat4::from_mappings(&mappings).unwrap();
501
        let expected = owned::CmapSubtableFormat4 {
502
            language: 0,
503
            start_codes: vec![97, 105, 0xFFFF],
504
            end_codes: vec![98, 106, 0xFFFF],
505
            id_deltas: vec![-96, 0, 1],
506
            id_range_offsets: vec![0, 4, 0],
507
            glyph_id_array: vec![4, 3],
508
        };
509
        assert_eq!(sub_table, expected);
510
    }
511

            
512
    #[test]
513
    fn test_format12_subtable() {
514
        let mappings = MappingsToKeep {
515
            mappings: vec![
516
                (Character::Unicode('a'), 1),
517
                (Character::Unicode('b'), 2),
518
                (Character::Unicode('🦀'), 3),
519
                (Character::Unicode('🦁'), 4),
520
            ]
521
            .into_iter()
522
            .collect(),
523
            plane: CharExistence::AstralPlane,
524
            _ids: PhantomData,
525
        };
526
        let sub_table = owned::CmapSubtableFormat12::from_mappings(&mappings);
527
        let expected = owned::CmapSubtableFormat12 {
528
            language: 0,
529
            groups: vec![
530
                SequentialMapGroup {
531
                    start_char_code: 97,
532
                    end_char_code: 98,
533
                    start_glyph_id: 1,
534
                },
535
                SequentialMapGroup {
536
                    start_char_code: 129408,
537
                    end_char_code: 129409,
538
                    start_glyph_id: 3,
539
                },
540
            ],
541
        };
542
        assert_eq!(sub_table, expected);
543
    }
544

            
545
    #[test]
546
    fn test_format12_subtable_empty_mappings() {
547
        let mappings = MappingsToKeep {
548
            mappings: BTreeMap::new(),
549
            plane: CharExistence::BasicMultilingualPlane,
550
            _ids: PhantomData,
551
        };
552
        let sub_table = owned::CmapSubtableFormat12::from_mappings(&mappings);
553
        assert!(sub_table.groups.is_empty());
554
    }
555

            
556
    #[test]
557
    fn test_target_macroman_from_symbol() {
558
        let buffer = read_fixture("tests/fonts/opentype/SymbolTest-Regular.ttf");
559
        let scope = ReadScope::new(&buffer);
560
        let font_file = scope
561
            .read::<OpenTypeFont<'_>>()
562
            .expect("unable to parse font file");
563
        let table_provider = font_file
564
            .table_provider(0)
565
            .expect("unable to create font provider");
566

            
567
        let to_keep = MappingsToKeep::new(&table_provider, &[0, 3, 4, 5], CmapTarget::MacRoman)
568
            .expect("error building mappings to keep");
569
        assert_eq!(to_keep.plane, CharExistence::MacRoman);
570
        let chars: String = to_keep
571
            .mappings
572
            .keys()
573
            .map(|ch| match ch {
574
                Character::Unicode(c) => *c,
575
                Character::Symbol(_) => panic!("expected Character::Unicode got Character::Symbol"),
576
            })
577
            .collect();
578
        assert_eq!(chars, "abc");
579
    }
580
}