1
#![deny(missing_docs)]
2

            
3
//! Font subsetting.
4

            
5
use std::collections::BTreeMap;
6
use std::fmt;
7
use std::num::Wrapping;
8

            
9
use crate::binary::read::{ReadArrayCow, ReadScope};
10
use crate::binary::write::{Placeholder, WriteBinary};
11
use crate::binary::write::{WriteBinaryDep, WriteBuffer, WriteContext};
12
use crate::binary::{long_align, U16Be, U32Be};
13
use crate::cff::cff2::{OutputFormat, CFF2};
14
use crate::cff::{CFFError, SubsetCFF, CFF};
15
use crate::error::{ParseError, ReadWriteError, WriteError};
16
use crate::post::PostTable;
17
use crate::tables::cmap::subset::{CmapStrategy, MappingsToKeep, NewIds, OldIds};
18
use crate::tables::cmap::{owned, EncodingId, PlatformId};
19
use crate::tables::glyf::GlyfTable;
20
use crate::tables::loca::{self, LocaTable};
21
use crate::tables::os2::Os2;
22
use crate::tables::{
23
    self, cmap, FontTableProvider, HeadTable, HheaTable, HmtxTable, IndexToLocFormat, MaxpTable,
24
    TableRecord,
25
};
26
use crate::{checksum, tag};
27

            
28
/// Minimal set of tables, suitable for PDF embedding
29
const PROFILE_PDF: &[u32] = &[
30
    tag::CMAP,
31
    tag::HEAD,
32
    tag::CVT,
33
    tag::FPGM,
34
    tag::HHEA,
35
    tag::HMTX,
36
    tag::MAXP,
37
    tag::NAME,
38
    tag::POST,
39
    tag::PREP,
40
];
41

            
42
/// Minimum tables required for a valid OpenType font.
43
///
44
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#required-tables>
45
const PROFILE_MINIMAL: &[u32] = &[
46
    tag::CMAP,
47
    tag::HEAD,
48
    tag::HHEA,
49
    tag::HMTX,
50
    tag::MAXP,
51
    tag::NAME,
52
    tag::OS_2,
53
    tag::POST,
54
];
55

            
56
/// Profiles for controlling the tables included in subset fonts.
57
#[derive(Debug, Clone, PartialEq, Eq)]
58
pub enum SubsetProfile {
59
    /// Minimal set of tables, suitable for PDF embedding
60
    Pdf,
61
    /// Minimum tables required for a valid OpenType font.
62
    Minimal,
63
    /// Custom profile, allows specifying a list of tables to include.
64
    Custom(Vec<u32>),
65
}
66

            
67
/// Target cmap format to use when subsetting
68
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
69
pub enum CmapTarget {
70
    /// Use the smallest suitable cmap
71
    #[default]
72
    Unrestricted,
73
    /// Use a Mac Roman cmap
74
    ///
75
    /// Characters outside the Mac Roman character set will be omitted.
76
    MacRoman,
77
    /// Use a Unicode cmap format
78
    ///
79
    /// Select this option if targeting the web as browsers reject fonts with only a
80
    /// Mac Roman cmap.
81
    Unicode,
82
}
83

            
84
impl SubsetProfile {
85
    /// Retrieve the tables included in this subset profile
86
    fn get_tables(&self, extra: &[u32]) -> Vec<u32> {
87
        let tables = match self {
88
            SubsetProfile::Pdf => PROFILE_PDF,
89
            SubsetProfile::Minimal => PROFILE_MINIMAL,
90
            SubsetProfile::Custom(items) => items.as_slice(),
91
        };
92
        let mut tables = tables.to_vec();
93
        tables.extend_from_slice(extra);
94
        tables
95
    }
96

            
97
    /// Parses a custom subset profile from a string
98
    ///
99
    /// The table names may be separated by commas or whitespace, such as `gsub,vmtx,prep`.
100
    /// Case is ignored. Tables from the Minimal profile are included automatically.
101
    ///
102
    /// **Note:** Only the following tables may be selected. Including other tables will
103
    /// result in a `ParseError::BadValue` error:
104
    ///
105
    /// - cmap
106
    /// - head
107
    /// - hhea
108
    /// - hmtx
109
    /// - maxp
110
    /// - name
111
    /// - os/2
112
    /// - post
113
    /// - gpos
114
    /// - gsub
115
    /// - vhea
116
    /// - vmtx
117
    /// - gdef
118
    /// - cvt
119
    /// - fpgm
120
    /// - prep
121
    pub fn parse_custom(s: String) -> Result<Self, ParseError> {
122
        let mut bytes = s.into_bytes();
123
        let tags = bytes
124
            .split_mut(|&c| c == b',' || c.is_ascii_whitespace())
125
            .map(|name| {
126
                name.make_ascii_lowercase();
127
                match &*name {
128
                    b"cmap" => Ok(tag::CMAP),
129
                    b"head" => Ok(tag::HEAD),
130
                    b"hhea" => Ok(tag::HHEA),
131
                    b"hmtx" => Ok(tag::HMTX),
132
                    b"maxp" => Ok(tag::MAXP),
133
                    b"name" => Ok(tag::NAME),
134
                    b"os/2" | b"os2" | b"os_2" => Ok(tag::OS_2),
135
                    b"post" => Ok(tag::POST),
136
                    b"gpos" => Ok(tag::GPOS),
137
                    b"gsub" => Ok(tag::GSUB),
138
                    b"vhea" => Ok(tag::VHEA),
139
                    b"vmtx" => Ok(tag::VMTX),
140
                    b"gdef" => Ok(tag::GDEF),
141
                    b"cvt" => Ok(tag::CVT),
142
                    b"fpgm" => Ok(tag::FPGM),
143
                    b"prep" => Ok(tag::PREP),
144
                    _ => return Err(ParseError::BadValue),
145
                }
146
            });
147
        let mut tables = PROFILE_MINIMAL
148
            .iter()
149
            .copied()
150
            .map(Ok)
151
            .chain(tags)
152
            .collect::<Result<Vec<_>, _>>()?;
153
        tables.sort();
154
        tables.dedup();
155
        Ok(Self::Custom(tables))
156
    }
157
}
158

            
159
/// Error type returned from subsetting.
160
#[derive(Debug)]
161
pub enum SubsetError {
162
    /// An error occurred reading or parsing data.
163
    Parse(ParseError),
164
    /// An error occurred serializing data.
165
    Write(WriteError),
166
    /// An error occurred when interpreting CFF CharStrings
167
    CFF(CFFError),
168
    /// The glyph subset did not include glyph 0/.notdef in the first position
169
    NotDef,
170
    /// The subset glyph count exceeded the maximum number of glyphs
171
    TooManyGlyphs,
172
    /// The CFF font did not contain a sole font, which is the only supported configuration for
173
    /// subsetting
174
    InvalidFontCount,
175
}
176

            
177
pub(crate) trait SubsetGlyphs {
178
    /// The number of glyphs in this collection
179
    fn len(&self) -> usize;
180

            
181
    /// Return the old glyph id for the supplied new glyph id
182
    fn old_id(&self, new_id: u16) -> u16;
183

            
184
    /// Return the new glyph id for the supplied old glyph id
185
    fn new_id(&self, old_id: u16) -> u16;
186
}
187

            
188
pub(crate) struct FontBuilder {
189
    sfnt_version: u32,
190
    tables: BTreeMap<u32, WriteBuffer>,
191
    filter: TableFilter,
192
}
193

            
194
pub(crate) enum TableFilter {
195
    /// Include all tables
196
    All,
197
    /// Include only the selected tables
198
    Tables(Vec<u32>),
199
}
200

            
201
pub(crate) struct FontBuilderWithHead {
202
    inner: FontBuilder,
203
    check_sum_adjustment: Placeholder<U32Be, u32>,
204
    index_to_loc_format: IndexToLocFormat,
205
}
206

            
207
struct TaggedBuffer {
208
    tag: u32,
209
    buffer: WriteBuffer,
210
}
211

            
212
struct OrderedTables {
213
    tables: Vec<TaggedBuffer>,
214
    checksum: Wrapping<u32>,
215
}
216

            
217
/// Subset this font so that it only contains the glyphs with the supplied `glyph_ids`.
218
///
219
/// `glyph_ids` requirements:
220
///
221
/// * Glyph id 0, corresponding to the `.notdef` glyph must always be present.
222
/// * There must be no duplicate glyph ids.
223
///
224
/// If either of these requirements are not upheld this function will return
225
/// `ParseError::BadValue`.
226
pub fn subset(
227
    provider: &impl FontTableProvider,
228
    glyph_ids: &[u16],
229
    profile: &SubsetProfile,
230
    cmap_target: CmapTarget,
231
) -> Result<Vec<u8>, SubsetError> {
232
    let mappings_to_keep = MappingsToKeep::new(provider, glyph_ids, cmap_target)?;
233
    if provider.has_table(tag::CFF) {
234
        subset_cff(provider, glyph_ids, mappings_to_keep, true, profile)
235
    } else if provider.has_table(tag::CFF2) {
236
        subset_cff2(
237
            provider,
238
            glyph_ids,
239
            mappings_to_keep,
240
            false,
241
            OutputFormat::Type1OrCid,
242
            profile,
243
        )
244
    } else {
245
        subset_ttf(
246
            provider,
247
            glyph_ids,
248
            CmapStrategy::Generate(mappings_to_keep),
249
            profile,
250
        )
251
        .map_err(SubsetError::from)
252
    }
253
}
254

            
255
/// Subset a TTF font.
256
///
257
/// If `mappings_to_keep` is `None` a `cmap` table in the subset font will be omitted.
258
/// Otherwise it will be used to build a new `cmap` table.
259
fn subset_ttf(
260
    provider: &impl FontTableProvider,
261
    glyph_ids: &[u16],
262
    cmap_strategy: CmapStrategy,
263
    profile: &SubsetProfile,
264
) -> Result<Vec<u8>, ReadWriteError> {
265
    let profile_tables = profile.get_tables(&[tag::LOCA, tag::GLYF]);
266
    let head = ReadScope::new(&provider.read_table_data(tag::HEAD)?).read::<HeadTable>()?;
267
    let mut maxp = ReadScope::new(&provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
268
    let loca_data = provider.read_table_data(tag::LOCA)?;
269
    let loca = ReadScope::new(&loca_data)
270
        .read_dep::<LocaTable<'_>>((maxp.num_glyphs, head.index_to_loc_format))?;
271
    let glyf_data = provider.read_table_data(tag::GLYF)?;
272
    let glyf = ReadScope::new(&glyf_data).read_dep::<GlyfTable<'_>>(&loca)?;
273
    let mut hhea = ReadScope::new(&provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
274
    let hmtx_data = provider.read_table_data(tag::HMTX)?;
275
    let hmtx = ReadScope::new(&hmtx_data).read_dep::<HmtxTable<'_>>((
276
        usize::from(maxp.num_glyphs),
277
        usize::from(hhea.num_h_metrics),
278
    ))?;
279

            
280
    // Build a new post table with version set to 3, which does not contain any additional
281
    // PostScript data
282
    let post_data = provider.read_table_data(tag::POST)?;
283
    let mut post = ReadScope::new(&post_data).read::<PostTable<'_>>()?;
284
    post.header.version = 0x00030000; // version 3.0
285
    post.opt_sub_table = None;
286

            
287
    // Get the OS/2 table if needed
288
    let maybe_os2 = profile_tables
289
        .contains(&tag::OS_2)
290
        .then(|| {
291
            provider
292
                .read_table_data(tag::OS_2)
293
                .and_then(|data| ReadScope::new(&data).read_dep::<Os2>(data.len()))
294
        })
295
        .transpose()?;
296

            
297
    // Subset the OS/2 table if we have one and mappings
298
    let subset_os2 = maybe_os2.map(|os2| match &cmap_strategy {
299
        CmapStrategy::Generate(mappings) => subset_os2(&os2, mappings),
300
        CmapStrategy::MacRomanSupplied(_) | CmapStrategy::Omit => os2,
301
    });
302

            
303
    // Subset the glyphs
304
    let subset_glyphs = glyf.subset(glyph_ids)?;
305

            
306
    // Build a new cmap table
307
    let cmap = match cmap_strategy {
308
        CmapStrategy::Generate(mappings_to_keep) => {
309
            let mappings_to_keep = mappings_to_keep.update_to_new_ids(&subset_glyphs);
310
            Some(create_cmap_table(&mappings_to_keep)?)
311
        }
312
        CmapStrategy::MacRomanSupplied(cmap) => {
313
            Some(create_cmap_table_from_cmap_array(glyph_ids, cmap)?)
314
        }
315
        CmapStrategy::Omit => None,
316
    };
317

            
318
    // Build new maxp table
319
    let num_glyphs = u16::try_from(subset_glyphs.len()).map_err(ParseError::from)?;
320
    maxp.num_glyphs = num_glyphs;
321

            
322
    // Build new hhea table
323
    let num_h_metrics = usize::from(hhea.num_h_metrics);
324
    hhea.num_h_metrics = num_glyphs;
325

            
326
    // Build new hmtx table
327
    let hmtx = create_hmtx_table(&hmtx, num_h_metrics, &subset_glyphs)?;
328

            
329
    // Extract the new glyf table now that we're done with subset_glyphs
330
    let glyf = GlyfTable::from(subset_glyphs);
331

            
332
    // Get the remaining tables
333
    let cvt = provider.table_data(tag::CVT)?;
334
    let fpgm = provider.table_data(tag::FPGM)?;
335
    let name = provider.table_data(tag::NAME)?;
336
    let prep = provider.table_data(tag::PREP)?;
337

            
338
    // Build the new font
339
    let mut builder = FontBuilder::new(0x00010000_u32, TableFilter::Tables(profile_tables));
340
    if let Some(cmap) = cmap {
341
        builder.add_table::<_, cmap::owned::Cmap>(tag::CMAP, cmap, ())?;
342
    }
343
    if let Some(cvt) = cvt {
344
        builder.add_table::<_, ReadScope<'_>>(tag::CVT, ReadScope::new(&cvt), ())?;
345
    }
346
    if let Some(fpgm) = fpgm {
347
        builder.add_table::<_, ReadScope<'_>>(tag::FPGM, ReadScope::new(&fpgm), ())?;
348
    }
349
    builder.add_table::<_, HheaTable>(tag::HHEA, &hhea, ())?;
350
    builder.add_table::<_, HmtxTable<'_>>(tag::HMTX, &hmtx, ())?;
351
    builder.add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())?;
