Skip to main content

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_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
72// Always depend on alloc. Perhaps make this a feature if someone really needs
73// heapless read-fonts.
74extern crate alloc;
75
76pub mod array;
77pub mod collections;
78mod font_data;
79pub mod model;
80mod offset;
81mod offset_array;
82pub mod ps;
83mod read;
84mod table_provider;
85mod table_ref;
86pub mod tables;
87#[cfg(feature = "experimental_traverse")]
88pub mod traversal;
89
90#[cfg(any(test, feature = "codegen_test"))]
91pub mod codegen_test;
92
93pub use font_data::FontData;
94pub use offset::{Offset, ResolveNullableOffset, ResolveOffset};
95pub use offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets};
96pub use read::{ComputeSize, FontRead, FontReadWithArgs, ReadArgs, ReadError, VarSize};
97pub use table_provider::{TableProvider, TopLevelTable};
98pub use table_ref::MinByteRange;
99
100/// Public re-export of the font-types crate.
101pub extern crate font_types as types;
102
103/// All the types that may be referenced in auto-generated code.
104#[doc(hidden)]
105pub(crate) mod codegen_prelude {
106    pub use crate::array::{ComputedArray, VarLenArray};
107    pub use crate::font_data::{Cursor, FontData};
108    pub use crate::offset::{Offset, ResolveNullableOffset, ResolveOffset};
109    pub use crate::offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets};
110    //pub(crate) use crate::read::sealed;
111    pub use crate::read::{
112        ComputeSize, FontRead, FontReadWithArgs, Format, ReadArgs, ReadError, VarSize,
113    };
114    pub use crate::table_provider::TopLevelTable;
115    pub use crate::table_ref::MinByteRange;
116    pub use std::ops::Range;
117
118    pub use types::*;
119
120    #[cfg(feature = "experimental_traverse")]
121    pub use crate::traversal::{self, Field, FieldType, RecordResolver, SomeRecord, SomeTable};
122
123    // used in generated traversal code to get type names of offset fields, which
124    // may include generics
125    #[cfg(feature = "experimental_traverse")]
126    pub(crate) fn better_type_name<T>() -> &'static str {
127        let raw_name = std::any::type_name::<T>();
128        raw_name.rsplit("::").next().unwrap_or(raw_name)
129    }
130
131    /// named transforms used in 'count', e.g
132    pub(crate) mod transforms {
133        pub fn subtract<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
134            lhs.try_into()
135                .unwrap_or_default()
136                .saturating_sub(rhs.try_into().unwrap_or_default())
137        }
138
139        pub fn add<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
140            lhs.try_into()
141                .unwrap_or_default()
142                .saturating_add(rhs.try_into().unwrap_or_default())
143        }
144
145        #[allow(dead_code)]
146        pub fn bitmap_len<T: TryInto<usize>>(count: T) -> usize {
147            count.try_into().unwrap_or_default().div_ceil(8)
148        }
149
150        #[cfg(feature = "ift")]
151        pub fn max_value_bitmap_len<T: TryInto<usize>>(count: T) -> usize {
152            let count: usize = count.try_into().unwrap_or_default() + 1usize;
153            count.div_ceil(8)
154        }
155
156        pub fn add_multiply<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
157            a: T,
158            b: U,
159            c: V,
160        ) -> usize {
161            a.try_into()
162                .unwrap_or_default()
163                .saturating_add(b.try_into().unwrap_or_default())
164                .saturating_mul(c.try_into().unwrap_or_default())
165        }
166
167        #[cfg(feature = "ift")]
168        pub fn multiply_add<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
169            a: T,
170            b: U,
171            c: V,
172        ) -> usize {
173            a.try_into()
174                .unwrap_or_default()
175                .saturating_mul(b.try_into().unwrap_or_default())
176                .saturating_add(c.try_into().unwrap_or_default())
177        }
178
179        pub fn half<T: TryInto<usize>>(val: T) -> usize {
180            val.try_into().unwrap_or_default() / 2
181        }
182
183        pub fn subtract_add_two<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
184            lhs.try_into()
185                .unwrap_or_default()
186                .saturating_sub(rhs.try_into().unwrap_or_default())
187                .saturating_add(2)
188        }
189    }
190
191    #[macro_export]
192    macro_rules! basic_table_impls {
193        (impl_the_methods) => {
194            /// Resolve the provided offset from the start of this table.
195            pub fn resolve_offset<O: Offset, R: FontRead<'a>>(
196                &self,
197                offset: O,
198            ) -> Result<R, ReadError> {
199                offset.resolve(self.data)
200            }
201
202            /// Return a reference to this table's raw data.
203            ///
204            /// We use this in the compile crate to resolve offsets.
205            pub fn offset_data(&self) -> FontData<'a> {
206                self.data
207            }
208
209            /// Return a reference to the table's 'Shape' struct.
210            ///
211            /// This is a low level implementation detail, but it can be useful in
212            /// some cases where you want to know things about a table's layout, such
213            /// as the byte offsets of specific fields.
214            #[deprecated(note = "just use the base type directly")]
215            pub fn shape(&self) -> &Self {
216                &self
217            }
218        };
219    }
220
221    pub(crate) use crate::basic_table_impls;
222}
223
224include!("../generated/font.rs");
225
226#[derive(Clone)]
227/// Reference to the content of a font or font collection file.
228pub enum FileRef<'a> {
229    /// A single font.
230    Font(FontRef<'a>),
231    /// A collection of fonts.
232    Collection(CollectionRef<'a>),
233}
234
235impl<'a> FileRef<'a> {
236    /// Creates a new reference to a file representing a font or font collection.
237    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
238        Ok(if let Ok(collection) = CollectionRef::new(data) {
239            Self::Collection(collection)
240        } else {
241            Self::Font(FontRef::new(data)?)
242        })
243    }
244
245    /// Returns an iterator over the fonts contained in the file.
246    pub fn fonts(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
247        let (iter_one, iter_two) = match self {
248            Self::Font(font) => (Some(Ok(font.clone())), None),
249            Self::Collection(collection) => (None, Some(collection.iter())),
250        };
251        iter_two.into_iter().flatten().chain(iter_one)
252    }
253}
254
255/// Reference to the content of a font collection file.
256#[derive(Clone)]
257pub struct CollectionRef<'a> {
258    data: FontData<'a>,
259    header: TTCHeader<'a>,
260}
261
262impl<'a> CollectionRef<'a> {
263    /// Creates a new reference to a font collection.
264    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
265        let data = FontData::new(data);
266        let header = TTCHeader::read(data)?;
267        if header.ttc_tag() != TTC_HEADER_TAG {
268            Err(ReadError::InvalidTtc(header.ttc_tag()))
269        } else {
270            Ok(Self { data, header })
271        }
272    }
273
274    /// Returns the number of fonts in the collection.
275    pub fn len(&self) -> u32 {
276        self.header.table_directory_offsets().len() as u32
277    }
278
279    /// Returns true if the collection is empty.
280    pub fn is_empty(&self) -> bool {
281        self.len() == 0
282    }
283
284    /// Returns the font in the collection at the specified index.
285    pub fn get(&self, index: u32) -> Result<FontRef<'a>, ReadError> {
286        let offset = self
287            .header
288            .table_directory_offsets()
289            .get(index as usize)
290            .ok_or(ReadError::InvalidCollectionIndex(index))?
291            .get() as usize;
292        let table_dir_data = self.data.slice(offset..).ok_or(ReadError::OutOfBounds)?;
293        FontRef::with_table_directory(
294            self.data,
295            TableDirectory::read(table_dir_data)?,
296            Some(index),
297        )
298    }
299
300    /// Returns an iterator over the fonts in the collection.
301    pub fn iter(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
302        let copy = self.clone();
303        (0..self.len()).map(move |ix| copy.get(ix))
304    }
305}
306
307impl TableDirectory<'_> {
308    fn is_sorted(&self) -> bool {
309        let mut last_tag = Tag::new(&[0u8; 4]);
310
311        for tag in self.table_records().iter().map(|rec| rec.tag()) {
312            if tag <= last_tag {
313                return false;
314            }
315
316            last_tag = tag;
317        }
318
319        true
320    }
321}
322
323/// Reference to an in-memory font.
324///
325/// This is a simple implementation of the [`TableProvider`] trait backed
326/// by a borrowed slice containing font data.
327#[derive(Clone)]
328pub struct FontRef<'a> {
329    data: FontData<'a>,
330    pub table_directory: TableDirectory<'a>,
331    /// The index of this font in a TrueType collection
332    ttc_index: u32,
333    /// Whether this font is a member of a TrueType collection.
334    ///
335    /// We use a bool rather than an Option to avoid bloating the struct
336    /// size.
337    in_ttc: bool,
338    // Whether the table directory is sorted and thus we can use binary search for
339    // finding table records. In principle, fonts are required to have a sorted
340    // table directory, but certain fonts don't seem to follow that requirement.
341    table_directory_sorted: bool,
342}
343
344impl<'a> FontRef<'a> {
345    /// Creates a new reference to an in-memory font backed by the given data.
346    ///
347    /// The data must be a single font (not a font collection) and must begin with a
348    /// [table directory] to be considered valid.
349    ///
350    /// To load a font from a font collection, use [`FontRef::from_index`] instead.
351    ///
352    /// [table directory]: https://github.com/googlefonts/fontations/pull/549
353    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
354        let data = FontData::new(data);
355        Self::with_table_directory(data, TableDirectory::read(data)?, None)
356    }
357
358    /// Creates a new reference to an in-memory font at the specified index
359    /// backed by the given data.
360    ///
361    /// The data slice must begin with either a
362    /// [table directory](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory)
363    /// or a [ttc header](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header)
364    /// to be considered valid.
365    ///
366    /// In other words, this accepts either font collection (ttc) or single
367    /// font (ttf/otf) files. If a single font file is provided, the index
368    /// parameter must be 0.
369    pub fn from_index(data: &'a [u8], index: u32) -> Result<Self, ReadError> {
370        let file = FileRef::new(data)?;
371        match file {
372            FileRef::Font(font) => {
373                if index == 0 {
374                    Ok(font)
375                } else {
376                    Err(ReadError::InvalidCollectionIndex(index))
377                }
378            }
379            FileRef::Collection(collection) => collection.get(index),
380        }
381    }
382
383    /// Returns the underlying font data.
384    ///
385    /// This is the base from which tables are loaded, meaning that for
386    /// TrueType collection files, this will be the entire font file data.
387    pub fn data(&self) -> FontData<'a> {
388        self.data
389    }
390
391    /// If the font is in a TrueType collection (ttc) file, returns the index
392    /// of the font in that collection.
393    pub fn ttc_index(&self) -> Option<u32> {
394        self.in_ttc.then_some(self.ttc_index)
395    }
396
397    /// Returns the associated table directory.
398    pub fn table_directory(&self) -> &TableDirectory<'a> {
399        &self.table_directory
400    }
401
402    /// Returns the data for the table with the specified tag, if present.
403    pub fn table_data(&self, tag: Tag) -> Option<FontData<'a>> {
404        let entry = if self.table_directory_sorted {
405            self.table_directory
406                .table_records()
407                .binary_search_by(|rec| rec.tag.get().cmp(&tag))
408                .ok()
409        } else {
410            self.table_directory
411                .table_records()
412                .iter()
413                .position(|rec| rec.tag.get().eq(&tag))
414        };
415
416        entry
417            .and_then(|idx| self.table_directory.table_records().get(idx))
418            .and_then(|record| {
419                let start = Offset32::new(record.offset()).non_null()?;
420                let len = record.length() as usize;
421                self.data.slice(start..start.checked_add(len)?)
422            })
423    }
424
425    /// Returns an iterator over all of the available fonts in
426    /// the given font data.
427    pub fn fonts(
428        data: &'a [u8],
429    ) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
430        let count = match FileRef::new(data) {
431            Ok(FileRef::Font(_)) => 1,
432            Ok(FileRef::Collection(ttc)) => ttc.len(),
433            _ => 0,
434        };
435        (0..count).map(|idx| FontRef::from_index(data, idx))
436    }
437
438    fn with_table_directory(
439        data: FontData<'a>,
440        table_directory: TableDirectory<'a>,
441        ttc_index: Option<u32>,
442    ) -> Result<Self, ReadError> {
443        if [TT_SFNT_VERSION, CFF_SFNT_VERSION, TRUE_SFNT_VERSION]
444            .contains(&table_directory.sfnt_version())
445        {
446            let table_directory_sorted = table_directory.is_sorted();
447
448            Ok(FontRef {
449                data,
450                table_directory,
451                ttc_index: ttc_index.unwrap_or_default(),
452                in_ttc: ttc_index.is_some(),
453                table_directory_sorted,
454            })
455        } else {
456            Err(ReadError::InvalidSfnt(table_directory.sfnt_version()))
457        }
458    }
459}
460
461impl<'a> TableProvider<'a> for FontRef<'a> {
462    fn data_for_tag(&self, tag: Tag) -> Option<FontData<'a>> {
463        self.table_data(tag)
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use font_test_data::{be_buffer, bebuffer::BeBuffer, ttc::TTC, AHEM};
470    use types::{Tag, TT_SFNT_VERSION};
471
472    use crate::{FileRef, FontRef};
473
474    #[test]
475    fn file_ref_non_collection() {
476        assert!(matches!(FileRef::new(AHEM), Ok(FileRef::Font(_))));
477    }
478
479    #[test]
480    fn file_ref_collection() {
481        let Ok(FileRef::Collection(collection)) = FileRef::new(TTC) else {
482            panic!("Expected a collection");
483        };
484        assert_eq!(2, collection.len());
485        assert!(!collection.is_empty());
486    }
487
488    #[test]
489    fn font_ref_fonts_iter() {
490        assert_eq!(FontRef::fonts(AHEM).count(), 1);
491        assert_eq!(FontRef::fonts(TTC).count(), 2);
492        assert_eq!(FontRef::fonts(b"NOT_A_FONT").count(), 0);
493    }
494
495    #[test]
496    fn ttc_index() {
497        for (idx, font) in FontRef::fonts(TTC).map(|font| font.unwrap()).enumerate() {
498            assert_eq!(font.ttc_index(), Some(idx as u32));
499        }
500        assert!(FontRef::new(AHEM).unwrap().ttc_index().is_none());
501    }
502
503    #[test]
504    fn unsorted_table_directory() {
505        let cff2_data = font_test_data::cff2::EXAMPLE;
506        let post_data = font_test_data::post::SIMPLE;
507        let gdef_data = [
508            font_test_data::gdef::GDEF_HEADER,
509            font_test_data::gdef::GLYPHCLASSDEF_TABLE,
510        ]
511        .concat();
512        let gpos_data = font_test_data::gpos::SINGLEPOSFORMAT1;
513
514        let font_data = be_buffer! {
515            TT_SFNT_VERSION,
516            4u16,    // num tables
517            64u16,   // search range
518            2u16,    // entry selector
519            0u16,    // range shift
520
521            (Tag::new(b"post")),
522            0u32,    // checksum
523            76u32,   // offset
524            (post_data.len() as u32),
525
526            (Tag::new(b"GPOS")),
527            0u32,    // checksum
528            108u32,  // offset
529            (gpos_data.len() as u32),
530
531            (Tag::new(b"GDEF")),
532            0u32,    // checksum
533            128u32,  // offset
534            (gdef_data.len() as u32),
535
536            (Tag::new(b"CFF2")),
537            0u32,    // checksum
538            160u32,  // offset
539            (cff2_data.len() as u32)
540        };
541
542        let mut full_font = font_data.to_vec();
543
544        full_font.extend_from_slice(post_data);
545        full_font.extend_from_slice(gpos_data);
546        full_font.extend_from_slice(&gdef_data);
547        full_font.extend_from_slice(cff2_data);
548
549        let font = FontRef::new(&full_font).unwrap();
550
551        assert!(!font.table_directory_sorted);
552
553        assert!(font.table_data(Tag::new(b"CFF2")).is_some());
554        assert!(font.table_data(Tag::new(b"GDEF")).is_some());
555        assert!(font.table_data(Tag::new(b"GPOS")).is_some());
556        assert!(font.table_data(Tag::new(b"post")).is_some());
557    }
558}