Skip to main content

allsorts/
subset.rs

1#![deny(missing_docs)]
2
3//! Font subsetting.
4
5use std::collections::BTreeMap;
6use std::fmt;
7use std::num::Wrapping;
8
9use crate::binary::read::{ReadArrayCow, ReadScope};
10use crate::binary::write::{Placeholder, WriteBinary};
11use crate::binary::write::{WriteBinaryDep, WriteBuffer, WriteContext};
12use crate::binary::{long_align, U16Be, U32Be};
13use crate::cff::cff2::{OutputFormat, CFF2};
14use crate::cff::{CFFError, SubsetCFF, CFF};
15use crate::error::{ParseError, ReadWriteError, WriteError};
16use crate::post::PostTable;
17use crate::tables::cmap::subset::{CmapStrategy, MappingsToKeep, NewIds, OldIds};
18use crate::tables::cmap::{owned, EncodingId, PlatformId};
19use crate::tables::glyf::GlyfTable;
20use crate::tables::loca::{self, LocaTable};
21use crate::tables::os2::Os2;
22use crate::tables::{
23    self, cmap, FontTableProvider, HeadTable, HheaTable, HmtxTable, IndexToLocFormat, MaxpTable,
24    TableRecord,
25};
26use crate::{checksum, tag};
27
28/// Minimal set of tables, suitable for PDF embedding
29const 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>
45const 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)]
58pub 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)]
69pub 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
84impl 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)]
161pub 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
177pub(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
188pub(crate) struct FontBuilder {
189    sfnt_version: u32,
190    tables: BTreeMap<u32, WriteBuffer>,
191    filter: TableFilter,
192}
193
194pub(crate) enum TableFilter {
195    /// Include all tables
196    All,
197    /// Include only the selected tables
198    Tables(Vec<u32>),
199}
200
201pub(crate) struct FontBuilderWithHead {
202    inner: FontBuilder,
203    check_sum_adjustment: Placeholder<U32Be, u32>,
204    index_to_loc_format: IndexToLocFormat,
205}
206
207struct TaggedBuffer {
208    tag: u32,
209    buffer: WriteBuffer,
210}
211
212struct 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`.
226pub 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.
259fn 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
367fn 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
404fn 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
440fn 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
525fn 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
534fn 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.
557pub 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
601fn 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
630fn 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
673impl 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
730impl 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
831impl 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
841fn 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")]
849pub 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
971impl From<ParseError> for SubsetError {
972    fn from(err: ParseError) -> SubsetError {
973        SubsetError::Parse(err)
974    }
975}
976
977impl From<WriteError> for SubsetError {
978    fn from(err: WriteError) -> SubsetError {
979        SubsetError::Write(err)
980    }
981}
982
983impl From<CFFError> for SubsetError {
984    fn from(err: CFFError) -> SubsetError {
985        SubsetError::CFF(err)
986    }
987}
988
989impl 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
998impl fmt::Display for SubsetError {
999    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1000        match self {
1001            SubsetError::Parse(err) => write!(f, "subset: parse error: {}", err),
1002            SubsetError::Write(err) => write!(f, "subset: write error: {}", err),
1003            SubsetError::CFF(err) => write!(f, "subset: CFF error: {}", err),
1004            SubsetError::NotDef => write!(f, "subset: first glyph is not .notdef"),
1005            SubsetError::TooManyGlyphs => write!(f, "subset: too many glyphs"),
1006            SubsetError::InvalidFontCount => write!(f, "subset: invalid font count in CFF font"),
1007        }
1008    }
1009}
1010
1011impl std::error::Error for SubsetError {}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use crate::font_data::FontData;
1017    use crate::tables::cmap::{Cmap, CmapSubtable};
1018    use crate::tables::glyf::{
1019        BoundingBox, CompositeGlyph, CompositeGlyphArgument, CompositeGlyphComponent,
1020        CompositeGlyphFlag, GlyfRecord, Glyph, Point, SimpleGlyph, SimpleGlyphFlag,
1021    };
1022    use crate::tables::{LongHorMetric, OpenTypeData, OpenTypeFont};
1023    use crate::tag::DisplayTag;
1024    use crate::tests::read_fixture;
1025    use crate::Font;
1026
1027    use std::collections::HashSet;
1028
1029    macro_rules! read_table {
1030        ($file:ident, $scope:expr, $tag:path, $t:ty) => {
1031            $file
1032                .read_table(&$scope, $tag)
1033                .expect("error reading table")
1034                .expect("no table found")
1035                .read::<$t>()
1036                .expect("unable to parse")
1037        };
1038        ($file:ident, $scope:expr, $tag:path, $t:ty, $args:expr) => {
1039            $file
1040                .read_table(&$scope, $tag)
1041                .expect("error reading table")
1042                .expect("no table found")
1043                .read_dep::<$t>($args)
1044                .expect("unable to parse")
1045        };
1046    }
1047
1048    #[test]
1049    fn create_glyf_and_hmtx() {
1050        let buffer = read_fixture("tests/fonts/opentype/SFNT-TTF-Composite.ttf");
1051        let fontfile = ReadScope::new(&buffer)
1052            .read::<OpenTypeFont<'_>>()
1053            .expect("error reading OpenTypeFile");
1054        let font = match fontfile.data {
1055            OpenTypeData::Single(font) => font,
1056            OpenTypeData::Collection(_) => unreachable!(),
1057        };
1058        let head = read_table!(font, fontfile.scope, tag::HEAD, HeadTable);
1059        let maxp = read_table!(font, fontfile.scope, tag::MAXP, MaxpTable);
1060        let hhea = read_table!(font, fontfile.scope, tag::HHEA, HheaTable);
1061        let loca = read_table!(
1062            font,
1063            fontfile.scope,
1064            tag::LOCA,
1065            LocaTable<'_>,
1066            (maxp.num_glyphs, head.index_to_loc_format)
1067        );
1068        let glyf = read_table!(font, fontfile.scope, tag::GLYF, GlyfTable<'_>, &loca);
1069        let hmtx = read_table!(
1070            font,
1071            fontfile.scope,
1072            tag::HMTX,
1073            HmtxTable<'_>,
1074            (
1075                usize::from(maxp.num_glyphs),
1076                usize::from(hhea.num_h_metrics),
1077            )
1078        );
1079
1080        // 0 - .notdef
1081        // 2 - composite
1082        // 4 - simple
1083        let glyph_ids = [0, 2, 4];
1084        let subset_glyphs = glyf.subset(&glyph_ids).unwrap();
1085        let expected_glyf = GlyfTable::new(vec![
1086            GlyfRecord::empty(),
1087            GlyfRecord::Parsed(Glyph::Composite(CompositeGlyph {
1088                bounding_box: BoundingBox {
1089                    x_min: 205,
1090                    x_max: 4514,
1091                    y_min: 0,
1092                    y_max: 1434,
1093                },
1094                glyphs: vec![
1095                    CompositeGlyphComponent {
1096                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
1097                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
1098                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
1099                            | CompositeGlyphFlag::MORE_COMPONENTS
1100                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
1101                        glyph_index: 3,
1102                        argument1: CompositeGlyphArgument::I16(3453),
1103                        argument2: CompositeGlyphArgument::I16(0),
1104                        scale: None,
1105                    },
1106                    CompositeGlyphComponent {
1107                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
1108                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
1109                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
1110                            | CompositeGlyphFlag::MORE_COMPONENTS
1111                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
1112                        glyph_index: 4,
1113                        argument1: CompositeGlyphArgument::I16(2773),
1114                        argument2: CompositeGlyphArgument::I16(0),
1115                        scale: None,
1116                    },
1117                    CompositeGlyphComponent {
1118                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
1119                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
1120                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
1121                            | CompositeGlyphFlag::MORE_COMPONENTS
1122                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
1123                        glyph_index: 5,
1124                        argument1: CompositeGlyphArgument::I16(1182),
1125                        argument2: CompositeGlyphArgument::I16(0),
1126                        scale: None,
1127                    },
1128                    CompositeGlyphComponent {
1129                        flags: CompositeGlyphFlag::ARG_1_AND_2_ARE_WORDS
1130                            | CompositeGlyphFlag::ARGS_ARE_XY_VALUES
1131                            | CompositeGlyphFlag::ROUND_XY_TO_GRID
1132                            | CompositeGlyphFlag::UNSCALED_COMPONENT_OFFSET,
1133                        glyph_index: 2,
1134                        argument1: CompositeGlyphArgument::I16(205),
1135                        argument2: CompositeGlyphArgument::I16(0),
1136                        scale: None,
1137                    },
1138                ],
1139                instructions: Box::default(),
1140                phantom_points: None,
1141            })),
1142            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
1143                bounding_box: BoundingBox {
1144                    x_min: 0,
1145                    x_max: 1073,
1146                    y_min: 0,
1147                    y_max: 1434,
1148                },
1149                end_pts_of_contours: vec![9],
1150                instructions: Box::default(),
1151                coordinates: vec![
1152                    (
1153                        SimpleGlyphFlag::ON_CURVE_POINT
1154                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1155                        Point(0, 1434),
1156                    ),
1157                    (
1158                        SimpleGlyphFlag::ON_CURVE_POINT
1159                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1160                        Point(1073, 1434),
1161                    ),
1162                    (
1163                        SimpleGlyphFlag::ON_CURVE_POINT
1164                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1165                        Point(1073, 1098),
1166                    ),
1167                    (
1168                        SimpleGlyphFlag::ON_CURVE_POINT
1169                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1170                        Point(485, 1098),
1171                    ),
1172                    (
1173                        SimpleGlyphFlag::ON_CURVE_POINT
1174                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1175                        Point(485, 831),
1176                    ),
1177                    (
1178                        SimpleGlyphFlag::ON_CURVE_POINT
1179                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1180                        Point(987, 831),
1181                    ),
1182                    (
1183                        SimpleGlyphFlag::ON_CURVE_POINT
1184                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1185                        Point(987, 500),
1186                    ),
1187                    (
1188                        SimpleGlyphFlag::ON_CURVE_POINT
1189                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1190                        Point(485, 500),
1191                    ),
1192                    (
1193                        SimpleGlyphFlag::ON_CURVE_POINT
1194                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1195                        Point(485, 0),
1196                    ),
1197                    (
1198                        SimpleGlyphFlag::ON_CURVE_POINT
1199                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1200                        Point::zero(),
1201                    ),
1202                ],
1203                phantom_points: None,
1204            })),
1205            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
1206                bounding_box: BoundingBox {
1207                    x_min: 0,
1208                    x_max: 1061,
1209                    y_min: 0,
1210                    y_max: 1434,
1211                },
1212                end_pts_of_contours: vec![5],
1213                instructions: Box::default(),
1214                coordinates: vec![
1215                    (
1216                        SimpleGlyphFlag::ON_CURVE_POINT
1217                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1218                        Point(0, 1434),
1219                    ),
1220                    (
1221                        SimpleGlyphFlag::ON_CURVE_POINT
1222                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1223                        Point(485, 1434),
1224                    ),
1225                    (
1226                        SimpleGlyphFlag::ON_CURVE_POINT
1227                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1228                        Point(485, 369),
1229                    ),
1230                    (
1231                        SimpleGlyphFlag::ON_CURVE_POINT
1232                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1233                        Point(1061, 369),
1234                    ),
1235                    (
1236                        SimpleGlyphFlag::ON_CURVE_POINT
1237                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1238                        Point(1061, 0),
1239                    ),
1240                    (
1241                        SimpleGlyphFlag::ON_CURVE_POINT
1242                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1243                        Point::zero(),
1244                    ),
1245                ],
1246                phantom_points: None,
1247            })),
1248            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
1249                bounding_box: BoundingBox {
1250                    x_min: 0,
1251                    x_max: 485,
1252                    y_min: 0,
1253                    y_max: 1434,
1254                },
1255                end_pts_of_contours: vec![3],
1256                instructions: Box::default(),
1257                coordinates: vec![
1258                    (
1259                        SimpleGlyphFlag::ON_CURVE_POINT
1260                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1261                        Point(0, 1434),
1262                    ),
1263                    (
1264                        SimpleGlyphFlag::ON_CURVE_POINT
1265                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1266                        Point(485, 1434),
1267                    ),
1268                    (
1269                        SimpleGlyphFlag::ON_CURVE_POINT
1270                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1271                        Point(485, 0),
1272                    ),
1273                    (
1274                        SimpleGlyphFlag::ON_CURVE_POINT
1275                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1276                        Point::zero(),
1277                    ),
1278                ],
1279                phantom_points: None,
1280            })),
1281            GlyfRecord::Parsed(Glyph::Simple(SimpleGlyph {
1282                bounding_box: BoundingBox {
1283                    x_min: 0,
1284                    x_max: 1478,
1285                    y_min: 0,
1286                    y_max: 1434,
1287                },
1288                end_pts_of_contours: vec![7, 10],
1289                instructions: Box::default(),
1290                coordinates: vec![
1291                    (
1292                        SimpleGlyphFlag::ON_CURVE_POINT
1293                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR
1294                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1295                        Point::zero(),
1296                    ),
1297                    (SimpleGlyphFlag::ON_CURVE_POINT.into(), Point(436, 1434)),
1298                    (
1299                        SimpleGlyphFlag::ON_CURVE_POINT
1300                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1301                        Point(1042, 1434),
1302                    ),
1303                    (SimpleGlyphFlag::ON_CURVE_POINT.into(), Point(1478, 0)),
1304                    (
1305                        SimpleGlyphFlag::ON_CURVE_POINT
1306                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1307                        Point(975, 0),
1308                    ),
1309                    (
1310                        SimpleGlyphFlag::ON_CURVE_POINT
1311                            | SimpleGlyphFlag::X_SHORT_VECTOR
1312                            | SimpleGlyphFlag::Y_SHORT_VECTOR
1313                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1314                        Point(909, 244),
1315                    ),
1316                    (
1317                        SimpleGlyphFlag::ON_CURVE_POINT
1318                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1319                        Point(493, 244),
1320                    ),
1321                    (
1322                        SimpleGlyphFlag::ON_CURVE_POINT
1323                            | SimpleGlyphFlag::X_SHORT_VECTOR
1324                            | SimpleGlyphFlag::Y_SHORT_VECTOR,
1325                        Point(430, 0),
1326                    ),
1327                    (
1328                        SimpleGlyphFlag::ON_CURVE_POINT
1329                            | SimpleGlyphFlag::X_SHORT_VECTOR
1330                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR,
1331                        Point(579, 565),
1332                    ),
1333                    (
1334                        SimpleGlyphFlag::ON_CURVE_POINT
1335                            | SimpleGlyphFlag::X_SHORT_VECTOR
1336                            | SimpleGlyphFlag::X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR
1337                            | SimpleGlyphFlag::Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR,
1338                        Point(825, 565),
1339                    ),
1340                    (
1341                        SimpleGlyphFlag::ON_CURVE_POINT | SimpleGlyphFlag::X_SHORT_VECTOR,
1342                        Point(702, 1032),
1343                    ),
1344                ],
1345                phantom_points: None,
1346            })),
1347        ])
1348        .unwrap();
1349
1350        let num_h_metrics = usize::from(hhea.num_h_metrics);
1351        let hmtx = create_hmtx_table(&hmtx, num_h_metrics, &subset_glyphs).unwrap();
1352
1353        let mut glyf: GlyfTable<'_> = subset_glyphs.into();
1354        glyf.records_mut()
1355            .iter_mut()
1356            .for_each(|rec| rec.parse().unwrap());
1357        assert_eq!(glyf, expected_glyf);
1358
1359        let expected = vec![
1360            LongHorMetric {
1361                advance_width: 1536,
1362                lsb: 0,
1363            },
1364            LongHorMetric {
1365                advance_width: 4719,
1366                lsb: 205,
1367            },
1368            LongHorMetric {
1369                advance_width: 0,
1370                lsb: 0,
1371            },
1372            LongHorMetric {
1373                advance_width: 0,
1374                lsb: 0,
1375            },
1376            LongHorMetric {
1377                advance_width: 0,
1378                lsb: 0,
1379            },
1380            LongHorMetric {
1381                advance_width: 0,
1382                lsb: 0,
1383            },
1384        ];
1385
1386        assert_eq!(hmtx.h_metrics.iter().collect::<Vec<_>>(), expected);
1387        assert_eq!(hmtx.left_side_bearings.iter().collect::<Vec<_>>(), vec![]);
1388    }
1389
1390    #[test]
1391    fn font_builder() {
1392        // Test that reading a font in, adding all its tables and writing it out equals the
1393        // original font
1394        let buffer = read_fixture("tests/fonts/opentype/test-font.ttf");
1395        let fontfile = ReadScope::new(&buffer)
1396            .read::<OpenTypeFont<'_>>()
1397            .expect("error reading OpenTypeFile");
1398        let font = match fontfile.data {
1399            OpenTypeData::Single(font) => font,
1400            OpenTypeData::Collection(_) => unreachable!(),
1401        };
1402        let head = read_table!(font, fontfile.scope, tag::HEAD, HeadTable);
1403        let maxp = read_table!(font, fontfile.scope, tag::MAXP, MaxpTable);
1404        let hhea = read_table!(font, fontfile.scope, tag::HHEA, HheaTable);
1405        let loca = read_table!(
1406            font,
1407            fontfile.scope,
1408            tag::LOCA,
1409            LocaTable<'_>,
1410            (maxp.num_glyphs, head.index_to_loc_format)
1411        );
1412        let glyf = read_table!(font, fontfile.scope, tag::GLYF, GlyfTable<'_>, &loca);
1413        let hmtx = read_table!(
1414            font,
1415            fontfile.scope,
1416            tag::HMTX,
1417            HmtxTable<'_>,
1418            (
1419                usize::from(maxp.num_glyphs),
1420                usize::from(hhea.num_h_metrics),
1421            )
1422        );
1423
1424        let mut builder = FontBuilder::new(tables::TTF_MAGIC, TableFilter::All);
1425        builder
1426            .add_table::<_, HheaTable>(tag::HHEA, &hhea, ())
1427            .unwrap();
1428        builder
1429            .add_table::<_, HmtxTable<'_>>(tag::HMTX, &hmtx, ())
1430            .unwrap();
1431        builder
1432            .add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())
1433            .unwrap();
1434
1435        let tables_added = [
1436            tag::HEAD,
1437            tag::GLYF,
1438            tag::HHEA,
1439            tag::HMTX,
1440            tag::MAXP,
1441            tag::LOCA,
1442        ]
1443        .iter()
1444        .collect::<HashSet<&u32>>();
1445        for record in font.table_records.iter() {
1446            if tables_added.contains(&record.table_tag) {
1447                continue;
1448            }
1449
1450            let table = font
1451                .read_table(&fontfile.scope, record.table_tag)
1452                .unwrap()
1453                .unwrap();
1454            builder
1455                .add_table::<_, ReadScope<'_>>(record.table_tag, table, ())
1456                .unwrap();
1457        }
1458
1459        let mut builder = builder.add_head_table(&head).unwrap();
1460        builder.add_glyf_table(glyf).unwrap();
1461        let data = builder.data().unwrap();
1462
1463        let new_fontfile = ReadScope::new(&data)
1464            .read::<OpenTypeFont<'_>>()
1465            .expect("error reading new OpenTypeFile");
1466        let new_font = match new_fontfile.data {
1467            OpenTypeData::Single(font) => font,
1468            OpenTypeData::Collection(_) => unreachable!(),
1469        };
1470
1471        assert_eq!(new_font.table_records.len(), font.table_records.len());
1472        for record in font.table_records.iter() {
1473            match record.table_tag {
1474                tag::GLYF | tag::LOCA => {
1475                    // TODO: check content of glyf and loca
1476                    // glyf differs because we don't do anything fancy with points at the moment
1477                    // and always write them out as i16 values.
1478                    // loca differs because glyf differs
1479                    continue;
1480                }
1481                tag => {
1482                    let new_record = new_font.find_table_record(record.table_tag).unwrap();
1483                    let tag = DisplayTag(tag);
1484                    assert_eq!((tag, new_record.checksum), (tag, record.checksum));
1485                }
1486            }
1487        }
1488    }
1489
1490    #[test]
1491    fn invalid_glyph_id() {
1492        // Test to ensure that invalid glyph ids don't panic when subsetting
1493        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
1494        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1495        let mut glyph_ids = [0, 9999];
1496
1497        match subset(
1498            &opentype_file.table_provider(0).unwrap(),
1499            &mut glyph_ids,
1500            &SubsetProfile::Pdf,
1501            CmapTarget::Unrestricted,
1502        ) {
1503            Err(SubsetError::Parse(ParseError::BadIndex)) => {}
1504            err => panic!(
1505                "expected SubsetError::Parse(ParseError::BadIndex) got {:?}",
1506                err
1507            ),
1508        }
1509    }
1510
1511    #[test]
1512    fn empty_mappings_to_keep() {
1513        // Test to ensure that an empty mappings to keep doesn't panic when subsetting
1514        let buffer = read_fixture("tests/fonts/opentype/SourceCodePro-Regular.otf");
1515        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1516        // glyph 118 is not Unicode, so does not end up in the mappings to keep
1517        let mut glyph_ids = [0, 118];
1518        let subset_font_data = subset(
1519            &opentype_file.table_provider(0).unwrap(),
1520            &mut glyph_ids,
1521            &SubsetProfile::Pdf,
1522            CmapTarget::Unrestricted,
1523        )
1524        .unwrap();
1525
1526        let opentype_file = ReadScope::new(&subset_font_data)
1527            .read::<OpenTypeFont<'_>>()
1528            .unwrap();
1529        let font = Font::new(opentype_file.table_provider(0).unwrap()).unwrap();
1530        let cmap = ReadScope::new(font.cmap_subtable_data())
1531            .read::<CmapSubtable<'_>>()
1532            .unwrap();
1533
1534        // If mappings_to_keep is empty a mac roman cmap sub-table is created, which doesn't
1535        // care that it's empty.
1536        if let CmapSubtable::Format0 { glyph_id_array, .. } = cmap {
1537            assert!(glyph_id_array.iter().all(|x| x == 0));
1538        } else {
1539            panic!("expected cmap sub-table format 0");
1540        }
1541    }
1542
1543    #[test]
1544    fn ttf_mappings_to_keep_is_none() {
1545        // Test that when subsetting a TTF font with mappings_to_keep set to None the cmap table is
1546        // omitted from the subset font.
1547        let buffer = read_fixture("tests/fonts/opentype/test-font.ttf");
1548        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1549        let mut glyph_ids = [0, 2];
1550        let subset_font_data = subset_ttf(
1551            &opentype_file.table_provider(0).unwrap(),
1552            &mut glyph_ids,
1553            CmapStrategy::Omit,
1554            &SubsetProfile::Pdf,
1555        )
1556        .unwrap();
1557
1558        let opentype_file = ReadScope::new(&subset_font_data)
1559            .read::<OpenTypeFont<'_>>()
1560            .unwrap();
1561        let table_provider = opentype_file.table_provider(0).unwrap();
1562        assert!(!table_provider.has_table(tag::CMAP));
1563    }
1564
1565    // This test ensures we can call whole_font on a font without a `glyf` table (E.g. CFF).
1566    #[test]
1567    fn test_whole_font() {
1568        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
1569        let scope = ReadScope::new(&buffer);
1570        let font_file = scope
1571            .read::<FontData<'_>>()
1572            .expect("unable to read FontFile");
1573        let provider = font_file
1574            .table_provider(0)
1575            .expect("unable to get FontTableProvider");
1576        let tags = [
1577            tag::CFF,
1578            tag::GDEF,
1579            tag::GPOS,
1580            tag::GSUB,
1581            tag::OS_2,
1582            tag::CMAP,
1583            tag::HEAD,
1584            tag::HHEA,
1585            tag::HMTX,
1586            tag::MAXP,
1587            tag::NAME,
1588            tag::POST,
1589        ];
1590        assert!(whole_font(&provider, &tags).is_ok());
1591    }
1592
1593    #[test]
1594    fn test_max_power_of_2() {
1595        assert_eq!(max_power_of_2(0), 0);
1596        assert_eq!(max_power_of_2(1), 0);
1597        assert_eq!(max_power_of_2(2), 1);
1598        assert_eq!(max_power_of_2(4), 2);
1599        assert_eq!(max_power_of_2(8), 3);
1600        assert_eq!(max_power_of_2(16), 4);
1601        assert_eq!(max_power_of_2(49), 5);
1602        assert_eq!(max_power_of_2(std::u16::MAX), 15);
1603    }
1604
1605    #[test]
1606    fn subset_cff2_type1() {
1607        let buffer = read_fixture("tests/fonts/opentype/cff2/SourceSans3.abc.otf");
1608        let otf = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1609        let provider = otf.table_provider(0).expect("error reading font file");
1610
1611        // Subset the CFF2, producing CFF. Since there is only two glyphs in the subset font it
1612        // will produce a Type 1 CFF font.
1613        let new_font = subset(
1614            &provider,
1615            &[0, 1],
1616            &SubsetProfile::Pdf,
1617            CmapTarget::Unrestricted,
1618        )
1619        .unwrap();
1620
1621        // Read it back
1622        let subset_otf = ReadScope::new(&new_font)
1623            .read::<OpenTypeFont<'_>>()
1624            .unwrap();
1625        let provider = subset_otf
1626            .table_provider(0)
1627            .expect("error reading new font");
1628        let cff_data = provider
1629            .read_table_data(tag::CFF)
1630            .expect("unable to read CFF data");
1631        let res = ReadScope::new(&cff_data).read::<CFF<'_>>();
1632        assert!(res.is_ok());
1633        let cff = res.unwrap();
1634        let font = &cff.fonts[0];
1635        assert!(!font.is_cid_keyed());
1636    }
1637
1638    #[test]
1639    fn subset_cff2_cid() {
1640        let buffer = read_fixture("tests/fonts/opentype/cff2/SourceSans3-Instance.256.otf");
1641        let otf = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1642        let provider = otf.table_provider(0).expect("error reading font file");
1643
1644        // Subset the CFF2, producing CFF. Since there is more than 255 glyphs in the subset font it
1645        // will produce a CID-keyed CFF font.
1646        let glyph_ids = (0..=256).collect::<Vec<_>>();
1647        let new_font = subset(
1648            &provider,
1649            &glyph_ids,
1650            &SubsetProfile::Pdf,
1651            CmapTarget::Unrestricted,
1652        )
1653        .unwrap();
1654
1655        // Read it back
1656        let subset_otf = ReadScope::new(&new_font)
1657            .read::<OpenTypeFont<'_>>()
1658            .unwrap();
1659        let provider = subset_otf
1660            .table_provider(0)
1661            .expect("error reading new font");
1662        let cff_data = provider
1663            .read_table_data(tag::CFF)
1664            .expect("unable to read CFF data");
1665        let res = ReadScope::new(&cff_data).read::<CFF<'_>>();
1666        assert!(res.is_ok());
1667        let cff = res.unwrap();
1668        assert_eq!(cff.fonts.len(), 1);
1669        let font = &cff.fonts[0];
1670        assert!(font.is_cid_keyed());
1671    }
1672
1673    #[test]
1674    fn test_subset_with_os2_and_unicode_cmap() {
1675        // Test string to use for the font subset
1676        let test_string = "hello world";
1677
1678        // Load the font
1679        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
1680        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1681        let provider = opentype_file.table_provider(0).unwrap();
1682
1683        // Create a font instance to access cmap
1684        let font = Font::new(provider).unwrap();
1685
1686        // Get the cmap subtable for unicode mapping
1687        let cmap_data = font.cmap_subtable_data();
1688        let cmap_subtable = ReadScope::new(cmap_data)
1689            .read::<CmapSubtable<'_>>()
1690            .unwrap();
1691
1692        // Map characters to glyph IDs
1693        let mut glyph_ids = vec![0]; // Always include glyph 0 (.notdef)
1694
1695        for c in test_string.chars() {
1696            if let Ok(Some(glyph_id)) = cmap_subtable.map_glyph(c as u32) {
1697                glyph_ids.push(glyph_id);
1698            }
1699        }
1700
1701        // Sort and deduplicate glyph IDs
1702        glyph_ids.sort();
1703        glyph_ids.dedup();
1704
1705        // Subset the font
1706        let subset_buffer = subset(
1707            &font.font_table_provider,
1708            &glyph_ids,
1709            &SubsetProfile::Minimal,
1710            CmapTarget::Unicode,
1711        )
1712        .unwrap();
1713        drop(font); // so we don't accidentally use it below
1714
1715        // Validate that the OS/2 table is present in the subsetted font
1716        let subset_otf = ReadScope::new(&subset_buffer)
1717            .read::<OpenTypeFont<'_>>()
1718            .unwrap();
1719        let subset_provider = subset_otf.table_provider(0).unwrap();
1720
1721        // Check that OS/2 table exists
1722        assert!(
1723            subset_provider.has_table(tag::OS_2),
1724            "Subset font is missing the OS/2 table."
1725        );
1726
1727        // Read back the cmap and check that it's a unicode cmap
1728        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
1729        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
1730        assert!(
1731            cmap.find_subtable(PlatformId::UNICODE, EncodingId::UNICODE_BMP)
1732                .is_some(),
1733            "subset font does not have expected Unicode cmap"
1734        );
1735    }
1736
1737    #[test]
1738    fn test_subset_with_macroman_cmap() {
1739        // Test string to use for the font subset
1740        let test_string = "hello world";
1741
1742        // Load the font
1743        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
1744        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1745        let provider = opentype_file.table_provider(0).unwrap();
1746
1747        // Create a font instance to access cmap
1748        let font = Font::new(provider).unwrap();
1749
1750        // Get the cmap subtable for unicode mapping
1751        let cmap_data = font.cmap_subtable_data();
1752        let cmap_subtable = ReadScope::new(cmap_data)
1753            .read::<CmapSubtable<'_>>()
1754            .unwrap();
1755
1756        // Map characters to glyph IDs
1757        let mut glyph_ids = vec![0]; // Always include glyph 0 (.notdef)
1758
1759        for c in test_string.chars() {
1760            if let Ok(Some(glyph_id)) = cmap_subtable.map_glyph(c as u32) {
1761                glyph_ids.push(glyph_id);
1762            }
1763        }
1764
1765        // Sort and deduplicate glyph IDs
1766        glyph_ids.sort();
1767        glyph_ids.dedup();
1768
1769        // Subset the font
1770        let subset_buffer = subset(
1771            &font.font_table_provider,
1772            &glyph_ids,
1773            &SubsetProfile::Minimal,
1774            CmapTarget::Unrestricted,
1775        )
1776        .unwrap();
1777        drop(font); // so we don't accidentally use it below
1778
1779        let subset_otf = ReadScope::new(&subset_buffer)
1780            .read::<OpenTypeFont<'_>>()
1781            .unwrap();
1782        let subset_provider = subset_otf.table_provider(0).unwrap();
1783
1784        // Read back the cmap and check that it's a Mac Roman cmap (because all the selected
1785        // glyphs are in the Mac Roman character set)
1786        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
1787        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
1788        assert!(
1789            cmap.find_subtable(PlatformId::MACINTOSH, EncodingId::MACINTOSH_APPLE_ROMAN)
1790                .is_some(),
1791            "subset font does not have expected Mac Roman cmap"
1792        );
1793    }
1794
1795    #[test]
1796    fn parse_custom_profile() {
1797        let tables = "fpGm,OS/2 os2,GSUB".to_string();
1798        let custom = SubsetProfile::parse_custom(tables)
1799            .unwrap()
1800            .get_tables(&[])
1801            .iter()
1802            .copied()
1803            .map(|table| DisplayTag(table).to_string())
1804            .collect::<Vec<_>>();
1805        let expected = vec![
1806            tag::GSUB,
1807            tag::OS_2,
1808            tag::CMAP,
1809            tag::FPGM,
1810            tag::HEAD,
1811            tag::HHEA,
1812            tag::HMTX,
1813            tag::MAXP,
1814            tag::NAME,
1815            tag::POST,
1816        ]
1817        .into_iter()
1818        .map(|table| DisplayTag(table).to_string())
1819        .collect::<Vec<_>>();
1820
1821        assert_eq!(custom, expected)
1822    }
1823
1824    #[test]
1825    fn parse_custom_profile_invalid() {
1826        assert!(SubsetProfile::parse_custom("toolong".to_string()).is_err());
1827        assert!(SubsetProfile::parse_custom("👓".to_string()).is_err());
1828    }
1829
1830    #[test]
1831    fn notdef_only_cff_produces_valid_font() {
1832        // When glyph_ids contains only .notdef (glyph id 0), subset should produce
1833        // a valid font with an empty cmap table, not panic.
1834        let buffer = read_fixture("tests/fonts/opentype/Klei.otf");
1835        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1836        let glyph_ids = [0];
1837
1838        let subset_buffer = subset(
1839            &opentype_file.table_provider(0).unwrap(),
1840            &glyph_ids,
1841            &SubsetProfile::Pdf,
1842            CmapTarget::Unicode,
1843        )
1844        .unwrap();
1845
1846        // Parse the subset font and verify the cmap table is present and valid
1847        let subset_otf = ReadScope::new(&subset_buffer)
1848            .read::<OpenTypeFont<'_>>()
1849            .unwrap();
1850        let subset_provider = subset_otf.table_provider(0).unwrap();
1851        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
1852        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
1853        assert!(
1854            cmap.find_subtable(PlatformId::UNICODE, EncodingId::UNICODE_BMP)
1855                .is_some(),
1856            "subset font should have a Unicode BMP cmap subtable"
1857        );
1858    }
1859
1860    #[test]
1861    fn notdef_only_ttf_produces_valid_font() {
1862        // Same test but with a TTF font to cover the subset_ttf path
1863        let buffer = read_fixture("tests/fonts/opentype/test-font.ttf");
1864        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
1865        let glyph_ids = [0];
1866
1867        let subset_buffer = subset(
1868            &opentype_file.table_provider(0).unwrap(),
1869            &glyph_ids,
1870            &SubsetProfile::Pdf,
1871            CmapTarget::Unicode,
1872        )
1873        .unwrap();
1874
1875        // Parse the subset font and verify the cmap table is present and valid
1876        let subset_otf = ReadScope::new(&subset_buffer)
1877            .read::<OpenTypeFont<'_>>()
1878            .unwrap();
1879        let subset_provider = subset_otf.table_provider(0).unwrap();
1880        let cmap_data = subset_provider.read_table_data(tag::CMAP).unwrap();
1881        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().unwrap();
1882        assert!(
1883            cmap.find_subtable(PlatformId::UNICODE, EncodingId::UNICODE_BMP)
1884                .is_some(),
1885            "subset font should have a Unicode BMP cmap subtable"
1886        );
1887    }
1888}