352
    if let Some(name) = name {
353
        builder.add_table::<_, ReadScope<'_>>(tag::NAME, ReadScope::new(&name), ())?;
354
    }
355
    builder.add_table::<_, PostTable<'_>>(tag::POST, &post, ())?;
356
    if let Some(prep) = prep {
357
        builder.add_table::<_, ReadScope<'_>>(tag::PREP, ReadScope::new(&prep), ())?;
358
    }
359
    if let Some(os2) = subset_os2 {
360
        builder.add_table::<_, Os2>(tag::OS_2, &os2, ())?;
361
    }
362
    let mut builder = builder.add_head_table(&head)?;
363
    builder.add_glyf_table(glyf)?;
364
    builder.data()
365
}
366

            
367
fn subset_cff(
368
    provider: &impl FontTableProvider,
369
    glyph_ids: &[u16],
370
    mappings_to_keep: MappingsToKeep<OldIds>,
371
    convert_cff_to_cid_if_more_than_255_glyphs: bool,
372
    profile: &SubsetProfile,
373
) -> Result<Vec<u8>, SubsetError> {
374
    let cff_data = provider.read_table_data(tag::CFF)?;
375
    let scope = ReadScope::new(&cff_data);
376
    let cff: CFF<'_> = scope.read::<CFF<'_>>()?;
377
    if cff.name_index.len() != 1 || cff.fonts.len() != 1 {
378
        return Err(SubsetError::InvalidFontCount);
379
    }
380

            
381
    let head = ReadScope::new(&provider.read_table_data(tag::HEAD)?).read::<HeadTable>()?;
382
    let maxp = ReadScope::new(&provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
383
    let hhea = ReadScope::new(&provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
384
    let hmtx_data = provider.read_table_data(tag::HMTX)?;
385
    let hmtx = ReadScope::new(&hmtx_data).read_dep::<HmtxTable<'_>>((
386
        usize::from(maxp.num_glyphs),
387
        usize::from(hhea.num_h_metrics),
388
    ))?;
389

            
390
    // Build the new CFF table
391
    let cff_subset = cff.subset(glyph_ids, convert_cff_to_cid_if_more_than_255_glyphs)?;
392
    build_otf(
393
        cff_subset,
394
        mappings_to_keep,
395
        provider,
396
        &head,
397
        maxp,
398
        hhea,
399
        &hmtx,
400
        profile,
401
    )
402
}
403

            
404
fn subset_cff2(
405
    provider: &impl FontTableProvider,
406
    glyph_ids: &[u16],
407
    mappings_to_keep: MappingsToKeep<OldIds>,
408
    include_fstype: bool,
409
    output_format: OutputFormat,
410
    profile: &SubsetProfile,
411
) -> Result<Vec<u8>, SubsetError> {
412
    let cff2_data = provider.read_table_data(tag::CFF2)?;
413
    let scope = ReadScope::new(&cff2_data);
414
    let cff2: CFF2<'_> = scope.read::<CFF2<'_>>()?;
415
    let head = ReadScope::new(&provider.read_table_data(tag::HEAD)?).read::<HeadTable>()?;
416
    let maxp = ReadScope::new(&provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
417
    let hhea = ReadScope::new(&provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
418
    let hmtx_data = provider.read_table_data(tag::HMTX)?;
419
    let hmtx = ReadScope::new(&hmtx_data).read_dep::<HmtxTable<'_>>((
420
        usize::from(maxp.num_glyphs),
421
        usize::from(hhea.num_h_metrics),
422
    ))?;
423

            
424
    // Build the new CFF table
425
    let cff_subset = cff2.subset_to_cff(glyph_ids, provider, include_fstype, output_format)?;
426

            
427
    // Wrap the rest of the OpenType tables around it
428
    build_otf(
429
        cff_subset,
430
        mappings_to_keep,
431
        provider,
432
        &head,
433
        maxp,
434
        hhea,
435
        &hmtx,
436
        profile,
437
    )
438
}
439

            
440
fn build_otf(
441
    cff_subset: SubsetCFF<'_>,
442
    mappings_to_keep: MappingsToKeep<OldIds>,
443
    provider: &impl FontTableProvider,
444
    head: &HeadTable,
445
    mut maxp: MaxpTable,
446
    mut hhea: HheaTable,
447
    hmtx: &HmtxTable<'_>,
448
    profile: &SubsetProfile,
449
) -> Result<Vec<u8>, SubsetError> {
450
    let profile_tables = profile.get_tables(&[tag::CFF]);
451

            
452
    // Get the OS/2 table if needed
453
    let os2 = if profile_tables.contains(&tag::OS_2) {
454
        match provider.table_data(tag::OS_2) {
455
            Ok(Some(data)) => {
456
                let os2 = ReadScope::new(&data).read_dep::<Os2>(data.len())?;
457
                let updated_os2 = subset_os2(&os2, &mappings_to_keep);
458
                Some(updated_os2)
459
            }
460
            _ => None,
461
        }
462
    } else {
463
        None
464
    };
465

            
466
    let mappings_to_keep = mappings_to_keep.update_to_new_ids(&cff_subset);
467

            
468
    // Build a new post table with version set to 3, which does not contain any additional
469
    // PostScript data
470
    let post_data = provider.read_table_data(tag::POST)?;
471
    let mut post = ReadScope::new(&post_data).read::<PostTable<'_>>()?;
472
    post.header.version = 0x00030000; // version 3.0
473
    post.opt_sub_table = None;
474

            
475
    // Build a new cmap table
476
    let cmap = create_cmap_table(&mappings_to_keep)?;
477

            
478
    // Build new maxp table
479
    let num_glyphs = u16::try_from(cff_subset.len()).map_err(ParseError::from)?;
480
    maxp.num_glyphs = num_glyphs;
481

            
482
    // Build new hhea table
483
    let num_h_metrics = usize::from(hhea.num_h_metrics);
484
    hhea.num_h_metrics = num_glyphs;
485

            
486
    // Build new hmtx table
487
    let hmtx = create_hmtx_table(hmtx, num_h_metrics, &cff_subset)?;
488

            
489
    // Get the remaining tables
490
    let cvt = provider.table_data(tag::CVT)?;
491
    let fpgm = provider.table_data(tag::FPGM)?;
492
    let name = provider.table_data(tag::NAME)?;
493
    let prep = provider.table_data(tag::PREP)?;
494

            
495
    // Build the new font
496
    let mut builder = FontBuilder::new(tag::OTTO, TableFilter::Tables(profile_tables));
497
    builder.add_table::<_, cmap::owned::Cmap>(tag::CMAP, cmap, ())?;
498
    if let Some(cvt) = cvt {
499
        builder.add_table::<_, ReadScope<'_>>(tag::CVT, ReadScope::new(&cvt), ())?;
500
    }
501
    if let Some(fpgm) = fpgm {
502
        builder.add_table::<_, ReadScope<'_>>(tag::FPGM, ReadScope::new(&fpgm), ())?;
503
    }
504
    builder.add_table::<_, HheaTable>(tag::HHEA, &hhea, ())?;
505
    builder.add_table::<_, HmtxTable<'_>>(tag::HMTX, &hmtx, ())?;
506
    builder.add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())?;
