#![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;
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;
pub extern crate font_types as types;
#[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 use crate::read::{
ComputeSize, 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};
#[cfg(feature = "experimental_traverse")]
pub(crate) fn better_type_name<T>() -> &'static str {
let raw_name = std::any::type_name::<T>();
raw_name.rsplit("::").next().unwrap_or(raw_name)
}
pub(crate) mod transforms {
pub fn subtract<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
lhs.try_into()
.unwrap_or_default()
.saturating_sub(rhs.try_into().unwrap_or_default())
}
pub fn add<T: TryInto<usize>, U: TryInto<usize>>(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<T: TryInto<usize>>(count: T) -> usize {
count.try_into().unwrap_or_default().div_ceil(8)
}
#[cfg(feature = "ift")]
pub fn max_value_bitmap_len<T: TryInto<usize>>(count: T) -> usize {
let count: usize = count.try_into().unwrap_or_default() + 1usize;
count.div_ceil(8)
}
pub fn add_multiply<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
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<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
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<T: TryInto<usize>>(val: T) -> usize {
val.try_into().unwrap_or_default() / 2
}
pub fn subtract_add_two<T: TryInto<usize>, U: TryInto<usize>>(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) => {
pub fn resolve_offset<O: Offset, R: FontRead<'a>>(
&self,
offset: O,
) -> Result<R, ReadError> {
offset.resolve(self.data)
}
pub fn offset_data(&self) -> FontData<'a> {
self.data
}
#[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)]
pub enum FileRef<'a> {
Font(FontRef<'a>),
Collection(CollectionRef<'a>),
}
impl<'a> FileRef<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
Ok(if let Ok(collection) = CollectionRef::new(data) {
Self::Collection(collection)
} else {
Self::Font(FontRef::new(data)?)
})
}
pub fn fonts(&self) -> impl Iterator<Item = Result<FontRef<'a>, 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)
}
}
#[derive(Clone)]
pub struct CollectionRef<'a> {
data: FontData<'a>,
header: TTCHeader<'a>,
}
impl<'a> CollectionRef<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
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 })
}
}
pub fn len(&self) -> u32 {
self.header.table_directory_offsets().len() as u32
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, index: u32) -> Result<FontRef<'a>, 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),
)
}
pub fn iter(&self) -> impl Iterator<Item = Result<FontRef<'a>, 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
}
}
#[derive(Clone)]
pub struct FontRef<'a> {
data: FontData<'a>,
pub table_directory: TableDirectory<'a>,
ttc_index: u32,
in_ttc: bool,
table_directory_sorted: bool,
}
impl<'a> FontRef<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
let data = FontData::new(data);
Self::with_table_directory(data, TableDirectory::read(data)?, None)
}
pub fn from_index(data: &'a [u8], index: u32) -> Result<Self, ReadError> {
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),
}
}
pub fn data(&self) -> FontData<'a> {
self.data
}
pub fn ttc_index(&self) -> Option<u32> {
self.in_ttc.then_some(self.ttc_index)
}
pub fn table_directory(&self) -> &TableDirectory<'a> {
&self.table_directory
}
pub fn table_data(&self, tag: Tag) -> Option<FontData<'a>> {
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)?)
})
}
pub fn fonts(
data: &'a [u8],
) -> impl Iterator<Item = Result<FontRef<'a>, 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<u32>,
) -> Result<Self, ReadError> {
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<FontData<'a>> {
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, 64u16, 2u16, 0u16,
(Tag::new(b"post")),
0u32, 76u32, (post_data.len() as u32),
(Tag::new(b"GPOS")),
0u32, 108u32, (gpos_data.len() as u32),
(Tag::new(b"GDEF")),
0u32, 128u32, (gdef_data.len() as u32),
(Tag::new(b"CFF2")),
0u32, 160u32, (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());
}
}