read_fonts/
lib.rs

1//! Reading OpenType tables
2//!
3//! This crate provides memory safe zero-allocation parsing of font files.
4//! It is unopinionated, and attempts to provide raw access to the underlying
5//! font data as it is described in the [OpenType specification][spec].
6//!
7//! This crate is intended for use by other parts of a font stack, such as a
8//! shaping engine or a glyph rasterizer.
9//!
10//! In addition to raw data access, this crate may also provide reference
11//! implementations of algorithms for interpreting that data, where such an
12//! implementation is required for the data to be useful. For instance, we
13//! provide functions for [mapping codepoints to glyph identifiers][cmap-impl]
14//! using the `cmap` table, or for [decoding entries in the `name` table][NameString].
15//!
16//! For higher level/more ergonomic access to font data, you may want to look
17//! into using [`skrifa`] instead.
18//!
19//! ## Structure & codegen
20//!
21//! The root [`tables`] module contains a submodule for each supported
22//! [table][table-directory], and that submodule contains items for each table,
23//! record, flagset or enum described in the relevant portion of the spec.
24//!
25//! The majority of the code in the tables module is auto-generated. For more
26//! information on our use of codegen, see the [codegen tour].
27//!
28//! # Related projects
29//!
30//! - [`write-fonts`] is a companion crate for creating/modifying font files
31//! - [`skrifa`] provides access to glyph outlines and metadata (in the same vein
32//!   as [freetype])
33//!
34//! # Example
35//!
36//! ```no_run
37//! # let path_to_my_font_file = std::path::Path::new("");
38//! use read_fonts::{FontRef, TableProvider};
39//! let font_bytes = std::fs::read(path_to_my_font_file).unwrap();
40//! // Single fonts only. for font collections (.ttc) use FontRef::from_index
41//! let font = FontRef::new(&font_bytes).expect("failed to read font data");
42//! let head = font.head().expect("missing 'head' table");
43//! let maxp = font.maxp().expect("missing 'maxp' table");
44//!
45//! println!("font version {} containing {} glyphs", head.font_revision(), maxp.num_glyphs());
46//! ```
47//!
48//!
49//! [spec]: https://learn.microsoft.com/en-us/typography/opentype/spec/
50//! [codegen-tour]: https://github.com/googlefonts/fontations/blob/main/docs/codegen-tour.md
51//! [cmap-impl]: tables::cmap::Cmap::map_codepoint
52//! [`write-fonts`]: https://docs.rs/write-fonts/
53//! [`skrifa`]: https://docs.rs/skrifa/
54//! [freetype]: http://freetype.org
55//! [codegen tour]: https://github.com/googlefonts/fontations/blob/main/docs/codegen-tour.md
56//! [NameString]: tables::name::NameString
57//! [table-directory]: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
58
59#![cfg_attr(docsrs, feature(doc_auto_cfg))]
60#![forbid(unsafe_code)]
61#![deny(rustdoc::broken_intra_doc_links)]
62#![cfg_attr(not(feature = "std"), no_std)]
63
64#[cfg(any(feature = "std", test))]
65#[macro_use]
66extern crate std;
67
68#[cfg(all(not(feature = "std"), not(test)))]
69#[macro_use]
70extern crate core as std;
71
72pub mod array;
73#[cfg(feature = "std")]
74pub mod collections;
75mod font_data;
76mod offset;
77mod offset_array;
78mod read;
79mod table_provider;
80mod table_ref;
81pub mod tables;
82#[cfg(feature = "experimental_traverse")]
83pub mod traversal;
84
85#[cfg(any(test, feature = "codegen_test"))]
86pub mod codegen_test;
87
88pub use font_data::FontData;
89pub use offset::{Offset, ResolveNullableOffset, ResolveOffset};
90pub use offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets};
91pub use read::{ComputeSize, FontRead, FontReadWithArgs, ReadArgs, ReadError, VarSize};
92pub use table_provider::{TableProvider, TopLevelTable};
93pub use table_ref::{MinByteRange, TableRef};
94
95/// Public re-export of the font-types crate.
96pub extern crate font_types as types;
97
98/// All the types that may be referenced in auto-generated code.
99#[doc(hidden)]
100pub(crate) mod codegen_prelude {
101    pub use crate::array::{ComputedArray, VarLenArray};
102    pub use crate::font_data::{Cursor, FontData};
103    pub use crate::offset::{Offset, ResolveNullableOffset, ResolveOffset};
104    pub use crate::offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets};
105    //pub(crate) use crate::read::sealed;
106    pub use crate::read::{
107        ComputeSize, FontRead, FontReadWithArgs, Format, ReadArgs, ReadError, VarSize,
108    };
109    pub use crate::table_provider::TopLevelTable;
110    pub use crate::table_ref::{MinByteRange, TableRef};
111    pub use std::ops::Range;
112
113    pub use types::*;
114
115    #[cfg(feature = "experimental_traverse")]
116    pub use crate::traversal::{self, Field, FieldType, RecordResolver, SomeRecord, SomeTable};
117
118    // used in generated traversal code to get type names of offset fields, which
119    // may include generics
120    #[cfg(feature = "experimental_traverse")]
121    pub(crate) fn better_type_name<T>() -> &'static str {
122        let raw_name = std::any::type_name::<T>();
123        let last = raw_name.rsplit("::").next().unwrap_or(raw_name);
124        // this happens if we end up getting a type name like TableRef<'a, module::SomeMarker>
125        last.trim_end_matches("Marker>")
126    }
127
128    /// named transforms used in 'count', e.g
129    pub(crate) mod transforms {
130        pub fn subtract<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
131            lhs.try_into()
132                .unwrap_or_default()
133                .saturating_sub(rhs.try_into().unwrap_or_default())
134        }
135
136        pub fn add<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
137            lhs.try_into()
138                .unwrap_or_default()
139                .saturating_add(rhs.try_into().unwrap_or_default())
140        }
141
142        #[allow(dead_code)]
143        pub fn bitmap_len<T: TryInto<usize>>(count: T) -> usize {
144            count.try_into().unwrap_or_default().div_ceil(8)
145        }
146
147        #[cfg(feature = "ift")]
148        pub fn max_value_bitmap_len<T: TryInto<usize>>(count: T) -> usize {
149            let count: usize = count.try_into().unwrap_or_default() + 1usize;
150            count.div_ceil(8)
151        }
152
153        pub fn add_multiply<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
154            a: T,
155            b: U,
156            c: V,
157        ) -> usize {
158            a.try_into()
159                .unwrap_or_default()
160                .saturating_add(b.try_into().unwrap_or_default())
161                .saturating_mul(c.try_into().unwrap_or_default())
162        }
163
164        #[cfg(feature = "ift")]
165        pub fn multiply_add<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
166            a: T,
167            b: U,
168            c: V,
169        ) -> usize {
170            a.try_into()
171                .unwrap_or_default()
172                .saturating_mul(b.try_into().unwrap_or_default())
173                .saturating_add(c.try_into().unwrap_or_default())
174        }
175
176        pub fn half<T: TryInto<usize>>(val: T) -> usize {
177            val.try_into().unwrap_or_default() / 2
178        }
179
180        pub fn subtract_add_two<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
181            lhs.try_into()
182                .unwrap_or_default()
183                .saturating_sub(rhs.try_into().unwrap_or_default())
184                .saturating_add(2)
185        }
186    }
187}
188
189include!("../generated/font.rs");
190
191#[derive(Clone)]
192/// Reference to the content of a font or font collection file.
193pub enum FileRef<'a> {
194    /// A single font.
195    Font(FontRef<'a>),
196    /// A collection of fonts.
197    Collection(CollectionRef<'a>),
198}
199
200impl<'a> FileRef<'a> {
201    /// Creates a new reference to a file representing a font or font collection.
202    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
203        Ok(if let Ok(collection) = CollectionRef::new(data) {
204            Self::Collection(collection)
205        } else {
206            Self::Font(FontRef::new(data)?)
207        })
208    }
209
210    /// Returns an iterator over the fonts contained in the file.
211    pub fn fonts(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
212        let (iter_one, iter_two) = match self {
213            Self::Font(font) => (Some(Ok(font.clone())), None),
214            Self::Collection(collection) => (None, Some(collection.iter())),
215        };
216        iter_two.into_iter().flatten().chain(iter_one)
217    }
218}
219
220/// Reference to the content of a font collection file.
221#[derive(Clone)]
222pub struct CollectionRef<'a> {
223    data: FontData<'a>,
224    header: TTCHeader<'a>,
225}
226
227impl<'a> CollectionRef<'a> {
228    /// Creates a new reference to a font collection.
229    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
230        let data = FontData::new(data);
231        let header = TTCHeader::read(data)?;
232        if header.ttc_tag() != TTC_HEADER_TAG {
233            Err(ReadError::InvalidTtc(header.ttc_tag()))
234        } else {
235            Ok(Self { data, header })
236        }
237    }
238
239    /// Returns the number of fonts in the collection.
240    pub fn len(&self) -> u32 {
241        self.header.num_fonts()
242    }
243
244    /// Returns true if the collection is empty.
245    pub fn is_empty(&self) -> bool {
246        self.len() == 0
247    }
248
249    /// Returns the font in the collection at the specified index.
250    pub fn get(&self, index: u32) -> Result<FontRef<'a>, ReadError> {
251        let offset = self
252            .header
253            .table_directory_offsets()
254            .get(index as usize)
255            .ok_or(ReadError::InvalidCollectionIndex(index))?
256            .get() as usize;
257        let table_dir_data = self.data.slice(offset..).ok_or(ReadError::OutOfBounds)?;
258        FontRef::with_table_directory(
259            self.data,
260            TableDirectory::read(table_dir_data)?,
261            Some(index),
262        )
263    }
264
265    /// Returns an iterator over the fonts in the collection.
266    pub fn iter(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
267        let copy = self.clone();
268        (0..self.len()).map(move |ix| copy.get(ix))
269    }
270}
271
272impl TableDirectory<'_> {
273    fn is_sorted(&self) -> bool {
274        let mut last_tag = Tag::new(&[0u8; 4]);
275
276        for tag in self.table_records().iter().map(|rec| rec.tag()) {
277            if tag <= last_tag {
278                return false;
279            }
280
281            last_tag = tag;
282        }
283
284        true
285    }
286}
287
288/// Reference to an in-memory font.
289///
290/// This is a simple implementation of the [`TableProvider`] trait backed
291/// by a borrowed slice containing font data.
292#[derive(Clone)]
293pub struct FontRef<'a> {
294    data: FontData<'a>,
295    pub table_directory: TableDirectory<'a>,
296    /// The index of this font in a TrueType collection
297    ttc_index: u32,
298    /// Whether this font is a member of a TrueType collection.
299    ///
300    /// We use a bool rather than an Option to avoid bloating the struct
301    /// size.
302    in_ttc: bool,
303    // Whether the table directory is sorted and thus we can use binary search for
304    // finding table records. In principle, fonts are required to have a sorted
305    // table directory, but certain fonts don't seem to follow that requirement.
306    table_directory_sorted: bool,
307}
308
309impl<'a> FontRef<'a> {
310    /// Creates a new reference to an in-memory font backed by the given data.
311    ///
312    /// The data must be a single font (not a font collection) and must begin with a
313    /// [table directory] to be considered valid.
314    ///
315    /// To load a font from a font collection, use [`FontRef::from_index`] instead.
316    ///
317    /// [table directory]: https://github.com/googlefonts/fontations/pull/549
318    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
319        let data = FontData::new(data);
320        Self::with_table_directory(data, TableDirectory::read(data)?, None)
321    }
322
323    /// Creates a new reference to an in-memory font at the specified index
324    /// backed by the given data.
325    ///
326    /// The data slice must begin with either a
327    /// [table directory](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory)
328    /// or a [ttc header](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header)
329    /// to be considered valid.
330    ///
331    /// In other words, this accepts either font collection (ttc) or single
332    /// font (ttf/otf) files. If a single font file is provided, the index
333    /// parameter must be 0.
334    pub fn from_index(data: &'a [u8], index: u32) -> Result<Self, ReadError> {
335        let file = FileRef::new(data)?;
336        match file {
337            FileRef::Font(font) => {
338                if index == 0 {
339                    Ok(font)
340                } else {
341                    Err(ReadError::InvalidCollectionIndex(index))
342                }
343            }
344            FileRef::Collection(collection) => collection.get(index),
345        }
346    }
347
348    /// Returns the underlying font data.
349    ///
350    /// This is the base from which tables are loaded, meaning that for
351    /// TrueType collection files, this will be the entire font file data.
352    pub fn data(&self) -> FontData<'a> {
353        self.data
354    }
355
356    /// If the font is in a TrueType collection (ttc) file, returns the index
357    /// of the font in that collection.
358    pub fn ttc_index(&self) -> Option<u32> {
359        self.in_ttc.then_some(self.ttc_index)
360    }
361
362    /// Returns the associated table directory.
363    pub fn table_directory(&self) -> &TableDirectory<'a> {
364        &self.table_directory
365    }
366
367    /// Returns the data for the table with the specified tag, if present.
368    pub fn table_data(&self, tag: Tag) -> Option<FontData<'a>> {
369        let entry = if self.table_directory_sorted {
370            self.table_directory
371                .table_records()
372                .binary_search_by(|rec| rec.tag.get().cmp(&tag))
373                .ok()
374        } else {
375            self.table_directory
376                .table_records()
377                .iter()
378                .position(|rec| rec.tag.get().eq(&tag))
379        };
380
381        entry
382            .and_then(|idx| self.table_directory.table_records().get(idx))
383            .and_then(|record| {
384                let start = Offset32::new(record.offset()).non_null()?;
385                let len = record.length() as usize;
386                self.data.slice(start..start.checked_add(len)?)
387            })
388    }
389
390    /// Returns an iterator over all of the available fonts in
391    /// the given font data.
392    pub fn fonts(
393        data: &'a [u8],
394    ) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
395        let count = match FileRef::new(data) {
396            Ok(FileRef::Font(_)) => 1,
397            Ok(FileRef::Collection(ttc)) => ttc.len(),
398            _ => 0,
399        };
400        (0..count).map(|idx| FontRef::from_index(data, idx))
401    }
402
403    fn with_table_directory(
404        data: FontData<'a>,
405        table_directory: TableDirectory<'a>,
406        ttc_index: Option<u32>,
407    ) -> Result<Self, ReadError> {
408        if [TT_SFNT_VERSION, CFF_SFNT_VERSION, TRUE_SFNT_VERSION]
409            .contains(&table_directory.sfnt_version())
410        {
411            let table_directory_sorted = table_directory.is_sorted();
412
413            Ok(FontRef {
414                data,
415                table_directory,
416                ttc_index: ttc_index.unwrap_or_default(),
417                in_ttc: ttc_index.is_some(),
418                table_directory_sorted,
419            })
420        } else {
421            Err(ReadError::InvalidSfnt(table_directory.sfnt_version()))
422        }
423    }
424}
425
426impl<'a> TableProvider<'a> for FontRef<'a> {
427    fn data_for_tag(&self, tag: Tag) -> Option<FontData<'a>> {
428        self.table_data(tag)
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use font_test_data::{be_buffer, bebuffer::BeBuffer, ttc::TTC, AHEM};
435    use types::{Tag, TT_SFNT_VERSION};
436
437    use crate::{FileRef, FontRef};
438
439    #[test]
440    fn file_ref_non_collection() {
441        assert!(matches!(FileRef::new(AHEM), Ok(FileRef::Font(_))));
442    }
443
444    #[test]
445    fn file_ref_collection() {
446        let Ok(FileRef::Collection(collection)) = FileRef::new(TTC) else {
447            panic!("Expected a collection");
448        };
449        assert_eq!(2, collection.len());
450        assert!(!collection.is_empty());
451    }
452
453    #[test]
454    fn font_ref_fonts_iter() {
455        assert_eq!(FontRef::fonts(AHEM).count(), 1);
456        assert_eq!(FontRef::fonts(TTC).count(), 2);
457        assert_eq!(FontRef::fonts(b"NOT_A_FONT").count(), 0);
458    }
459
460    #[test]
461    fn ttc_index() {
462        for (idx, font) in FontRef::fonts(TTC).map(|font| font.unwrap()).enumerate() {
463            assert_eq!(font.ttc_index(), Some(idx as u32));
464        }
465        assert!(FontRef::new(AHEM).unwrap().ttc_index().is_none());
466    }
467
468    #[test]
469    fn unsorted_table_directory() {
470        let cff2_data = font_test_data::cff2::EXAMPLE;
471        let post_data = font_test_data::post::SIMPLE;
472        let gdef_data = [
473            font_test_data::gdef::GDEF_HEADER,
474            font_test_data::gdef::GLYPHCLASSDEF_TABLE,
475        ]
476        .concat();
477        let gpos_data = font_test_data::gpos::SINGLEPOSFORMAT1;
478
479        let font_data = be_buffer! {
480            TT_SFNT_VERSION,
481            4u16,    // num tables
482            64u16,   // search range
483            2u16,    // entry selector
484            0u16,    // range shift
485
486            (Tag::new(b"post")),
487            0u32,    // checksum
488            76u32,   // offset
489            (post_data.len() as u32),
490
491            (Tag::new(b"GPOS")),
492            0u32,    // checksum
493            108u32,  // offset
494            (gpos_data.len() as u32),
495
496            (Tag::new(b"GDEF")),
497            0u32,    // checksum
498            128u32,  // offset
499            (gdef_data.len() as u32),
500
501            (Tag::new(b"CFF2")),
502            0u32,    // checksum
503            160u32,  // offset
504            (cff2_data.len() as u32)
505        };
506
507        let mut full_font = font_data.to_vec();
508
509        full_font.extend_from_slice(post_data);
510        full_font.extend_from_slice(gpos_data);
511        full_font.extend_from_slice(&gdef_data);
512        full_font.extend_from_slice(cff2_data);
513
514        let font = FontRef::new(&full_font).unwrap();
515
516        assert!(!font.table_directory_sorted);
517
518        assert!(font.table_data(Tag::new(b"CFF2")).is_some());
519        assert!(font.table_data(Tag::new(b"GDEF")).is_some());
520        assert!(font.table_data(Tag::new(b"GPOS")).is_some());
521        assert!(font.table_data(Tag::new(b"post")).is_some());
522    }
523}