507
    if let Some(name) = name {
508
        builder.add_table::<_, ReadScope<'_>>(tag::NAME, ReadScope::new(&name), ())?;
509
    }
510
    if let Some(os2) = os2 {
511
        builder.add_table::<_, Os2>(tag::OS_2, &os2, ())?;
512
    }
513
    builder.add_table::<_, PostTable<'_>>(tag::POST, &post, ())?;
514
    if let Some(prep) = prep {
515
        builder.add_table::<_, ReadScope<'_>>(tag::PREP, ReadScope::new(&prep), ())?;
516
    }
517

            
518
    // Extract the new CFF table now that we're done with cff_subset
519
    let cff = CFF::from(cff_subset);
520
    builder.add_table::<_, CFF<'_>>(tag::CFF, &cff, ())?;
521
    let builder = builder.add_head_table(head)?;
522
    builder.data().map_err(SubsetError::from)
523
}
524

            
525
fn create_cmap_table(
526
    mappings_to_keep: &MappingsToKeep<NewIds>,
527
) -> Result<owned::Cmap, ReadWriteError> {
528
    let encoding_record = owned::EncodingRecord::from_mappings(mappings_to_keep)?;
529
    Ok(owned::Cmap {
530
        encoding_records: vec![encoding_record],
531
    })
532
}
533

            
534
fn create_cmap_table_from_cmap_array(
535
    glyph_ids: &[u16],
536
    cmap: Box<[u8; 256]>,
537
) -> Result<owned::Cmap, ReadWriteError> {
538
    use cmap::owned::{Cmap, CmapSubtable, EncodingRecord};
539

            
540
    if glyph_ids.len() > 256 {
541
        return Err(ReadWriteError::Write(WriteError::BadValue));
542
    }
543

            
544
    Ok(Cmap {
545
        encoding_records: vec![EncodingRecord {
546
            platform_id: PlatformId::MACINTOSH,
547
            encoding_id: EncodingId::MACINTOSH_APPLE_ROMAN,
548
            sub_table: CmapSubtable::Format0 {
549
                language: 0, // the subtable is language independent
550
                glyph_id_array: cmap,
551
            },
552
        }],
553
    })
554
}
555

            
556
/// Construct a complete font from the supplied provider and tags.
557
pub fn whole_font<F: FontTableProvider>(
558
    provider: &F,
559
    tags: &[u32],
560
) -> Result<Vec<u8>, ReadWriteError> {
561
    let head = ReadScope::new(&provider.read_table_data(tag::HEAD)?).read::<HeadTable>()?;
562
    let maxp = ReadScope::new(&provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
563

            
564
    let sfnt_version = tags
565
        .iter()
566
        .position(|&tag| tag == tag::CFF)
567
        .map(|_| tables::CFF_MAGIC)
568
        .unwrap_or(tables::TTF_MAGIC);
569
    let mut builder = FontBuilder::new(sfnt_version, TableFilter::All);
570
    let mut wants_glyf = false;
571
    for &tag in tags {
572
        match tag {
573
            tag::GLYF => wants_glyf = true,
574
            tag::HEAD | tag::MAXP | tag::LOCA => (),
575
            _ => {
576
                builder.add_table::<_, ReadScope<'_>>(
577
                    tag,
578
                    ReadScope::new(&provider.read_table_data(tag)?),
579
                    (),
580
                )?;
581
            }
582
        }
583
    }
584
    // maxp and head are required for the font to be usable, so they're always added.
585
    builder.add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())?;
