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() + 7) / 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 + 7) / 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(self.data, TableDirectory::read(table_dir_data)?)
259    }
260
261    /// Returns an iterator over the fonts in the collection.
262    pub fn iter(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
263        let copy = self.clone();
264        (0..self.len()).map(move |ix| copy.get(ix))
265    }
266}
267
268/// Reference to an in-memory font.
269///
270/// This is a simple implementation of the [`TableProvider`] trait backed
271/// by a borrowed slice containing font data.
272#[derive(Clone)]
273pub struct FontRef<'a> {
274    pub data: FontData<'a>,
275    pub table_directory: TableDirectory<'a>,
276}
277
278impl<'a> FontRef<'a> {
279    /// Creates a new reference to an in-memory font backed by the given data.
280    ///
281    /// The data must be a single font (not a font collection) and must begin with a
282    /// [table directory] to be considered valid.
283    ///
284    /// To load a font from a font collection, use [`FontRef::from_index`] instead.
285    ///
286    /// [table directory]: https://github.com/googlefonts/fontations/pull/549
287    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
288        let data = FontData::new(data);
289        Self::with_table_directory(data, TableDirectory::read(data)?)
290    }
291
292    /// Creates a new reference to an in-memory font at the specified index
293    /// backed by the given data.
294    ///
295    /// The data slice must begin with either a
296    /// [table directory](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory)
297    /// or a [ttc header](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header)
298    /// to be considered valid.
299    ///
300    /// In other words, this accepts either font collection (ttc) or single
301    /// font (ttf/otf) files. If a single font file is provided, the index
302    /// parameter must be 0.
303    pub fn from_index(data: &'a [u8], index: u32) -> Result<Self, ReadError> {
304        let file = FileRef::new(data)?;
305        match file {
306            FileRef::Font(font) => {
307                if index == 0 {
308                    Ok(font)
309                } else {
310                    Err(ReadError::InvalidCollectionIndex(index))
311                }
312            }
313            FileRef::Collection(collection) => collection.get(index),
314        }
315    }
316
317    /// Returns the data for the table with the specified tag, if present.
318    pub fn table_data(&self, tag: Tag) -> Option<FontData<'a>> {
319        self.table_directory
320            .table_records()
321            .binary_search_by(|rec| rec.tag.get().cmp(&tag))
322            .ok()
323            .and_then(|idx| self.table_directory.table_records().get(idx))
324            .and_then(|record| {
325                let start = Offset32::new(record.offset()).non_null()?;
326                let len = record.length() as usize;
327                self.data.slice(start..start.checked_add(len)?)
328            })
329    }
330
331    fn with_table_directory(
332        data: FontData<'a>,
333        table_directory: TableDirectory<'a>,
334    ) -> Result<Self, ReadError> {
335        if [TT_SFNT_VERSION, CFF_SFNT_VERSION, TRUE_SFNT_VERSION]
336            .contains(&table_directory.sfnt_version())
337        {
338            Ok(FontRef {
339                data,
340                table_directory,
341            })
342        } else {
343            Err(ReadError::InvalidSfnt(table_directory.sfnt_version()))
344        }
345    }
346}
347
348impl<'a> TableProvider<'a> for FontRef<'a> {
349    fn data_for_tag(&self, tag: Tag) -> Option<FontData<'a>> {
350        self.table_data(tag)
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use font_test_data::{ttc::TTC, AHEM};
357
358    use crate::FileRef;
359
360    #[test]
361    fn file_ref_non_collection() {
362        assert!(matches!(FileRef::new(AHEM), Ok(FileRef::Font(_))));
363    }
364
365    #[test]
366    fn file_ref_collection() {
367        let Ok(FileRef::Collection(collection)) = FileRef::new(TTC) else {
368            panic!("Expected a collection");
369        };
370        assert_eq!(2, collection.len());
371        assert!(!collection.is_empty());
372    }
373}