//! Reading OpenType tables //! //! This crate provides memory safe zero-allocation parsing of font files. //! It is unopinionated, and attempts to provide raw access to the underlying //! font data as it is described in the [OpenType specification][spec]. //! //! This crate is intended for use by other parts of a font stack, such as a //! shaping engine or a glyph rasterizer. //! //! In addition to raw data access, this crate may also provide reference //! implementations of algorithms for interpreting that data, where such an //! implementation is required for the data to be useful. For instance, we //! provide functions for [mapping codepoints to glyph identifiers][cmap-impl] //! using the `cmap` table, or for [decoding entries in the `name` table][NameString]. //! //! For higher level/more ergonomic access to font data, you may want to look //! into using [`skrifa`] instead. //! //! ## Structure & codegen //! //! The root [`tables`] module contains a submodule for each supported //! [table][table-directory], and that submodule contains items for each table, //! record, flagset or enum described in the relevant portion of the spec. //! //! The majority of the code in the tables module is auto-generated. For more //! information on our use of codegen, see the [codegen tour]. //! //! # Related projects //! //! - [`write-fonts`] is a companion crate for creating/modifying font files //! - [`skrifa`] provides access to glyph outlines and metadata (in the same vein //! as [freetype]) //! //! # Example //! //! ```no_run //! # let path_to_my_font_file = std::path::Path::new(""); //! use read_fonts::{FontRef, TableProvider}; //! let font_bytes = std::fs::read(path_to_my_font_file).unwrap(); //! // Single fonts only. for font collections (.ttc) use FontRef::from_index //! let font = FontRef::new(&font_bytes).expect("failed to read font data"); //! let head = font.head().expect("missing 'head' table"); //! let maxp = font.maxp().expect("missing 'maxp' table"); //! //! println!("font version {} containing {} glyphs", head.font_revision(), maxp.num_glyphs()); //! ``` //! //! //! [spec]: https://learn.microsoft.com/en-us/typography/opentype/spec/ //! [codegen-tour]: https://github.com/googlefonts/fontations/blob/main/docs/codegen-tour.md //! [cmap-impl]: tables::cmap::Cmap::map_codepoint //! [`write-fonts`]: https://docs.rs/write-fonts/ //! [`skrifa`]: https://docs.rs/skrifa/ //! [freetype]: http://freetype.org //! [codegen tour]: https://github.com/googlefonts/fontations/blob/main/docs/codegen-tour.md //! [NameString]: tables::name::NameString //! [table-directory]: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory #![cfg_attr(docsrs, feature(doc_cfg))] #![forbid(unsafe_code)] #![deny(rustdoc::broken_intra_doc_links)] #![cfg_attr(not(feature = "std"), no_std)] #[cfg(any(feature = "std", test))] #[macro_use] extern crate std; #[cfg(all(not(feature = "std"), not(test)))] #[macro_use] extern crate core as std; // Always depend on alloc. Perhaps make this a feature if someone really needs // heapless read-fonts. extern crate alloc; pub mod array; pub mod collections; mod font_data; pub mod model; mod offset; mod offset_array; pub mod ps; mod read; mod table_provider; mod table_ref; pub mod tables; #[cfg(feature = "experimental_traverse")] pub mod traversal; #[cfg(any(test, feature = "codegen_test"))] pub mod codegen_test; pub use font_data::FontData; pub use offset::{Offset, ResolveNullableOffset, ResolveOffset}; pub use offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets}; pub use read::{ComputeSize, FontRead, FontReadWithArgs, ReadArgs, ReadError, VarSize}; pub use table_provider::{TableProvider, TopLevelTable}; pub use table_ref::MinByteRange; /// Public re-export of the font-types crate. pub extern crate font_types as types; /// All the types that may be referenced in auto-generated code. #[doc(hidden)] pub(crate) mod codegen_prelude { pub use crate::array::{ComputedArray, VarLenArray}; pub use crate::font_data::{Cursor, FontData}; pub use crate::offset::{Offset, ResolveNullableOffset, ResolveOffset}; pub use crate::offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets}; //pub(crate) use crate::read::sealed; pub use crate::read::{ ComputeSize, Discriminant, FontRead, FontReadWithArgs, Format, ReadArgs, ReadError, VarSize, }; pub use crate::table_provider::TopLevelTable; pub use crate::table_ref::MinByteRange; pub use std::ops::Range; pub use types::*; #[cfg(feature = "experimental_traverse")] pub use crate::traversal::{self, Field, FieldType, RecordResolver, SomeRecord, SomeTable}; /// named transforms used in 'count', e.g pub(crate) mod transforms { pub fn to_usize>(value: T) -> usize { value.try_into().unwrap_or_default() } pub fn subtract, U: TryInto>(lhs: T, rhs: U) -> usize { lhs.try_into() .unwrap_or_default() .saturating_sub(rhs.try_into().unwrap_or_default()) } pub fn add, U: TryInto>(lhs: T, rhs: U) -> usize { lhs.try_into() .unwrap_or_default() .saturating_add(rhs.try_into().unwrap_or_default()) } #[allow(dead_code)] pub fn bitmap_len>(count: T) -> usize { count.try_into().unwrap_or_default().div_ceil(8) } #[cfg(feature = "ift")] pub fn max_value_bitmap_len>(count: T) -> usize { let count: usize = count.try_into().unwrap_or_default() + 1usize; count.div_ceil(8) } pub fn add_multiply, U: TryInto, V: TryInto>( a: T, b: U, c: V, ) -> usize { a.try_into() .unwrap_or_default() .saturating_add(b.try_into().unwrap_or_default()) .saturating_mul(c.try_into().unwrap_or_default()) } #[cfg(feature = "ift")] pub fn multiply_add, U: TryInto, V: TryInto>( a: T, b: U, c: V, ) -> usize { a.try_into() .unwrap_or_default() .saturating_mul(b.try_into().unwrap_or_default()) .saturating_add(c.try_into().unwrap_or_default()) } pub fn half>(val: T) -> usize { val.try_into().unwrap_or_default() / 2 } pub fn subtract_add_two, U: TryInto>(lhs: T, rhs: U) -> usize { lhs.try_into() .unwrap_or_default() .saturating_sub(rhs.try_into().unwrap_or_default()) .saturating_add(2) } } #[macro_export] macro_rules! basic_table_impls { (impl_the_methods) => { /// Resolve the provided offset from the start of this table. pub fn resolve_offset>( &self, offset: O, ) -> Result { offset.resolve(self.data) } /// Return a reference to this table's raw data. /// /// We use this in the compile crate to resolve offsets. pub fn offset_data(&self) -> FontData<'a> { self.data } /// Return a reference to the table's 'Shape' struct. /// /// This is a low level implementation detail, but it can be useful in /// some cases where you want to know things about a table's layout, such /// as the byte offsets of specific fields. #[deprecated(note = "just use the base type directly")] pub fn shape(&self) -> &Self { &self } }; } pub(crate) use crate::basic_table_impls; } include!("../generated/font.rs"); #[derive(Clone)] /// Reference to the content of a font or font collection file. pub enum FileRef<'a> { /// A single font. Font(FontRef<'a>), /// A collection of fonts. Collection(CollectionRef<'a>), } impl<'a> FileRef<'a> { /// Creates a new reference to a file representing a font or font collection. pub fn new(data: &'a [u8]) -> Result { Ok(if let Ok(collection) = CollectionRef::new(data) { Self::Collection(collection) } else { Self::Font(FontRef::new(data)?) }) } /// Returns an iterator over the fonts contained in the file. pub fn fonts(&self) -> impl Iterator, ReadError>> + 'a + Clone { let (iter_one, iter_two) = match self { Self::Font(font) => (Some(Ok(font.clone())), None), Self::Collection(collection) => (None, Some(collection.iter())), }; iter_two.into_iter().flatten().chain(iter_one) } } /// Reference to the content of a font collection file. #[derive(Clone)] pub struct CollectionRef<'a> { data: FontData<'a>, header: TTCHeader<'a>, } impl<'a> CollectionRef<'a> { /// Creates a new reference to a font collection. pub fn new(data: &'a [u8]) -> Result { let data = FontData::new(data); let header = TTCHeader::read(data)?; if header.ttc_tag() != TTC_HEADER_TAG { Err(ReadError::InvalidTtc(header.ttc_tag())) } else { Ok(Self { data, header }) } } /// Returns the number of fonts in the collection. pub fn len(&self) -> u32 { self.header.table_directory_offsets().len() as u32 } /// Returns true if the collection is empty. pub fn is_empty(&self) -> bool { self.len() == 0 } /// Returns the font in the collection at the specified index. pub fn get(&self, index: u32) -> Result, ReadError> { let offset = self .header .table_directory_offsets() .get(index as usize) .ok_or(ReadError::InvalidCollectionIndex(index))? .get() as usize; let table_dir_data = self.data.slice(offset..).ok_or(ReadError::OutOfBounds)?; FontRef::with_table_directory( self.data, TableDirectory::read(table_dir_data)?, Some(index), ) } /// Returns an iterator over the fonts in the collection. pub fn iter(&self) -> impl Iterator, ReadError>> + 'a + Clone { let copy = self.clone(); (0..self.len()).map(move |ix| copy.get(ix)) } } impl TableDirectory<'_> { fn is_sorted(&self) -> bool { let mut last_tag = Tag::new(&[0u8; 4]); for tag in self.table_records().iter().map(|rec| rec.tag()) { if tag <= last_tag { return false; } last_tag = tag; } true } } /// Reference to an in-memory font. /// /// This is a simple implementation of the [`TableProvider`] trait backed /// by a borrowed slice containing font data. #[derive(Clone)] pub struct FontRef<'a> { data: FontData<'a>, pub table_directory: TableDirectory<'a>, /// The index of this font in a TrueType collection ttc_index: u32, /// Whether this font is a member of a TrueType collection. /// /// We use a bool rather than an Option to avoid bloating the struct /// size. in_ttc: bool, // Whether the table directory is sorted and thus we can use binary search for // finding table records. In principle, fonts are required to have a sorted // table directory, but certain fonts don't seem to follow that requirement. table_directory_sorted: bool, } impl<'a> FontRef<'a> { /// Creates a new reference to an in-memory font backed by the given data. /// /// The data must be a single font (not a font collection) and must begin with a /// [table directory] to be considered valid. /// /// To load a font from a font collection, use [`FontRef::from_index`] instead. /// /// [table directory]: https://github.com/googlefonts/fontations/pull/549 pub fn new(data: &'a [u8]) -> Result { let data = FontData::new(data); Self::with_table_directory(data, TableDirectory::read(data)?, None) } /// Creates a new reference to an in-memory font at the specified index /// backed by the given data. /// /// The data slice must begin with either a /// [table directory](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory) /// or a [ttc header](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header) /// to be considered valid. /// /// In other words, this accepts either font collection (ttc) or single /// font (ttf/otf) files. If a single font file is provided, the index /// parameter must be 0. pub fn from_index(data: &'a [u8], index: u32) -> Result { let file = FileRef::new(data)?; match file { FileRef::Font(font) => { if index == 0 { Ok(font) } else { Err(ReadError::InvalidCollectionIndex(index)) } } FileRef::Collection(collection) => collection.get(index), } } /// Returns the underlying font data. /// /// This is the base from which tables are loaded, meaning that for /// TrueType collection files, this will be the entire font file data. pub fn data(&self) -> FontData<'a> { self.data } /// If the font is in a TrueType collection (ttc) file, returns the index /// of the font in that collection. pub fn ttc_index(&self) -> Option { self.in_ttc.then_some(self.ttc_index) } /// Returns the associated table directory. pub fn table_directory(&self) -> &TableDirectory<'a> { &self.table_directory } /// Returns the data for the table with the specified tag, if present. pub fn table_data(&self, tag: Tag) -> Option> { let entry = if self.table_directory_sorted { self.table_directory .table_records() .binary_search_by(|rec| rec.tag.get().cmp(&tag)) .ok() } else { self.table_directory .table_records() .iter() .position(|rec| rec.tag.get().eq(&tag)) }; entry .and_then(|idx| self.table_directory.table_records().get(idx)) .and_then(|record| { let start = Offset32::new(record.offset()).non_null()?; let len = record.length() as usize; self.data.slice(start..start.checked_add(len)?) }) } /// Returns an iterator over all of the available fonts in /// the given font data. pub fn fonts( data: &'a [u8], ) -> impl Iterator, ReadError>> + 'a + Clone { let count = match FileRef::new(data) { Ok(FileRef::Font(_)) => 1, Ok(FileRef::Collection(ttc)) => ttc.len(), _ => 0, }; (0..count).map(|idx| FontRef::from_index(data, idx)) } fn with_table_directory( data: FontData<'a>, table_directory: TableDirectory<'a>, ttc_index: Option, ) -> Result { if [TT_SFNT_VERSION, CFF_SFNT_VERSION, TRUE_SFNT_VERSION] .contains(&table_directory.sfnt_version()) { let table_directory_sorted = table_directory.is_sorted(); Ok(FontRef { data, table_directory, ttc_index: ttc_index.unwrap_or_default(), in_ttc: ttc_index.is_some(), table_directory_sorted, }) } else { Err(ReadError::InvalidSfnt(table_directory.sfnt_version())) } } } impl<'a> TableProvider<'a> for FontRef<'a> { fn data_for_tag(&self, tag: Tag) -> Option> { self.table_data(tag) } } #[cfg(test)] mod tests { use font_test_data::{be_buffer, bebuffer::BeBuffer, ttc::TTC, AHEM}; use types::{Tag, TT_SFNT_VERSION}; use crate::{FileRef, FontRef}; #[test] fn file_ref_non_collection() { assert!(matches!(FileRef::new(AHEM), Ok(FileRef::Font(_)))); } #[test] fn file_ref_collection() { let Ok(FileRef::Collection(collection)) = FileRef::new(TTC) else { panic!("Expected a collection"); }; assert_eq!(2, collection.len()); assert!(!collection.is_empty()); } #[test] fn font_ref_fonts_iter() { assert_eq!(FontRef::fonts(AHEM).count(), 1); assert_eq!(FontRef::fonts(TTC).count(), 2); assert_eq!(FontRef::fonts(b"NOT_A_FONT").count(), 0); } #[test] fn ttc_index() { for (idx, font) in FontRef::fonts(TTC).map(|font| font.unwrap()).enumerate() { assert_eq!(font.ttc_index(), Some(idx as u32)); } assert!(FontRef::new(AHEM).unwrap().ttc_index().is_none()); } #[test] fn unsorted_table_directory() { let cff2_data = font_test_data::cff2::EXAMPLE; let post_data = font_test_data::post::SIMPLE; let gdef_data = [ font_test_data::gdef::GDEF_HEADER, font_test_data::gdef::GLYPHCLASSDEF_TABLE, ] .concat(); let gpos_data = font_test_data::gpos::SINGLEPOSFORMAT1; let font_data = be_buffer! { TT_SFNT_VERSION, 4u16, // num tables 64u16, // search range 2u16, // entry selector 0u16, // range shift (Tag::new(b"post")), 0u32, // checksum 76u32, // offset (post_data.len() as u32), (Tag::new(b"GPOS")), 0u32, // checksum 108u32, // offset (gpos_data.len() as u32), (Tag::new(b"GDEF")), 0u32, // checksum 128u32, // offset (gdef_data.len() as u32), (Tag::new(b"CFF2")), 0u32, // checksum 160u32, // offset (cff2_data.len() as u32) }; let mut full_font = font_data.to_vec(); full_font.extend_from_slice(post_data); full_font.extend_from_slice(gpos_data); full_font.extend_from_slice(&gdef_data); full_font.extend_from_slice(cff2_data); let font = FontRef::new(&full_font).unwrap(); assert!(!font.table_directory_sorted); assert!(font.table_data(Tag::new(b"CFF2")).is_some()); assert!(font.table_data(Tag::new(b"GDEF")).is_some()); assert!(font.table_data(Tag::new(b"GPOS")).is_some()); assert!(font.table_data(Tag::new(b"post")).is_some()); } }