586
    let mut builder_with_head = builder.add_head_table(&head)?;
587

            
588
    // Add glyf and loca if requested, glyf implies loca. They may not be requested in the case of
589
    // a CFF font, or CBDT/CBLC font.
590
    if wants_glyf {
591
        let loca_data = provider.read_table_data(tag::LOCA)?;
592
        let loca = ReadScope::new(&loca_data)
593
            .read_dep::<LocaTable<'_>>((maxp.num_glyphs, head.index_to_loc_format))?;
594
        let glyf_data = provider.read_table_data(tag::GLYF)?;
595
        let glyf = ReadScope::new(&glyf_data).read_dep::<GlyfTable<'_>>(&loca)?;
596
        builder_with_head.add_glyf_table(glyf)?;
597
    }
598
    builder_with_head.data()
599
}
600

            
601
fn create_hmtx_table<'b>(
602
    hmtx: &HmtxTable<'_>,
603
    num_h_metrics: usize,
604
    subset_glyphs: &impl SubsetGlyphs,
605
) -> Result<HmtxTable<'b>, ReadWriteError> {
606
    let mut h_metrics = Vec::with_capacity(num_h_metrics);
607

            
608
    for glyph_id in 0..subset_glyphs.len() {
609
        // Cast is safe as glyph indexes are 16-bit values
610
        let old_id = usize::from(subset_glyphs.old_id(glyph_id as u16));
611

            
612
        if old_id < num_h_metrics {
613
            h_metrics.push(hmtx.h_metrics.read_item(old_id)?);
614
        } else {
615
            // As an optimization, the number of records can be less than the number of glyphs, in which case the
616
            // advance width value of the last record applies to all remaining glyph IDs.
617
            // https://docs.microsoft.com/en-us/typography/opentype/spec/hmtx
618
            let mut metric = hmtx.h_metrics.read_item(num_h_metrics - 1)?;
619
            metric.lsb = hmtx.left_side_bearings.read_item(old_id - num_h_metrics)?;
620
            h_metrics.push(metric);
621
        }
622
    }
623

            
624
    Ok(HmtxTable {
625
        h_metrics: ReadArrayCow::Owned(h_metrics),
626
        left_side_bearings: ReadArrayCow::Owned(vec![]),
627
    })
628
}
629

            
630
fn subset_os2(os2: &Os2, mappings: &MappingsToKeep<OldIds>) -> Os2 {
631
    let (new_first, new_last) = mappings.first_last_codepoints();
632
    let new_unicode_mask = mappings.unicode_bitmask();
633
    let new_ul_unicode_range1 = (new_unicode_mask & 0xFFFF_FFFF) as u32;
634
    let new_ul_unicode_range2 = ((new_unicode_mask >> 32) & 0xFFFF_FFFF) as u32;
635
    let new_ul_unicode_range3 = ((new_unicode_mask >> 64) & 0xFFFF_FFFF) as u32;
636
    let new_ul_unicode_range4 = ((new_unicode_mask >> 96) & 0xFFFF_FFFF) as u32;
637

            
638
    Os2 {
639
        version: os2.version,
640
        x_avg_char_width: os2.x_avg_char_width, // Ideally would be recalculated based on subset glyphs
641
        us_weight_class: os2.us_weight_class,
642
        us_width_class: os2.us_width_class,
643
        fs_type: os2.fs_type,
644
        y_subscript_x_size: os2.y_subscript_x_size,
645
        y_subscript_y_size: os2.y_subscript_y_size,
646
        y_subscript_x_offset: os2.y_subscript_x_offset,
647
        y_subscript_y_offset: os2.y_subscript_y_offset,
648
        y_superscript_x_size: os2.y_superscript_x_size,
649
        y_superscript_y_size: os2.y_superscript_y_size,
650
        y_superscript_x_offset: os2.y_superscript_x_offset,
651
        y_superscript_y_offset: os2.y_superscript_y_offset,
652
        y_strikeout_size: os2.y_strikeout_size,
653
        y_strikeout_position: os2.y_strikeout_position,
654
        s_family_class: os2.s_family_class,
655
        panose: os2.panose,
656
        ul_unicode_range1: new_ul_unicode_range1,
657
        ul_unicode_range2: new_ul_unicode_range2,
658
        ul_unicode_range3: new_ul_unicode_range3,
659
        ul_unicode_range4: new_ul_unicode_range4,
660
        ach_vend_id: os2.ach_vend_id,
661
        fs_selection: os2.fs_selection,
662
        // Fonts that support supplementary characters should set the value in this field to
663
        // 0xFFFF if the minimum index value is a supplementary character.
664
        us_first_char_index: u16::try_from(new_first).unwrap_or(0xFFFF),
665
        us_last_char_index: u16::try_from(new_last).unwrap_or(0xFFFF),
666
        version0: os2.version0.clone(),
667
        version1: os2.version1.clone(),
668
        version2to4: os2.version2to4.clone(),
669
        version5: os2.version5.clone(),
670
    }
671
}
672

            
673
impl FontBuilder {
674
    pub fn new(sfnt_version: u32, filter: TableFilter) -> Self {
675
        FontBuilder {
676
            sfnt_version,
677
            tables: BTreeMap::new(),
678
            filter,
679
        }
680
    }
681

            
682
    pub fn add_table<HostType, T: WriteBinaryDep<HostType>>(
683
        &mut self,
684
        tag: u32,
685
        table: HostType,
686
        args: T::Args,
687
    ) -> Result<T::Output, ReadWriteError> {
688
        assert_ne!(tag, tag::HEAD, "head table must use add_head_table");
689
        assert_ne!(tag, tag::GLYF, "glyf table must use add_glyf_table");
690

            
691
        self.add_table_inner::<HostType, T>(tag, table, args)
692
    }
693

            
694
    pub fn table_tags(&self) -> impl Iterator<Item = u32> + '_ {
695
        self.tables.keys().copied()
696
    }
697

            
698
    fn add_table_inner<HostType, T: WriteBinaryDep<HostType>>(
699
        &mut self,
700
        tag: u32,
701
        table: HostType,
702
        args: T::Args,
703
    ) -> Result<T::Output, ReadWriteError> {
704
        let mut buffer = WriteBuffer::new();
705
        let output = T::write_dep(&mut buffer, table, args)?;
706

            
707
        // It's a bit wasteful doing the write when it's not needed,
708
        // but we need to be able to return T::Output
709
        if self.filter.contains(tag) {
710
            self.tables.insert(tag, buffer);
711
        }
712

            
713
        Ok(output)
714
    }
715

            
716
    pub fn add_head_table(
717
        mut self,
718
        table: &HeadTable,
719
    ) -> Result<FontBuilderWithHead, ReadWriteError> {
720
        let placeholder = self.add_table_inner::<_, HeadTable>(tag::HEAD, table, ())?;
721

            
722
        Ok(FontBuilderWithHead {
723
            inner: self,
724
            check_sum_adjustment: placeholder,
725
            index_to_loc_format: table.index_to_loc_format,
726
        })
727
    }
728
}
729

            
730
impl FontBuilderWithHead {
731
    pub fn add_glyf_table(&mut self, table: GlyfTable<'_>) -> Result<(), ReadWriteError> {
732
        let loca = self.inner.add_table_inner::<_, GlyfTable<'_>>(
733
            tag::GLYF,
734
            table,
735
            self.index_to_loc_format,
736
        )?;
737
        self.inner.add_table_inner::<_, loca::owned::LocaTable>(
738
            tag::LOCA,
739
            loca,
740
            self.index_to_loc_format,
741
        )?;
742

            
743
        Ok(())
744
    }
745

            
746
    /// Returns a `Vec<u8>` containing the built font
