font_subset/font/
mod.rs

1//! OpenType parsing logic.
2
3use core::{fmt, mem, ops};
4
5#[cfg(feature = "woff2")]
6pub use self::woff2::Woff2Reader;
7pub(crate) use self::{
8    cmap::CmapTable,
9    fvar::FvarTable,
10    glyph::{GlyfTable, Glyph, GlyphWithMetrics},
11    head::HeadTable,
12    hhea::HheaTable,
13    hmtx::HmtxTable,
14    loca::LocaTable,
15    maxp::MaxpTable,
16    name::NameTable,
17    os2::Os2Table,
18    post::PostTable,
19    stat::StatTable,
20    types::{Cursor, OffsetFormat},
21};
22pub use self::{
23    fvar::{VariationAxis, VariationAxisTag},
24    name::FontNaming,
25    os2::{EmbeddingPermissions, FontCategory, UsagePermissions},
26    types::{Fixed, LongDateTime, TableTag},
27};
28use self::{hhea::HorizontalGlyphStats, types::BoundingBox};
29use crate::{
30    alloc::{format, BTreeSet, Box, Cow, Vec},
31    errors::{ParseError, ParseErrorKind, Warnings},
32    font::gvar::GvarTable,
33    subset::FontSubset,
34    utils::{Either, RangeConcat},
35};
36
37mod cmap;
38mod fvar;
39mod glyph;
40mod gvar;
41mod head;
42mod hhea;
43mod hmtx;
44mod loca;
45mod maxp;
46mod name;
47mod os2;
48mod post;
49mod stat;
50mod types;
51#[cfg(feature = "woff2")]
52mod woff2;
53
54/// Basic font metrics.
55#[derive(Debug, Clone)]
56#[non_exhaustive]
57pub struct FontMetrics {
58    /// Font design units per em. Usually 1,000 (for OpenType fonts) or a power of 2 (e.g., 2,048; for TrueType fonts).
59    pub units_per_em: u16,
60    /// Horizontal advance width in font design units. Not set for non-monospace fonts.
61    pub monospace_advance_width: Option<u16>,
62    /// Typographic ascent in font design units. Usually positive.
63    pub ascent: i16,
64    /// Typographic descent in font design units. Usually negative.
65    pub descent: i16,
66}
67
68/// Reader for OpenType files (`.otf` / `.ttf`). Borrows data from an external source.
69#[derive(Debug, Clone)]
70pub struct OpenTypeReader<'a> {
71    tables: Vec<(TableTag, Cursor<'a>)>,
72}
73
74impl<'a> OpenTypeReader<'a> {
75    /// Creates a reader from the specified raw bytes.
76    ///
77    /// This will parse the OpenType header and table records.
78    ///
79    /// # Errors
80    ///
81    /// Returns parsing errors if any are encountered.
82    #[allow(clippy::missing_panics_doc)] // false positive
83    #[cfg_attr(
84        feature = "tracing",
85        tracing::instrument(
86            level = "debug",
87            name = "OpenTypeReader::new",
88            err,
89            skip_all,
90            fields(bytes.len = bytes.len()),
91        )
92    )]
93    pub fn new(bytes: &'a [u8]) -> Result<Self, ParseError> {
94        let mut cursor = Cursor::new(bytes);
95        let font_bytes = bytes;
96        cursor.read_u32_checked(|sfnt_version| check_exact!(sfnt_version, Font::SFNT_VERSION))?;
97
98        let table_count = cursor.read_u16()?;
99        #[cfg(feature = "tracing")]
100        tracing::debug!(table_count, "read table count");
101
102        let expected_entry_selector = u16::try_from(table_count.ilog2()).unwrap();
103        let expected_search_range = 1 << (4 + expected_entry_selector);
104        cursor
105            .read_u16_checked(|search_range| check_exact!(search_range, expected_search_range))?;
106        cursor.read_u16_checked(|entry_selector| {
107            check_exact!(entry_selector, expected_entry_selector)
108        })?;
109        cursor.read_u16_checked(|range_shift| {
110            check_exact!(range_shift, 16 * table_count - expected_search_range)
111        })?;
112
113        let tables = (0..table_count)
114            .map(|_| Self::parse_table_record(&mut cursor, font_bytes))
115            .collect::<Result<Vec<_>, _>>()?;
116        Ok(Self { tables })
117    }
118
119    fn aligned_checksum(cursor: &Cursor<'_>) -> Result<u32, ParseError> {
120        if cursor.offset() % 4 != 0 {
121            return Err(cursor.err(ParseErrorKind::UnalignedTable));
122        }
123        Ok(Font::checksum(cursor.bytes()))
124    }
125
126    fn parse_table_record(
127        header_cursor: &mut Cursor<'_>,
128        font_bytes: &'a [u8],
129    ) -> Result<(TableTag, Cursor<'a>), ParseError> {
130        let tag = TableTag::from(header_cursor.read_u32()?);
131        let checksum = header_cursor.read_u32()?;
132        let offset = header_cursor.read_u32()? as usize;
133        let len = header_cursor.read_u32()? as usize;
134        let table_bytes = font_bytes.get(offset..(offset + len)).ok_or_else(|| {
135            header_cursor.err(ParseErrorKind::RangeOutOfBounds {
136                range: offset..(offset + len),
137                len: font_bytes.len(),
138            })
139        })?;
140        let cursor = Cursor::for_table(table_bytes, offset, tag);
141        let mut actual_checksum = Self::aligned_checksum(&cursor)?;
142        if tag == TableTag::HEAD {
143            // Zero out the checksum adjustment field.
144            let adjustment =
145                &table_bytes[Font::HEAD_CHECKSUM_OFFSET..Font::HEAD_CHECKSUM_OFFSET + 4];
146            let adjustment = u32::from_be_bytes(adjustment.try_into().unwrap());
147            actual_checksum = actual_checksum.wrapping_sub(adjustment);
148        }
149
150        if checksum != actual_checksum {
151            return Err(cursor.err(ParseErrorKind::Checksum {
152                expected: checksum,
153                actual: actual_checksum,
154            }));
155        }
156
157        #[cfg(feature = "tracing")]
158        tracing::debug!(?tag, checksum, offset, len, "read table record");
159
160        Ok((tag, cursor))
161    }
162
163    // visible for testing
164    pub(crate) fn iter(&self) -> impl ExactSizeIterator<Item = (TableTag, Cursor<'a>)> + '_ {
165        self.tables.iter().copied()
166    }
167
168    #[cfg(test)]
169    pub(crate) fn table(&self, tag: TableTag) -> Cursor<'a> {
170        let cursor = self
171            .tables
172            .iter()
173            .find_map(|(actual_tag, cursor)| (*actual_tag == tag).then_some(*cursor));
174        cursor.unwrap_or_else(|| panic!("font does not contain `{tag}` table"))
175    }
176
177    /// Iterates over all tables in the file (including ones that are not processed by [`Font`]).
178    pub fn raw_tables(&self) -> impl ExactSizeIterator<Item = (TableTag, &'a [u8])> + '_ {
179        self.tables
180            .iter()
181            .map(|(tag, cursor)| (*tag, cursor.bytes()))
182    }
183
184    /// Reads a [`Font`] from this reader. The font will borrow data from the underlying source.
185    ///
186    /// # Errors
187    ///
188    /// Returns parsing errors (e.g., on missing required tables).
189    pub fn read(&self) -> Result<Font<'a>, ParseError> {
190        Font::from_tables(self.iter())
191    }
192}
193
194/// Supported font formats.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196#[non_exhaustive]
197pub enum FileFormat {
198    /// OpenType / TrueType font (`.ttf` / `.otf` extension).
199    OpenType,
200    /// WOFF2 font.
201    #[cfg(feature = "woff2")]
202    #[cfg_attr(docsrs, doc(cfg(feature = "woff2")))]
203    Woff2,
204}
205
206impl fmt::Display for FileFormat {
207    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208        formatter.write_str(match self {
209            Self::OpenType => "OpenType",
210            #[cfg(feature = "woff2")]
211            Self::Woff2 => "WOFF2",
212        })
213    }
214}
215
216/// Generic font reader that auto-detects the file format based on its first bytes.
217#[derive(Debug, Clone)]
218#[non_exhaustive]
219pub enum FontReader<'a> {
220    /// OpenType reader.
221    OpenType(OpenTypeReader<'a>),
222    /// WOFF2 reader.
223    #[cfg(feature = "woff2")]
224    #[cfg_attr(docsrs, doc(cfg(feature = "woff2")))]
225    Woff2(Woff2Reader),
226}
227
228impl<'a> FontReader<'a> {
229    /// Creates a reader.
230    ///
231    /// # Errors
232    ///
233    /// Returns parsing errors if any are encountered. This includes the case when the file format cannot be detected.
234    #[cfg_attr(
235        feature = "tracing",
236        tracing::instrument(level = "debug", name = "FontReader::new", skip_all,)
237    )]
238    pub fn new(bytes: &'a [u8]) -> Result<Self, ParseError> {
239        let format = Cursor::new(bytes).read_u32_checked(|signature| match signature {
240            Font::SFNT_VERSION => Ok(FileFormat::OpenType),
241            #[cfg(feature = "woff2")]
242            Font::WOFF2_SIGNATURE => Ok(FileFormat::Woff2),
243            _ => {
244                #[cfg(not(feature = "woff2"))]
245                let expected = format!("OpenType ({:x}) signature", Font::SFNT_VERSION);
246                #[cfg(feature = "woff2")]
247                let expected = format!(
248                    "OpenType ({:x}) or WOFF2 ({:x}) signature",
249                    Font::SFNT_VERSION,
250                    Font::WOFF2_SIGNATURE
251                );
252
253                Err(ParseErrorKind::UnexpectedValue {
254                    name: "signature",
255                    expected,
256                    actual: signature,
257                })
258            }
259        })?;
260        #[cfg(feature = "tracing")]
261        tracing::debug!(?format, "detected font file format");
262
263        match format {
264            FileFormat::OpenType => OpenTypeReader::new(bytes).map(Self::OpenType),
265            #[cfg(feature = "woff2")]
266            FileFormat::Woff2 => Woff2Reader::new(bytes).map(Self::Woff2),
267        }
268    }
269
270    /// Returns the font format.
271    pub fn format(&self) -> FileFormat {
272        match self {
273            Self::OpenType(_) => FileFormat::OpenType,
274            #[cfg(feature = "woff2")]
275            Self::Woff2(_) => FileFormat::Woff2,
276        }
277    }
278
279    /// Iterates over all tables in the file (including ones that are not processed by [`Font`]).
280    pub fn raw_tables(&self) -> impl ExactSizeIterator<Item = (TableTag, &[u8])> + '_ {
281        #[cfg(not(feature = "woff2"))]
282        match self {
283            Self::OpenType(reader) => reader.raw_tables(),
284        }
285
286        #[cfg(feature = "woff2")]
287        match self {
288            Self::OpenType(reader) => Either::Left(reader.raw_tables()),
289            Self::Woff2(reader) => Either::Right(reader.raw_tables()),
290        }
291    }
292
293    /// Reads a [`Font`] from this reader. The font may borrow data from this reader.
294    ///
295    /// # Errors
296    ///
297    /// Returns parsing errors (e.g., on missing required tables).
298    pub fn read(&self) -> Result<Font<'_>, ParseError> {
299        match self {
300            Self::OpenType(reader) => reader.read(),
301            #[cfg(feature = "woff2")]
302            Self::Woff2(reader) => reader.read(),
303        }
304    }
305}
306
307#[derive(Debug, Clone)]
308pub(crate) struct VariableFontTables<'a> {
309    pub(crate) fvar: FvarTable<'a>,
310    pub(crate) gvar: GvarTable<'a>,
311    pub(crate) stat: StatTable<'a>,
312    pub(crate) unparsed: Vec<(TableTag, Cursor<'a>)>,
313}
314
315/// Shallowly parsed OpenType font.
316#[derive(Debug, Clone)]
317pub struct Font<'a> {
318    pub(crate) cmap: CmapTable<'a>,
319    pub(crate) head: HeadTable,
320    pub(crate) hhea: HheaTable,
321    pub(crate) hmtx: HmtxTable<'a>,
322    pub(crate) maxp: MaxpTable<'a>,
323    pub(crate) name: NameTable<'a>,
324    pub(crate) os2: Os2Table<'a>,
325    pub(crate) post: PostTable<'a>,
326    pub(crate) loca: LocaTable<'a>,
327    pub(crate) glyf: GlyfTable<'a>,
328    pub(crate) variable: Option<VariableFontTables<'a>>,
329    /// Unparsed tables in the order of their appearance in the source font.
330    pub(crate) unparsed: Vec<(TableTag, Cursor<'a>)>,
331}
332
333impl<'a> Font<'a> {
334    pub(crate) const SFNT_VERSION: u32 = 0x_0001_0000;
335    pub(crate) const SFNT_CHECKSUM: u32 = 0x_b1b0_afba;
336    pub(crate) const SFNT_HEADER_LEN: usize = 12;
337    pub(crate) const TABLE_RECORD_LEN: usize = 16;
338
339    /// Offset of the checksum in the `head` table.
340    pub(crate) const HEAD_CHECKSUM_OFFSET: usize = 8;
341
342    /// Parses `bytes` as an OpenType font. This is a shortcut for instantiating and reading
343    /// from an [`OpenTypeReader`].
344    ///
345    /// # Errors
346    ///
347    /// Returns parsing errors.
348    pub fn opentype(bytes: &'a [u8]) -> Result<Self, ParseError> {
349        OpenTypeReader::new(bytes)?.read()
350    }
351
352    #[cfg_attr(
353        feature = "tracing",
354        tracing::instrument(level = "debug", err, skip_all)
355    )]
356    fn from_tables(
357        table_records: impl Iterator<Item = (TableTag, Cursor<'a>)>,
358    ) -> Result<Self, ParseError> {
359        let (mut cmap, mut head, mut hhea, mut maxp, mut hmtx) = (None, None, None, None, None);
360        let (mut name, mut os2, mut post, mut loca, mut glyf) = (None, None, None, None, None);
361        let (mut fvar, mut gvar, mut stat) = (None, None, None);
362        let (mut unparsed, mut unparsed_var) = (Vec::new(), Vec::new());
363        for (tag, table_cursor) in table_records {
364            match tag {
365                TableTag::CMAP => {
366                    cmap = Some(CmapTable::parse(table_cursor)?);
367                }
368                TableTag::HEAD => head = Some(HeadTable::parse(table_cursor)?),
369                TableTag::HHEA => hhea = Some(HheaTable::parse(table_cursor)?),
370                TableTag::HMTX => hmtx = Some(table_cursor),
371                TableTag::MAXP => maxp = Some(MaxpTable::parse(table_cursor)?),
372                TableTag::NAME => name = Some(table_cursor),
373                TableTag::OS2 => os2 = Some(Os2Table::parse(table_cursor)?),
374                TableTag::POST => post = Some(table_cursor),
375                TableTag::LOCA => loca = Some(table_cursor),
376                TableTag::GLYF => glyf = Some(table_cursor),
377                TableTag::FVAR => fvar = Some(FvarTable::parse(table_cursor)?),
378                TableTag::GVAR => gvar = Some(table_cursor),
379                TableTag::STAT => stat = Some(table_cursor),
380                tag if tag.is_variable() => {
381                    #[cfg(feature = "tracing")]
382                    tracing::debug!(?tag, "unparsed variation table");
383                    unparsed_var.push((tag, table_cursor));
384                }
385                _ => {
386                    #[cfg(feature = "tracing")]
387                    tracing::debug!(?tag, "unparsed table");
388                    unparsed.push((tag, table_cursor));
389                }
390            }
391        }
392
393        let head = head.ok_or_else(|| ParseError::missing_table(TableTag::HEAD))?;
394        let maxp = maxp.ok_or_else(|| ParseError::missing_table(TableTag::MAXP))?;
395        let loca = loca.ok_or_else(|| ParseError::missing_table(TableTag::LOCA))?;
396        let loca = LocaTable::new(head.loca_format, maxp.glyph_count, loca)?;
397        let hhea = hhea.ok_or_else(|| ParseError::missing_table(TableTag::HHEA))?;
398        let hmtx = hmtx.ok_or_else(|| ParseError::missing_table(TableTag::HMTX))?;
399        let hmtx = HmtxTable::parse(hmtx, maxp.glyph_count, hhea.number_of_h_metrics)?;
400        let glyf = glyf.ok_or_else(|| ParseError::missing_table(TableTag::GLYF))?;
401        let post = post.ok_or_else(|| ParseError::missing_table(TableTag::POST))?;
402        let post = PostTable::new(post);
403
404        let name = name.ok_or_else(|| ParseError::missing_table(TableTag::NAME))?;
405        let additional_ids = fvar
406            .as_ref()
407            .map_or_else(Vec::new, FvarTable::axis_name_ids);
408        let name = NameTable::parse(name, &additional_ids)?;
409
410        let variable = if let Some(mut fvar) = fvar {
411            fvar.resolve_axe_names(&name);
412            let gvar = gvar
413                .map(|cursor| GvarTable::parse(cursor, maxp.glyph_count))
414                .ok_or_else(|| ParseError::missing_table(TableTag::GVAR))??;
415            let stat = stat
416                .map(StatTable::parse)
417                .ok_or_else(|| ParseError::missing_table(TableTag::STAT))??;
418
419            Some(VariableFontTables {
420                fvar,
421                gvar,
422                stat,
423                unparsed: unparsed_var,
424            })
425        } else {
426            None
427        };
428
429        Ok(Self {
430            cmap: cmap.ok_or_else(|| ParseError::missing_table(TableTag::CMAP))?,
431            head,
432            hhea,
433            hmtx,
434            maxp,
435            name,
436            os2: os2.ok_or_else(|| ParseError::missing_table(TableTag::OS2))?,
437            post,
438            loca,
439            glyf: GlyfTable::Parsed(glyf),
440            variable,
441            unparsed,
442        })
443    }
444
445    pub(crate) fn checksum(bytes: &[u8]) -> u32 {
446        bytes.chunks(4).fold(0_u32, |acc, chunk| {
447            debug_assert!(chunk.len() <= 4);
448            let mut u32_bytes = [0_u8; 4];
449            u32_bytes[..chunk.len()].copy_from_slice(chunk);
450            acc.wrapping_add(u32::from_be_bytes(u32_bytes))
451        })
452    }
453
454    /// Returns naming information for this font.
455    pub fn naming(&self) -> FontNaming<'_> {
456        self.name.parsed()
457    }
458
459    /// Gets usage permissions for this font.
460    pub fn permissions(&self) -> UsagePermissions {
461        self.os2.usage_permissions
462    }
463
464    /// Returns the "created at" timestamp for the font.
465    pub fn created_at(&self) -> LongDateTime {
466        self.head.created
467    }
468
469    /// Returns the "modified at" timestamp for the font.
470    pub fn modified_at(&self) -> LongDateTime {
471        self.head.modified
472    }
473
474    /// Returns the basic font category.
475    pub fn category(&self) -> FontCategory {
476        self.os2.category()
477    }
478
479    /// Returns basic font metrics read from `head`, `hhea` and `hmtx` tables.
480    pub fn metrics(&self) -> FontMetrics {
481        FontMetrics {
482            units_per_em: self.head.units_per_em,
483            ascent: self.hhea.ascender,
484            descent: self.hhea.descender,
485            monospace_advance_width: self.hmtx.monospace_advance(),
486        }
487    }
488
489    /// Checks whether this font is variable. This returns `true` iff [`Self::variation_axes()`]
490    /// returns `Some(_)`.
491    pub fn is_variable(&self) -> bool {
492        self.variable.is_some()
493    }
494
495    /// Returns variation axes for this font.
496    pub fn variation_axes(&self) -> Option<&[VariationAxis]> {
497        Some(self.variable.as_ref()?.fvar.axes())
498    }
499
500    pub(crate) fn map_char(&self, ch: char) -> Result<u16, ParseError> {
501        self.cmap.map_char(ch)
502    }
503
504    /// Checks whether the font contains a glyph for the specified char.
505    pub fn contains_char(&self, ch: char) -> bool {
506        self.cmap.map_char(ch).is_ok_and(|glyph_id| glyph_id != 0)
507    }
508
509    /// Iterates over char ranges covered by this font.
510    pub fn char_ranges(&self) -> impl Iterator<Item = ops::RangeInclusive<char>> + '_ {
511        RangeConcat::new(self.cmap.char_ranges())
512    }
513
514    /// Returns the total glyph count in this font.
515    pub fn glyph_count(&self) -> usize {
516        self.maxp.glyph_count.into()
517    }
518
519    pub(crate) fn glyph(&self, glyph_idx: u16) -> Result<GlyphWithMetrics<'a>, ParseError> {
520        match &self.glyf {
521            GlyfTable::Parsed(cursor) => {
522                let range = self.loca.glyph_range(glyph_idx)?;
523                let raw = cursor.read_range(range)?;
524                let inner = Glyph::new(raw)?;
525                let (advance, lsb) = self.hmtx.advance_and_lsb(glyph_idx)?;
526                Ok(GlyphWithMetrics {
527                    inner,
528                    advance,
529                    lsb,
530                })
531            }
532            GlyfTable::Subset(glyphs) => Ok(glyphs[usize::from(glyph_idx)].clone()),
533        }
534    }
535
536    fn all_glyphs(
537        &self,
538    ) -> impl Iterator<Item = Result<Cow<'_, GlyphWithMetrics<'a>>, ParseError>> + '_ {
539        match &self.glyf {
540            &GlyfTable::Parsed(cursor) => {
541                Either::Left(self.loca.all_ranges().zip(self.hmtx.iter()).map(
542                    move |(range, (advance, lsb))| {
543                        let raw = cursor.read_range(range)?;
544                        Ok(Cow::Owned(GlyphWithMetrics {
545                            inner: Glyph::new(raw)?,
546                            advance,
547                            lsb,
548                        }))
549                    },
550                ))
551            }
552            GlyfTable::Subset(glyphs) => {
553                Either::Right(glyphs.iter().map(|glyph| Ok(Cow::Borrowed(glyph))))
554            }
555        }
556    }
557
558    /// Drops variable font tables if they are present.
559    pub fn drop_variation(&mut self) {
560        self.variable = None;
561    }
562
563    /// Performs some in-depth checks regarding font consistency.
564    /// This involves parsing more font data, hence returning a `Result`.
565    ///
566    /// # Errors
567    ///
568    /// Returns parsing errors if any are encountered during additional parsing.
569    pub fn validate(&self) -> Result<Warnings, ParseError> {
570        let mut bounding_box = BoundingBox {
571            x_min: i16::MAX,
572            y_min: i16::MAX,
573            x_max: i16::MIN,
574            y_max: i16::MIN,
575        };
576        let mut horizontal_stats = HorizontalGlyphStats::default();
577
578        for glyph in self.all_glyphs() {
579            let glyph = glyph?;
580            if let Some(bbox) = glyph.inner.bounding_box() {
581                bounding_box = bounding_box.union(bbox);
582            }
583            horizontal_stats.update(&glyph);
584        }
585
586        let mut warnings = Warnings::empty();
587        // `head` table checks
588        {
589            let mut warnings = warnings.for_table(TableTag::HEAD);
590            warnings.check_match("x_min", bounding_box.x_min, self.head.bounding_box.x_min);
591            warnings.check_match("y_min", bounding_box.y_min, self.head.bounding_box.y_min);
592            warnings.check_match("x_max", bounding_box.x_max, self.head.bounding_box.x_max);
593            warnings.check_match("y_max", bounding_box.y_max, self.head.bounding_box.y_max);
594        }
595
596        // `OS/2` table checks
597        {
598            let mut warnings = warnings.for_table(TableTag::OS2);
599            let actual_range = self.cmap.char_range();
600            let computed_first_char = u16::try_from(*actual_range.start()).unwrap_or(u16::MAX);
601            warnings.check_match(
602                "first_char_index",
603                computed_first_char,
604                self.os2.first_char_index,
605            );
606            let computed_last_char = u16::try_from(*actual_range.end()).unwrap_or(u16::MAX);
607            warnings.check_match(
608                "last_char_index",
609                computed_last_char,
610                self.os2.last_char_index,
611            );
612        }
613
614        // `hhea` table checks
615        {
616            let mut warnings = warnings.for_table(TableTag::HHEA);
617            warnings.check_match(
618                "advance_width_max",
619                horizontal_stats.advance_width_max,
620                self.hhea.advance_width_max,
621            );
622            warnings.check_match(
623                "x_max_extent",
624                horizontal_stats.x_max_extent,
625                self.hhea.x_max_extent,
626            );
627            warnings.check_match(
628                "min_left_side_bearing",
629                horizontal_stats.min_left_side_bearing,
630                self.hhea.min_left_side_bearing,
631            );
632            warnings.check_match(
633                "min_right_side_bearing",
634                horizontal_stats.min_right_side_bearing,
635                self.hhea.min_right_side_bearing,
636            );
637        }
638
639        Ok(warnings)
640    }
641
642    /// Subsets this font by retaining only specified `chars`.
643    ///
644    /// # Errors
645    ///
646    /// This operation will parse more font data, so it may return parsing errors.
647    pub fn subset(&self, chars: &BTreeSet<char>) -> Result<Self, ParseError> {
648        FontSubset::subset(self, chars)
649    }
650}
651
652/// Version of [`Font`] that owns its data.
653pub struct OwnedFont {
654    font: Font<'static>,
655    /// Bytes that `font` borrows from. Declared last to be dropped last.
656    _bytes: Box<[u8]>,
657}
658
659impl fmt::Debug for OwnedFont {
660    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
661        formatter
662            .debug_tuple("OwnedFont")
663            .field(&self.font)
664            .finish_non_exhaustive()
665    }
666}
667
668impl OwnedFont {
669    /// Creates a font that owns its data.
670    ///
671    /// # Errors
672    ///
673    /// Returns parsing errors.
674    pub fn new(bytes: Box<[u8]>) -> Result<Self, ParseError> {
675        let font_reader = FontReader::new(&bytes)?;
676        let font: Font<'_> = font_reader.read()?;
677        let font: Font<'static> = unsafe {
678            // SAFETY: This extends the `font` lifetime to 'static. This is safe because:
679            //
680            // - `bytes` are never changed while borrowed by `font`; i.e., aliasing rules are not violated.
681            // - `bytes` have a stable address.
682            // - `bytes` are dropped after `font` as per `OwnedFont` declaration.
683            // - We only expose `Font` with the "correct" lifetime via `get()`, so there's no chance to clone `Font` and use it
684            //   after `bytes` is freed. The exposed font cannot outlive `OwnedFont`.
685            mem::transmute(font)
686        };
687
688        let bytes = match font_reader {
689            FontReader::OpenType(_) => bytes,
690            #[cfg(feature = "woff2")]
691            FontReader::Woff2(reader) => reader.into_table_data().into(),
692        };
693
694        Ok(Self {
695            font,
696            _bytes: bytes,
697        })
698    }
699
700    /// Gets the underlying [`Font`].
701    pub fn get(&self) -> &Font<'_> {
702        &self.font
703    }
704}
705
706#[cfg(test)]
707mod tests {
708    use std::collections::HashSet;
709
710    use allsorts::{binary::read::ReadScope, font::MatchingPresentation, font_data::FontData};
711    use test_casing::test_casing;
712
713    use super::*;
714    use crate::{testonly::TestFont, WarningKind};
715
716    #[test_casing(5, TestFont::ALL)]
717    fn reading_font(font: TestFont) {
718        let parsed_font = Font::opentype(font.bytes).unwrap();
719
720        let font_file = ReadScope::new(font.bytes).read::<FontData>().unwrap();
721        let font_provider = font_file.table_provider(0).unwrap();
722        let mut reference_font = allsorts::Font::new(font_provider).unwrap();
723
724        let char_count = parsed_font
725            .char_ranges()
726            .map(Iterator::count)
727            .sum::<usize>();
728        assert!(char_count > 100, "{char_count}");
729
730        for ch in parsed_font.char_ranges().flatten() {
731            assert!(parsed_font.contains_char(ch));
732
733            let glyph_id = parsed_font.map_char(ch).unwrap();
734            let (expected_id, _) =
735                reference_font.lookup_glyph_index(ch, MatchingPresentation::NotRequired, None);
736            assert_eq!(glyph_id, expected_id);
737        }
738
739        for range in parsed_font.char_ranges() {
740            if let Some(prev) = (char::MIN..*range.start()).next_back() {
741                assert!(!parsed_font.contains_char(prev));
742            }
743            if let Some(ch) = (*range.end()..).nth(1) {
744                assert!(!parsed_font.contains_char(ch));
745            }
746        }
747    }
748
749    #[test_casing(5, TestFont::ALL)]
750    fn parsing_permissions(font: TestFont) {
751        let font = Font::opentype(font.bytes).unwrap();
752        let permissions = font.permissions();
753        assert!(permissions.embedding.is_lenient());
754        assert!(!permissions.embed_only_bitmaps);
755        assert!(permissions.allow_subsetting);
756    }
757
758    #[test]
759    fn parsing_name_table() {
760        let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
761        let naming = font.naming();
762
763        assert_eq!(naming.family, Some("Fira Mono"));
764        assert_eq!(naming.subfamily, Some("Regular"));
765        assert_eq!(
766            naming.manufacturer,
767            Some("Carrois Corporate GbR & Edenspiekermann AG")
768        );
769        assert_eq!(
770            naming.designer,
771            Some("Carrois Corporate & Edenspiekermann AG")
772        );
773        assert_eq!(naming.designer_url, Some("http://www.carrois.com"));
774        assert_eq!(
775            naming.license,
776            Some("Licensed under the Open Font License, version 1.1 or later")
777        );
778        assert_eq!(naming.license_url, Some("http://scripts.sil.org/OFL"));
779        assert_eq!(
780            naming.copyright_notice,
781            Some(
782                "Digitized data copyright © 2012-2014, The Mozilla Foundation and Telefonica S.A."
783            )
784        );
785    }
786
787    #[test]
788    fn reading_metrics_for_fira_mono() {
789        let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
790        let metrics = font.metrics();
791        assert_eq!(metrics.units_per_em, 1_000);
792        assert_eq!(metrics.ascent, 1_050);
793        assert_eq!(metrics.descent, -350);
794        assert_eq!(metrics.monospace_advance_width, Some(600));
795    }
796
797    #[test]
798    fn reading_metrics_for_roboto_mono() {
799        let font = Font::opentype(TestFont::ROBOTO_MONO.bytes).unwrap();
800        let metrics = font.metrics();
801        assert_eq!(metrics.units_per_em, 2_048);
802        assert_eq!(metrics.ascent, 2_146);
803        assert_eq!(metrics.descent, -555);
804        assert_eq!(metrics.monospace_advance_width, Some(1_229));
805    }
806
807    #[test]
808    fn reading_metrics_for_roboto() {
809        let font = Font::opentype(TestFont::ROBOTO.bytes).unwrap();
810        let metrics = font.metrics();
811        assert_eq!(metrics.units_per_em, 2_048);
812        assert_eq!(metrics.ascent, 1_900);
813        assert_eq!(metrics.descent, -500);
814        assert_eq!(metrics.monospace_advance_width, None);
815    }
816
817    #[test]
818    fn getting_font_category() {
819        let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
820        assert_eq!(font.category(), FontCategory::Regular);
821        let font = Font::opentype(TestFont::FIRA_MONO_BOLD.bytes).unwrap();
822        assert_eq!(font.category(), FontCategory::Bold);
823        let font = Font::opentype(TestFont::ROBOTO_MONO_ITALIC.bytes).unwrap();
824        assert_eq!(font.category(), FontCategory::Italic);
825    }
826
827    #[test_casing(5, TestFont::ALL)]
828    fn validating_font(font: TestFont) {
829        let font = Font::opentype(font.bytes).unwrap();
830        font.validate().unwrap().into_result().unwrap();
831    }
832
833    #[test]
834    fn validating_font_with_mutations() {
835        let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
836
837        let mut bogus_font = font.clone();
838        bogus_font.head.bounding_box.x_min -= 1;
839        bogus_font.head.bounding_box.y_max += 1;
840
841        let warnings = bogus_font
842            .validate()
843            .unwrap()
844            .into_result()
845            .expect_err("no warnings");
846        assert_eq!(warnings.len(), 2);
847        let field_names = warnings.iter().map(|warn| {
848            assert_eq!(warn.table(), Some(TableTag::HEAD));
849            match warn.kind() {
850                WarningKind::ValueMismatch { name, .. } => *name,
851            }
852        });
853        let field_names: HashSet<_> = field_names.collect();
854        assert_eq!(field_names, HashSet::from(["x_min", "y_max"]));
855
856        let mut bogus_font = font.clone();
857        bogus_font.os2.first_char_index = 0x7f;
858        bogus_font.os2.last_char_index = 0x7fff;
859        bogus_font.hhea.min_right_side_bearing += 1;
860
861        let warnings = bogus_font
862            .validate()
863            .unwrap()
864            .into_result()
865            .expect_err("no warnings");
866        assert_eq!(warnings.len(), 3);
867        let field_names = warnings.iter().map(|warn| match warn.kind() {
868            WarningKind::ValueMismatch { name, .. } => (warn.table().unwrap(), *name),
869        });
870        let fields: HashSet<_> = field_names.collect();
871        assert_eq!(
872            fields,
873            HashSet::from([
874                (TableTag::OS2, "first_char_index"),
875                (TableTag::OS2, "last_char_index"),
876                (TableTag::HHEA, "min_right_side_bearing"),
877            ])
878        );
879
880        warnings.into_result().unwrap_err();
881    }
882}