747
    pub fn data(mut self) -> Result<Vec<u8>, ReadWriteError> {
748
        let mut font = WriteBuffer::new();
749

            
750
        self.write_offset_table(&mut font)?;
751
        let table_offset =
752
            long_align(self.inner.tables.len() * TableRecord::SIZE + font.bytes_written());
753

            
754
        // Add tables in desired order
755
        let mut ordered_tables = self.write_table_directory(&mut font)?;
756

            
757
        // pad
758
        let length = font.bytes_written();
759
        let padded_length = long_align(length);
760
        assert_eq!(
761
            padded_length, table_offset,
762
            "offset after writing table directory is not at expected position"
763
        );
764
        font.write_zeros(padded_length - length)?;
765

            
766
        // Fill in check_sum_adjustment in the head table. the magic number comes from the OpenType spec.
767
        let headers_checksum = checksum::table_checksum(font.bytes())?;
768
        let checksum = Wrapping(0xB1B0AFBA) - (headers_checksum + ordered_tables.checksum);
769

            
770
        // Write out the font tables
771
        let mut placeholder = Some(self.check_sum_adjustment);
772
        for TaggedBuffer { tag, buffer } in ordered_tables.tables.iter_mut() {
773
            if *tag == tag::HEAD {
774
                buffer.write_placeholder(placeholder.take().unwrap(), checksum.0)?;
775
            }
776
            font.write_bytes(buffer.bytes())?;
777
        }
778

            
779
        Ok(font.into_inner())
780
    }
781

            
782
    fn write_offset_table(&self, font: &mut WriteBuffer) -> Result<(), WriteError> {
783
        let num_tables = u16::try_from(self.inner.tables.len())?;
784
        let n = max_power_of_2(num_tables);
785
        let search_range = (1 << n) * 16;
786
        let entry_selector = n;
787
        let range_shift = num_tables * 16 - search_range;
788

            
789
        U32Be::write(font, self.inner.sfnt_version)?;
790
        U16Be::write(font, num_tables)?;
791
        U16Be::write(font, search_range)?;
792
        U16Be::write(font, entry_selector)?;
793
        U16Be::write(font, range_shift)?;
794

            
795
        Ok(())
796
    }
797

            
798
    fn write_table_directory(
799
        &mut self,
800
        font: &mut WriteBuffer,
801
    ) -> Result<OrderedTables, ReadWriteError> {
802
        let mut tables = Vec::with_capacity(self.inner.tables.len());
803
        let mut checksum = Wrapping(0);
804
        let mut table_offset =
805
            long_align(self.inner.tables.len() * TableRecord::SIZE + font.bytes_written());
806

            
807
        for (tag, mut table) in std::mem::take(&mut self.inner.tables) {
808
            let length = table.len();
809
            let padded_length = long_align(length);
810
            table.write_zeros(padded_length - length)?;
811

            
812
            let table_checksum = checksum::table_checksum(table.bytes())?;
813
            checksum += table_checksum;
814

            
815
            let record = TableRecord {
816
                table_tag: tag,
817
                checksum: table_checksum.0,
818
                offset: u32::try_from(table_offset).map_err(WriteError::from)?,
819
                length: u32::try_from(length).map_err(WriteError::from)?,
820
            };
821

            
822
            table_offset += padded_length;
823
            TableRecord::write(font, &record)?;
824
            tables.push(TaggedBuffer { tag, buffer: table });
825
        }
826

            
827
        Ok(OrderedTables { tables, checksum })
828
    }
829
}
830

            
831
impl TableFilter {
832
    fn contains(&self, tag: u32) -> bool {
833
        match self {
834
            TableFilter::All => true,
835
            TableFilter::Tables(tables) => tables.contains(&tag),
836
        }
837
    }
838
}
839

            
840
/// Calculate the maximum power of 2 that is <= num
841
fn max_power_of_2(num: u16) -> u16 {
842
    15u16.saturating_sub(num.leading_zeros() as u16)
843
}
844

            
845
/// Prince specific subsetting behaviour.
846
///
847
/// prince::subset will produce a bare CFF table in the case of an input CFF font.
848
#[cfg(feature = "prince")]
849
pub mod prince {
850
    use super::{
851
        tag, FontTableProvider, MappingsToKeep, ReadScope, SubsetError, WriteBinary, WriteBuffer,
852
        CFF,
853
    };
854
    use crate::cff::cff2::{OutputFormat, CFF2};
855
    use crate::subset::{CmapTarget, SubsetProfile};
856
    use crate::tables::cmap::subset::CmapStrategy;
857
    use std::ffi::c_int;
858

            
859
    /// This enum describes the desired cmap generation and maps to the `cmap_target` type in Prince
860
    #[derive(Debug, Clone)]
861
    pub enum PrinceCmapTarget {
862
        /// Build a suitable cmap table
863
        Unrestricted,
864
        /// Build a Mac Roman cmap table
865
        MacRoman,
866
        /// Omit the cmap table entirely
867
        Omit,
868
        /// Use the supplied array as a Mac Roman cmap table
869
        MacRomanCmap(Box<[u8; 256]>),
870
    }
871

            
872
    impl PrinceCmapTarget {
873
        /// Build a new cmap from a `cmap_target` tag
874
        pub fn new(tag: c_int, cmap: Option<Box<[u8; 256]>>) -> Self {
875
            // NOTE: These tags should be kept in sync with the `cmap_target` type in Prince.
876
            match (tag, cmap) {
877
                (1, _) => PrinceCmapTarget::Unrestricted,
878
                (2, _) => PrinceCmapTarget::MacRoman,
879
                (3, _) => PrinceCmapTarget::Omit,
880
                (4, Some(cmap)) => PrinceCmapTarget::MacRomanCmap(cmap),
881
                _ => panic!("invalid value for PrinceCmapTarget: {}", tag),
882
            }
883
        }
884
    }
885

            
886
    /// Subset this font so that it only contains the glyphs with the supplied `glyph_ids`.
887
    ///
888
    /// Returns just the CFF table in the case of a CFF font, not a complete OpenType font.
889
    pub fn subset(
890
        provider: &impl FontTableProvider,
891
        glyph_ids: &[u16],
892
        cmap_target: PrinceCmapTarget,
893
        convert_cff_to_cid_if_more_than_255_glyphs: bool,
894
    ) -> Result<Vec<u8>, SubsetError> {
895
        if provider.has_table(tag::CFF) {
896
            subset_cff_table(
897
                provider,
898
                glyph_ids,
899
                convert_cff_to_cid_if_more_than_255_glyphs,
900
            )
901
        } else if provider.has_table(tag::CFF2) {
902
            subset_cff2_table(provider, glyph_ids)
903
        } else {
904
            let cmap_strategy = match cmap_target {
905
                PrinceCmapTarget::Unrestricted => {
906
                    let mappings_to_keep =
907
                        MappingsToKeep::new(provider, glyph_ids, CmapTarget::Unrestricted)?;
908
                    CmapStrategy::Generate(mappings_to_keep)
909
                }
910
                PrinceCmapTarget::MacRoman => {
911
                    let mappings_to_keep =
912
                        MappingsToKeep::new(provider, glyph_ids, CmapTarget::MacRoman)?;
913
                    CmapStrategy::Generate(mappings_to_keep)
914
                }
915
                PrinceCmapTarget::Omit => CmapStrategy::Omit,
916
                PrinceCmapTarget::MacRomanCmap(cmap) => CmapStrategy::MacRomanSupplied(cmap),
917
            };
918
            super::subset_ttf(provider, glyph_ids, cmap_strategy, &SubsetProfile::Pdf)
919
                .map_err(SubsetError::from)
920
        }
921
    }
922

            
923
    /// Subset the CFF table and discard the rest
924
    ///
925
    /// Useful for PDF because a CFF table can be embedded directly without the need to wrap it in
926
    /// an OTF.
927
    fn subset_cff_table(
928
        provider: &impl FontTableProvider,
929
        glyph_ids: &[u16],
930
        convert_cff_to_cid_if_more_than_255_glyphs: bool,
931
    ) -> Result<Vec<u8>, SubsetError> {
932
        let cff_data = provider.read_table_data(tag::CFF)?;
933
        let scope = ReadScope::new(&cff_data);
934
        let cff: CFF<'_> = scope.read::<CFF<'_>>()?;
935
        if cff.name_index.len() != 1 || cff.fonts.len() != 1 {
936
            return Err(SubsetError::InvalidFontCount);
937
        }
938

            
939
        // Build the new CFF table
940
        let cff = cff
941
            .subset(glyph_ids, convert_cff_to_cid_if_more_than_255_glyphs)?
942
            .into();
943

            
944
        let mut buffer = WriteBuffer::new();
945
        CFF::write(&mut buffer, &cff)?;
946

            
947
        Ok(buffer.into_inner())
948
    }
949

            
950
    /// Subset a non-variable CFF2 font into a CFF table
951
    pub fn subset_cff2_table(
952
        provider: &impl FontTableProvider,
953
        glyph_ids: &[u16],
954
    ) -> Result<Vec<u8>, SubsetError> {
955
        let cff2_data = provider.read_table_data(tag::CFF2)?;
956
        let scope = ReadScope::new(&cff2_data);
957
        let cff2: CFF2<'_> = scope.read::<CFF2<'_>>()?;
958

            
959
        // Build the new CFF table
960
        let cff = cff2
961
            .subset_to_cff(glyph_ids, provider, true, OutputFormat::CidOnly)?
962
            .into();
963

            
964
        let mut buffer = WriteBuffer::new();
965
        CFF::write(&mut buffer, &cff)?;
966

            
967
        Ok(buffer.into_inner())
968
    }
969
}
970

            
971
impl From<ParseError> for SubsetError {
972
    fn from(err: ParseError) -> SubsetError {
973
        SubsetError::Parse(err)
974
    }
975
}
976

            
977
impl From<WriteError> for SubsetError {
978
    fn from(err: WriteError) -> SubsetError {
979
        SubsetError::Write(err)
980
    }
981
}
982

            
983
impl From<CFFError> for SubsetError {
984
    fn from(err: CFFError) -> SubsetError {
985
        SubsetError::CFF(err)
986
    }
987
}
988

            
989
impl From<ReadWriteError> for SubsetError {
990
    fn from(err: ReadWriteError) -> SubsetError {
991
        match err {
992
            ReadWriteError::Read(err) => SubsetError::Parse(err),
993
            ReadWriteError::Write(err) => SubsetError::Write(err),
994
        }
995
    }
996
}
997

            
998
impl fmt::Display for SubsetError {
999
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SubsetError::Parse(err) => write!(f, "subset: parse error: {}", err),
            SubsetError::Write(err) => write!(f, "subset: write error: {}", err),
            SubsetError::CFF(err) => write!(f, "subset: CFF error: {}", err),
            SubsetError::NotDef => write!(f, "subset: first glyph is not .notdef"),
            SubsetError::TooManyGlyphs => write!(f, "subset: too many glyphs"),
            SubsetError::InvalidFontCount => write!(f, "subset: invalid font count in CFF font"),
        }
    }
}
impl std::error::Error for SubsetError {}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::font_data::FontData;
    use crate::tables::cmap::{Cmap, CmapSubtable};
    use crate::tables::glyf::{
        BoundingBox, CompositeGlyph, CompositeGlyphArgument, CompositeGlyphComponent,
        CompositeGlyphFlag, GlyfRecord, Glyph, Point, SimpleGlyph, SimpleGlyphFlag,
    };
    use crate::tables::{LongHorMetric, OpenTypeData, OpenTypeFont};
    use crate::tag::DisplayTag;
    use crate::tests::read_fixture;
    use crate::Font;
    use std::collections::HashSet;
    macro_rules! read_table {
        ($file:ident, $scope:expr, $tag:path, $t:ty) => {
            $file
                .read_table(&$scope, $tag)
                .expect("error reading table")
                .expect("no table found")
                .read::<$t>()
                .expect("unable to parse")
        };
        ($file:ident, $scope:expr, $tag:path, $t:ty, $args:expr) => {
            $file
                .read_table(&$scope, $tag)
                .expect("error reading table")
                .expect("no table found")
                .read_dep::<$t>($args)
                .expect("unable to parse")
        };
    }
    #[test]
    fn create_glyf_and_hmtx() {
        let buffer = read_fixture("tests/fonts/opentype/SFNT-TTF-Composite.ttf");
        let fontfile = ReadScope::new(&buffer)
            .read::<OpenTypeFont<'_>>()
            .expect("error reading OpenTypeFile");
        let font = match fontfile.data {
            OpenTypeData::Single(font) => font,
            OpenTypeData::Collection(_) => unreachable!(),
        };
        let head = read_table!(font, fontfile.scope, tag::HEAD, HeadTable);
        let maxp = read_table!(font, fontfile.scope, tag::MAXP, MaxpTable);
        let hhea = read_table!(font, fontfile.scope, tag::HHEA, HheaTable);
        let loca = read_table!(
            font,
            fontfile.scope,
            tag::LOCA,
            LocaTable<'_>,
            (maxp.num_glyphs, head.index_to_loc_format)
        );
        let glyf = read_table!(font, fontfile.scope, tag::GLYF, GlyfTable<'_>, &loca);
        let hmtx = read_table!(
            font,
            fontfile.scope,
            tag::HMTX,
            HmtxTable<'_>,
            (
                usize::from(maxp.num_glyphs),
                usize::from(hhea.num_h_metrics),
            )
        );
        // 0 - .notdef
        // 2 - composite
        // 4 - simple
        let glyph_ids = [0, 2, 4];
        let subset_glyphs = glyf.subset(&glyph_ids).unwrap();
        let expected_glyf = GlyfTable::new(vec![
            GlyfRecord::empty(),
            GlyfRecord::Parsed(Glyph::Composite(CompositeGlyph {
                bounding_box: BoundingBox {
                    x_min: 205,
                    x_max: 4514,
                    y_min: 0,
                    y_max: 1434,
                },
                glyphs: vec![
                    CompositeGlyphComponent {
                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
                            | CompositeGlyphFlag::MORE_COMPONENTS
                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
                        glyph_index: 3,
                        argument1: CompositeGlyphArgument::I16(3453),
                        argument2: CompositeGlyphArgument::I16(0),
                        scale: None,
                    },
                    CompositeGlyphComponent {
                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
                            | CompositeGlyphFlag::MORE_COMPONENTS
                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
                        glyph_index: 4,
                        argument1: CompositeGlyphArgument::I16(2773),
                        argument2: CompositeGlyphArgument::I16(0),
                        scale: None,
                    },
                    CompositeGlyphComponent {
                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
                            | CompositeGlyphFlag::MORE_COMPONENTS
                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
                        glyph_index: 5,
                        argument1: CompositeGlyphArgument::I16(1182),
                        argument2: CompositeGlyphArgument::I16(0),
                        scale: None,
                    },
                    CompositeGlyphComponent {
                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
                        glyph_index: 2,
                        argument1: CompositeGlyphArgument::I16(205),
                        argument2: CompositeGlyphArgument::I16(0),
                        scale: None,
                    },
                ],
                instructions: Box::default(),
                phantom_points: None,
            })),
            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
                bounding_box: BoundingBox {
                    x_min: 0,
                    x_max: 1073,
                    y_min: 0,
                    y_max: 1434,
                },
                end_pts_of_contours: vec![9],
                instructions: Box::default(),
                coordinates: vec![
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(0, 1434),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(1073, 1434),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(1073, 1098),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(485, 1098),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(485, 831),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(987, 831),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(987, 500),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(485, 500),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(485, 0),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point::zero(),
                    ),
                ],
                phantom_points: None,
            })),
            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
                bounding_box: BoundingBox {
                    x_min: 0,
                    x_max: 1061,
                    y_min: 0,
                    y_max: 1434,
                },
                end_pts_of_contours: vec![5],
                instructions: Box::default(),
                coordinates: vec![
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(0, 1434),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(485, 1434),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(485, 369),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(1061, 369),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(1061, 0),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point::zero(),
                    ),
                ],
                phantom_points: None,
            })),
            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
                bounding_box: BoundingBox {
                    x_min: 0,
                    x_max: 485,
                    y_min: 0,
                    y_max: 1434,
                },
                end_pts_of_contours: vec![3],
                instructions: Box::default(),
                coordinates: vec![
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(0, 1434),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(485, 1434),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(485, 0),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point::zero(),
                    ),
                ],
                phantom_points: None,
            })),
            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
                bounding_box: BoundingBox {
                    x_min: 0,
                    x_max: 1478,
                    y_min: 0,
                    y_max: 1434,
                },
                end_pts_of_contours: vec![7, 10],
                instructions: Box::default(),
                coordinates: vec![
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point::zero(),
                    ),
                    (SimpleGlyphFlag::ON_CURVE_POINT.into(), Point(436, 1434)),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(1042, 1434),
                    ),
                    (SimpleGlyphFlag::ON_CURVE_POINT.into(), Point(1478, 0)),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(975, 0),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_SHORT_VECTOR
                            | SimpleGlyphFlag::Y_SHORT_VECTOR
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(909, 244),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(493, 244),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_SHORT_VECTOR
                            | SimpleGlyphFlag::Y_SHORT_VECTOR,
                        Point(430, 0),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_SHORT_VECTOR
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
                        Point(579, 565),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT
                            | SimpleGlyphFlag::X_SHORT_VECTOR
                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR
                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
                        Point(825, 565),
                    ),
                    (
                        SimpleGlyphFlag::ON_CURVE_POINT | SimpleGlyphFlag::X_SHORT_VECTOR,
                        Point(702, 1032),
                    ),
                ],
                phantom_points: None,
            })),
        ])
        .unwrap();
        let num_h_metrics = usize::from(hhea.num_h_metrics);
        let hmtx = create_hmtx_table(&hmtx, num_h_metrics, &subset_glyphs).unwrap();
        let mut glyf: GlyfTable<'_> = subset_glyphs.into();
        glyf.records_mut()
            .iter_mut()
            .for_each(|rec| rec.parse().unwrap());
        assert_eq!(glyf, expected_glyf);
        let expected = vec![
            LongHorMetric {
                advance_width: 1536,
                lsb: 0,
            },
            LongHorMetric {
                advance_width: 4719,
                lsb: 205,
            },
            LongHorMetric {
                advance_width: 0,
                lsb: 0,
            },
            LongHorMetric {
                advance_width: 0,
                lsb: 0,
            },
            LongHorMetric {
                advance_width: 0,
                lsb: 0,
            },
            LongHorMetric {
                advance_width: 0,
                lsb: 0,
            },
        ];
        assert_eq!(hmtx.h_metrics.iter().collect::<Vec<_>>(), expected);
        assert_eq!(hmtx.left_side_bearings.iter().collect::<Vec<_>>(), vec![]);
    }
    #[test]
    fn font_builder() {
        // Test that reading a font in, adding all its tables and writing it out equals the
        // original font
        let buffer = read_fixture("tests/fonts/opentype/test-font.ttf");
        let fontfile = ReadScope::new(&buffer)
            .read::<OpenTypeFont<'_>>()
            .expect("error reading OpenTypeFile");
        let font = match fontfile.data {
            OpenTypeData::Single(font) => font,
            OpenTypeData::Collection(_) => unreachable!(),
        };
        let head = read_table!(font, fontfile.scope, tag::HEAD, HeadTable);
        let maxp = read_table!(font, fontfile.scope, tag::MAXP, MaxpTable);
        let hhea = read_table!(font, fontfile.scope, tag::HHEA, HheaTable);
        let loca = read_table!(
            font,
            fontfile.scope,
            tag::LOCA,
            LocaTable<'_>,
            (maxp.num_glyphs, head.index_to_loc_format)
        );
        let glyf = read_table!(font, fontfile.scope, tag::GLYF, GlyfTable<'_>, &loca);
        let hmtx = read_table!(
            font,
            fontfile.scope,
            tag::HMTX,
            HmtxTable<'_>,
            (
                usize::from(maxp.num_glyphs),
                usize::from(hhea.num_h_metrics),
            )
        );
        let mut builder = FontBuilder::new(tables::TTF_MAGIC, TableFilter::All);
        builder
            .add_table::<_, HheaTable>(tag::HHEA, &hhea, ())
            .unwrap();
        builder
            .add_table::<_, HmtxTable<'_>>(tag::HMTX, &hmtx, ())
            .unwrap();
        builder
            .add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())
            .unwrap();
        let tables_added = [
            tag::HEAD,
            tag::GLYF,
            tag::HHEA,
            tag::HMTX,
            tag::MAXP,
            tag::LOCA,
        ]
        .iter()
        .collect::<HashSet<&u32>>();
        for record in font.table_records.iter() {
            if tables_added.contains(&record.table_tag) {
                continue;
            }
            let table = font
                .read_table(&fontfile.scope, record.table_tag)
                .unwrap()
                .unwrap();
            builder
                .add_table::<_, ReadScope<'_>>(record.table_tag, table, ())
                .unwrap();
        }
        let mut builder = builder.add_head_table(&head).unwrap();
        builder.add_glyf_table(glyf).unwrap();
        let data = builder.data().unwrap();
        let new_fontfile = ReadScope::new(&data)
            .read::<OpenTypeFont<'_>>()
            .expect("error reading new OpenTypeFile");
        let new_font = match new_fontfile.data {
            OpenTypeData::Single(font) => font,
            OpenTypeData::Collection(_) => unreachable!(),
        };
        assert_eq!(new_font.table_records.len(), font.table_records.len());
        for record in font.table_records.iter() {
            match record.table_tag {
                tag::GLYF | tag::LOCA => {
                    // TODO: check content of glyf and loca
                    // glyf differs because we don't do anything fancy with points at the moment
                    // and always write them out as i16 values.
                    // loca differs because glyf differs
                    continue;
                }
                tag => {
                    let new_record = new_font.find_table_record(record.table_tag).unwrap();
                    let tag = DisplayTag(tag);
                    assert_eq!((tag, new_record.checksum), (tag, record.checksum));
                }
            }
        }
    }
    #[test]
    fn invalid_glyph_id() {
        // Test to ensure that invalid glyph ids don't panic when subsetting
        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let mut glyph_ids = [0, 9999];
        match subset(
            &opentype_file.table_provider(0).unwrap(),
            &mut glyph_ids,
            &SubsetProfile::Pdf,
            CmapTarget::Unrestricted,
        ) {
            Err(SubsetError::Parse(ParseError::BadIndex)) => {}
            err => panic!(
                "expected SubsetError::Parse(ParseError::BadIndex) got {:?}",
                err
            ),
        }
    }
    #[test]
    fn empty_mappings_to_keep() {
        // Test to ensure that an empty mappings to keep doesn't panic when subsetting
        let buffer = read_fixture("tests/fonts/opentype/SourceCodePro-Regular.otf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        // glyph 118 is not Unicode, so does not end up in the mappings to keep
        let mut glyph_ids = [0, 118];
        let subset_font_data = subset(
            &opentype_file.table_provider(0).unwrap(),
            &mut glyph_ids,
            &SubsetProfile::Pdf,
            CmapTarget::Unrestricted,
        )
        .unwrap();
        let opentype_file = ReadScope::new(&subset_font_data)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let font = Font::new(opentype_file.table_provider(0).unwrap()).unwrap();
        let cmap = ReadScope::new(font.cmap_subtable_data())
            .read::<CmapSubtable<'_>>()
            .unwrap();
        // If mappings_to_keep is empty a mac roman cmap sub-table is created, which doesn't
        // care that it's empty.
        if let CmapSubtable::Format0 { glyph_id_array, .. } = cmap {
            assert!(glyph_id_array.iter().all(|x| x == 0));
        } else {
            panic!("expected cmap sub-table format 0");
        }
    }
    #[test]
    fn ttf_mappings_to_keep_is_none() {
        // Test that when subsetting a TTF font with mappings_to_keep set to None the cmap table is
        // omitted from the subset font.
        let buffer = read_fixture("tests/fonts/opentype/test-font.ttf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let mut glyph_ids = [0, 2];
        let subset_font_data = subset_ttf(
            &opentype_file.table_provider(0).unwrap(),
            &mut glyph_ids,
            CmapStrategy::Omit,
            &SubsetProfile::Pdf,
        )
        .unwrap();
        let opentype_file = ReadScope::new(&subset_font_data)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let table_provider = opentype_file.table_provider(0).unwrap();
        assert!(!table_provider.has_table(tag::CMAP));
    }
    // This test ensures we can call whole_font on a font without a `glyf` table (E.g. CFF).
    #[test]
    fn test_whole_font() {
        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope
            .read::<FontData<'_>>()
            .expect("unable to read FontFile");
        let provider = font_file
            .table_provider(0)
            .expect("unable to get FontTableProvider");
        let tags = [
            tag::CFF,
            tag::GDEF,
            tag::GPOS,
            tag::GSUB,
            tag::OS_2,
            tag::CMAP,
            tag::HEAD,
            tag::HHEA,
            tag::HMTX,
            tag::MAXP,
            tag::NAME,
            tag::POST,
        ];
        assert!(whole_font(&provider, &tags).is_ok());
    }
    #[test]
    fn test_max_power_of_2() {
        assert_eq!(max_power_of_2(0), 0);
        assert_eq!(max_power_of_2(1), 0);
        assert_eq!(max_power_of_2(2), 1);
        assert_eq!(max_power_of_2(4), 2);
        assert_eq!(max_power_of_2(8), 3);
        assert_eq!(max_power_of_2(16), 4);
        assert_eq!(max_power_of_2(49), 5);
        assert_eq!(max_power_of_2(std::u16::MAX), 15);
    }
    #[test]
    fn subset_cff2_type1() {
        let buffer = read_fixture("tests/fonts/opentype/cff2/SourceSans3.abc.otf");
        let otf = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let provider = otf.table_provider(0).expect("error reading font file");
        // Subset the CFF2, producing CFF. Since there is only two glyphs in the subset font it
        // will produce a Type 1 CFF font.
        let new_font = subset(
            &provider,
            &[0, 1],
            &SubsetProfile::Pdf,
            CmapTarget::Unrestricted,
        )
        .unwrap();
        // Read it back
        let subset_otf = ReadScope::new(&new_font)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let provider = subset_otf
            .table_provider(0)
            .expect("error reading new font");
        let cff_data = provider
            .read_table_data(tag::CFF)
            .expect("unable to read CFF data");
        let res = ReadScope::new(&cff_data).read::<CFF<'_>>();
        assert!(res.is_ok());
        let cff = res.unwrap();
        let font = &cff.fonts[0];
        assert!(!font.is_cid_keyed());
    }
    #[test]
    fn subset_cff2_cid() {
        let buffer = read_fixture("tests/fonts/opentype/cff2/SourceSans3-Instance.256.otf");
        let otf = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let provider = otf.table_provider(0).expect("error reading font file");
        // Subset the CFF2, producing CFF. Since there is more than 255 glyphs in the subset font it
        // will produce a CID-keyed CFF font.
        let glyph_ids = (0..=256).collect::<Vec<_>>();
        let new_font = subset(
            &provider,
            &glyph_ids,
            &SubsetProfile::Pdf,
            CmapTarget::Unrestricted,
        )
        .unwrap();
        // Read it back
        let subset_otf = ReadScope::new(&new_font)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let provider = subset_otf
            .table_provider(0)
            .expect("error reading new font");
        let cff_data = provider
            .read_table_data(tag::CFF)
            .expect("unable to read CFF data");
        let res = ReadScope::new(&cff_data).read::<CFF<'_>>();
        assert!(res.is_ok());
        let cff = res.unwrap();
        assert_eq!(cff.fonts.len(), 1);
        let font = &cff.fonts[0];
        assert!(font.is_cid_keyed());
    }
    #[test]
    fn test_subset_with_os2_and_unicode_cmap() {
        // Test string to use for the font subset
        let test_string = "hello world";
        // Load the font
        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let provider = opentype_file.table_provider(0).unwrap();
        // Create a font instance to access cmap
        let font = Font::new(provider).unwrap();
        // Get the cmap subtable for unicode mapping
        let cmap_data = font.cmap_subtable_data();
        let cmap_subtable = ReadScope::new(cmap_data)
            .read::<CmapSubtable<'_>>()
            .unwrap();
        // Map characters to glyph IDs
        let mut glyph_ids = vec![0]; // Always include glyph 0 (.notdef)
        for c in test_string.chars() {
            if let Ok(Some(glyph_id)) = cmap_subtable.map_glyph(c as u32) {
                glyph_ids.push(glyph_id);
            }
        }
        // Sort and deduplicate glyph IDs
        glyph_ids.sort();
        glyph_ids.dedup();
        // Subset the font
        let subset_buffer = subset(
            &font.font_table_provider,
            &glyph_ids,
            &SubsetProfile::Minimal,
            CmapTarget::Unicode,
        )
        .unwrap();
        drop(font); // so we don't accidentally use it below
        // Validate that the OS/2 table is present in the subsetted font
        let subset_otf = ReadScope::new(&subset_buffer)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let subset_provider = subset_otf.table_provider(0).unwrap();
        // Check that OS/2 table exists
        assert!(
            subset_provider.has_table(tag::OS_2),
            "Subset font is missing the OS/2 table."
        );
        // Read back the cmap and check that it's a unicode cmap
        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
        assert!(
            cmap.find_subtable(PlatformId::UNICODE, EncodingId::UNICODE_BMP)
                .is_some(),
            "subset font does not have expected Unicode cmap"
        );
    }
    #[test]
    fn test_subset_with_macroman_cmap() {
        // Test string to use for the font subset
        let test_string = "hello world";
        // Load the font
        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let provider = opentype_file.table_provider(0).unwrap();
        // Create a font instance to access cmap
        let font = Font::new(provider).unwrap();
        // Get the cmap subtable for unicode mapping
        let cmap_data = font.cmap_subtable_data();
        let cmap_subtable = ReadScope::new(cmap_data)
            .read::<CmapSubtable<'_>>()
            .unwrap();
        // Map characters to glyph IDs
        let mut glyph_ids = vec![0]; // Always include glyph 0 (.notdef)
        for c in test_string.chars() {
            if let Ok(Some(glyph_id)) = cmap_subtable.map_glyph(c as u32) {
                glyph_ids.push(glyph_id);
            }
        }
        // Sort and deduplicate glyph IDs
        glyph_ids.sort();
        glyph_ids.dedup();
        // Subset the font
        let subset_buffer = subset(
            &font.font_table_provider,
            &glyph_ids,
            &SubsetProfile::Minimal,
            CmapTarget::Unrestricted,
        )
        .unwrap();
        drop(font); // so we don't accidentally use it below
        let subset_otf = ReadScope::new(&subset_buffer)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let subset_provider = subset_otf.table_provider(0).unwrap();
        // Read back the cmap and check that it's a Mac Roman cmap (because all the selected
        // glyphs are in the Mac Roman character set)
        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
        assert!(
            cmap.find_subtable(PlatformId::MACINTOSH, EncodingId::MACINTOSH_APPLE_ROMAN)
                .is_some(),
            "subset font does not have expected Mac Roman cmap"
        );
    }
    #[test]
    fn parse_custom_profile() {
        let tables = "fpGm,OS/2 os2,GSUB".to_string();
        let custom = SubsetProfile::parse_custom(tables)
            .unwrap()
            .get_tables(&[])
            .iter()
            .copied()
            .map(|table| DisplayTag(table).to_string())
            .collect::<Vec<_>>();
        let expected = vec![
            tag::GSUB,
            tag::OS_2,
            tag::CMAP,
            tag::FPGM,
            tag::HEAD,
            tag::HHEA,
            tag::HMTX,
            tag::MAXP,
            tag::NAME,
            tag::POST,
        ]
        .into_iter()
        .map(|table| DisplayTag(table).to_string())
        .collect::<Vec<_>>();
        assert_eq!(custom, expected)
    }
    #[test]
    fn parse_custom_profile_invalid() {
        assert!(SubsetProfile::parse_custom("toolong".to_string()).is_err());
        assert!(SubsetProfile::parse_custom("👓".to_string()).is_err());
    }
    #[test]
    fn notdef_only_cff_produces_valid_font() {
        // When glyph_ids contains only .notdef (glyph id 0), subset should produce
        // a valid font with an empty cmap table, not panic.
        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let glyph_ids = [0];
        let subset_buffer = subset(
            &opentype_file.table_provider(0).unwrap(),
            &glyph_ids,
            &SubsetProfile::Pdf,
            CmapTarget::Unicode,
        )
        .unwrap();
        // Parse the subset font and verify the cmap table is present and valid
        let subset_otf = ReadScope::new(&subset_buffer)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let subset_provider = subset_otf.table_provider(0).unwrap();
        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
        assert!(
            cmap.find_subtable(PlatformId::UNICODE, EncodingId::UNICODE_BMP)
                .is_some(),
            "subset font should have a Unicode BMP cmap subtable"
        );
    }
    #[test]
    fn notdef_only_ttf_produces_valid_font() {
        // Same test but with a TTF font to cover the subset_ttf path
        let buffer = read_fixture("tests/fonts/opentype/test-font.ttf");
        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
        let glyph_ids = [0];
        let subset_buffer = subset(
            &opentype_file.table_provider(0).unwrap(),
            &glyph_ids,
            &SubsetProfile::Pdf,
            CmapTarget::Unicode,
        )
        .unwrap();
        // Parse the subset font and verify the cmap table is present and valid
        let subset_otf = ReadScope::new(&subset_buffer)
            .read::<OpenTypeFont<'_>>()
            .unwrap();
        let subset_provider = subset_otf.table_provider(0).unwrap();
        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
        assert!(
            cmap.find_subtable(PlatformId::UNICODE, EncodingId::UNICODE_BMP)
                .is_some(),
            "subset font should have a Unicode BMP cmap subtable"
        );
    }
}