1#![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
72extern crate alloc;
75
76pub mod array;
77#[cfg(any(test, feature = "codegen_test"))]
78pub mod codegen_test;
79pub mod collections;
80mod font_data;
81pub mod limits;
82pub mod mem;
83pub mod model;
84mod offset;
85mod offset_array;
86pub mod ps;
87mod read;
88mod table_provider;
89mod table_ref;
90pub mod tables;
91
92pub use font_data::FontData;
93pub use offset::{Offset, ResolveNullableOffset, ResolveOffset};
94pub use offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets};
95pub use read::{ComputeSize, FontRead, FontReadAt, ReadArgs, ReadError, VarSize};
96pub use table_provider::{TableProvider, TopLevelTable};
97pub use table_ref::MinByteRange;
98
99pub extern crate font_types as types;
101
102#[doc(hidden)]
104pub(crate) mod codegen_prelude {
105 pub use crate::array::{ComputedArray, VarLenArray};
106 pub use crate::font_data::{Cursor, FontData};
107 pub use crate::offset::{Offset, ResolveNullableOffset, ResolveOffset};
108 pub use crate::offset_array::{ArrayOfNullableOffsets, ArrayOfOffsets};
109 pub use crate::read::{
110 ComputeSize, Discriminant, FontRead, FontReadAt, Format, ReadArgs, ReadError, VarSize,
111 };
112 pub use crate::table_provider::TopLevelTable;
113 pub use crate::table_ref::MinByteRange;
114 pub use std::ops::Range;
115
116 pub use types::*;
117
118 pub(crate) mod transforms {
120 pub fn to_usize<T: TryInto<usize>>(value: T) -> usize {
121 value.try_into().unwrap_or_default()
122 }
123
124 pub fn subtract<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
125 lhs.try_into()
126 .unwrap_or_default()
127 .saturating_sub(rhs.try_into().unwrap_or_default())
128 }
129
130 pub fn add<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
131 lhs.try_into()
132 .unwrap_or_default()
133 .saturating_add(rhs.try_into().unwrap_or_default())
134 }
135
136 #[allow(dead_code)]
137 pub fn bitmap_len<T: TryInto<usize>>(count: T) -> usize {
138 count.try_into().unwrap_or_default().div_ceil(8)
139 }
140
141 pub fn add_multiply<T: TryInto<usize>, U: TryInto<usize>, V: TryInto<usize>>(
142 a: T,
143 b: U,
144 c: V,
145 ) -> usize {
146 a.try_into()
147 .unwrap_or_default()
148 .saturating_add(b.try_into().unwrap_or_default())
149 .saturating_mul(c.try_into().unwrap_or_default())
150 }
151
152 #[cfg(feature = "ift")]
153 pub fn multiply_add<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_mul(b.try_into().unwrap_or_default())
161 .saturating_add(c.try_into().unwrap_or_default())
162 }
163
164 pub fn half<T: TryInto<usize>>(val: T) -> usize {
165 val.try_into().unwrap_or_default() / 2
166 }
167
168 pub fn subtract_add_two<T: TryInto<usize>, U: TryInto<usize>>(lhs: T, rhs: U) -> usize {
169 lhs.try_into()
170 .unwrap_or_default()
171 .saturating_sub(rhs.try_into().unwrap_or_default())
172 .saturating_add(2)
173 }
174 }
175
176 #[macro_export]
177 macro_rules! basic_table_impls {
178 (impl_the_methods) => {
179 pub fn resolve_offset<O: Offset, R: FontRead<'a, Args = ()>>(
181 &self,
182 offset: O,
183 ) -> Result<R, ReadError> {
184 offset.resolve(self.data)
185 }
186
187 pub fn offset_data(&self) -> FontData<'a> {
191 self.data
192 }
193
194 #[deprecated(note = "just use the base type directly")]
200 pub fn shape(&self) -> &Self {
201 &self
202 }
203 };
204 }
205
206 pub(crate) use crate::basic_table_impls;
207}
208
209include!("../generated/font.rs");
210
211#[derive(Clone)]
212pub enum FileRef<'a> {
214 Font(FontRef<'a>),
216 Collection(CollectionRef<'a>),
218}
219
220impl<'a> FileRef<'a> {
221 pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
223 Ok(if let Ok(collection) = CollectionRef::new(data) {
224 Self::Collection(collection)
225 } else {
226 Self::Font(FontRef::new(data)?)
227 })
228 }
229
230 pub fn fonts(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
232 let (iter_one, iter_two) = match self {
233 Self::Font(font) => (Some(Ok(font.clone())), None),
234 Self::Collection(collection) => (None, Some(collection.iter())),
235 };
236 iter_two.into_iter().flatten().chain(iter_one)
237 }
238}
239
240#[derive(Clone)]
242pub struct CollectionRef<'a> {
243 data: FontData<'a>,
244 header: TTCHeader<'a>,
245}
246
247impl<'a> CollectionRef<'a> {
248 pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
250 let data = FontData::new(data);
251 let header = TTCHeader::read(data)?;
252 if header.ttc_tag() != TTC_HEADER_TAG {
253 Err(ReadError::InvalidTtc(header.ttc_tag()))
254 } else {
255 Ok(Self { data, header })
256 }
257 }
258
259 pub fn len(&self) -> u32 {
261 self.header.table_directory_offsets().len() as u32
262 }
263
264 pub fn is_empty(&self) -> bool {
266 self.len() == 0
267 }
268
269 pub fn get(&self, index: u32) -> Result<FontRef<'a>, ReadError> {
271 let offset = self
272 .header
273 .table_directory_offsets()
274 .get(index as usize)
275 .ok_or(ReadError::InvalidCollectionIndex(index))?
276 .get() as usize;
277 let table_dir_data = self.data.slice(offset..).ok_or(ReadError::OutOfBounds)?;
278 FontRef::with_table_directory(
279 self.data,
280 TableDirectory::read(table_dir_data)?,
281 Some(index),
282 )
283 }
284
285 pub fn iter(&self) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
287 let copy = self.clone();
288 (0..self.len()).map(move |ix| copy.get(ix))
289 }
290}
291
292impl TableDirectory<'_> {
293 fn is_sorted(&self) -> bool {
294 let mut last_tag = Tag::new(&[0u8; 4]);
295
296 for tag in self.table_records().iter().map(|rec| rec.tag()) {
297 if tag <= last_tag {
298 return false;
299 }
300
301 last_tag = tag;
302 }
303
304 true
305 }
306}
307
308#[derive(Clone)]
313pub struct FontRef<'a> {
314 data: FontData<'a>,
315 pub table_directory: TableDirectory<'a>,
316 ttc_index: u32,
318 in_ttc: bool,
323 table_directory_sorted: bool,
327}
328
329impl<'a> FontRef<'a> {
330 pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
339 let data = FontData::new(data);
340 Self::with_table_directory(data, TableDirectory::read(data)?, None)
341 }
342
343 pub fn from_index(data: &'a [u8], index: u32) -> Result<Self, ReadError> {
355 let file = FileRef::new(data)?;
356 match file {
357 FileRef::Font(font) => {
358 if index == 0 {
359 Ok(font)
360 } else {
361 Err(ReadError::InvalidCollectionIndex(index))
362 }
363 }
364 FileRef::Collection(collection) => collection.get(index),
365 }
366 }
367
368 pub fn data(&self) -> FontData<'a> {
373 self.data
374 }
375
376 pub fn ttc_index(&self) -> Option<u32> {
379 self.in_ttc.then_some(self.ttc_index)
380 }
381
382 pub fn table_directory(&self) -> &TableDirectory<'a> {
384 &self.table_directory
385 }
386
387 pub fn table_data(&self, tag: Tag) -> Option<FontData<'a>> {
389 let entry = if self.table_directory_sorted {
390 self.table_directory
391 .table_records()
392 .binary_search_by(|rec| rec.tag.get().cmp(&tag))
393 .ok()
394 } else {
395 self.table_directory
396 .table_records()
397 .iter()
398 .position(|rec| rec.tag.get().eq(&tag))
399 };
400
401 entry
402 .and_then(|idx| self.table_directory.table_records().get(idx))
403 .and_then(|record| {
404 let start = Offset32::new(record.offset()).non_null()?;
405 let len = record.length() as usize;
406 self.data.slice(start..start.checked_add(len)?)
407 })
408 }
409
410 pub fn fonts(
413 data: &'a [u8],
414 ) -> impl Iterator<Item = Result<FontRef<'a>, ReadError>> + 'a + Clone {
415 let count = match FileRef::new(data) {
416 Ok(FileRef::Font(_)) => 1,
417 Ok(FileRef::Collection(ttc)) => ttc.len(),
418 _ => 0,
419 };
420 (0..count).map(|idx| FontRef::from_index(data, idx))
421 }
422
423 fn with_table_directory(
424 data: FontData<'a>,
425 table_directory: TableDirectory<'a>,
426 ttc_index: Option<u32>,
427 ) -> Result<Self, ReadError> {
428 if [TT_SFNT_VERSION, CFF_SFNT_VERSION, TRUE_SFNT_VERSION]
429 .contains(&table_directory.sfnt_version())
430 {
431 let table_directory_sorted = table_directory.is_sorted();
432
433 Ok(FontRef {
434 data,
435 table_directory,
436 ttc_index: ttc_index.unwrap_or_default(),
437 in_ttc: ttc_index.is_some(),
438 table_directory_sorted,
439 })
440 } else {
441 Err(ReadError::InvalidSfnt(table_directory.sfnt_version()))
442 }
443 }
444}
445
446impl<'a> TableProvider<'a> for FontRef<'a> {
447 fn data_for_tag(&self, tag: Tag) -> Option<FontData<'a>> {
448 self.table_data(tag)
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use font_test_data::{be_buffer, bebuffer::BeBuffer, ttc::TTC, AHEM};
455 use types::{Tag, TT_SFNT_VERSION};
456
457 use crate::{FileRef, FontRef};
458
459 #[test]
460 fn file_ref_non_collection() {
461 assert!(matches!(FileRef::new(AHEM), Ok(FileRef::Font(_))));
462 }
463
464 #[test]
465 fn file_ref_collection() {
466 let Ok(FileRef::Collection(collection)) = FileRef::new(TTC) else {
467 panic!("Expected a collection");
468 };
469 assert_eq!(2, collection.len());
470 assert!(!collection.is_empty());
471 }
472
473 #[test]
474 fn font_ref_fonts_iter() {
475 assert_eq!(FontRef::fonts(AHEM).count(), 1);
476 assert_eq!(FontRef::fonts(TTC).count(), 2);
477 assert_eq!(FontRef::fonts(b"NOT_A_FONT").count(), 0);
478 }
479
480 #[test]
481 fn ttc_index() {
482 for (idx, font) in FontRef::fonts(TTC).map(|font| font.unwrap()).enumerate() {
483 assert_eq!(font.ttc_index(), Some(idx as u32));
484 }
485 assert!(FontRef::new(AHEM).unwrap().ttc_index().is_none());
486 }
487
488 #[test]
489 fn unsorted_table_directory() {
490 let cff2_data = font_test_data::cff2::EXAMPLE;
491 let post_data = font_test_data::post::SIMPLE;
492 let gdef_data = [
493 font_test_data::gdef::GDEF_HEADER,
494 font_test_data::gdef::GLYPHCLASSDEF_TABLE,
495 ]
496 .concat();
497 let gpos_data = font_test_data::gpos::SINGLEPOSFORMAT1;
498
499 let font_data = be_buffer! {
500 TT_SFNT_VERSION,
501 4u16, 64u16, 2u16, 0u16, (Tag::new(b"post")),
507 0u32, 76u32, (post_data.len() as u32),
510
511 (Tag::new(b"GPOS")),
512 0u32, 108u32, (gpos_data.len() as u32),
515
516 (Tag::new(b"GDEF")),
517 0u32, 128u32, (gdef_data.len() as u32),
520
521 (Tag::new(b"CFF2")),
522 0u32, 160u32, (cff2_data.len() as u32)
525 };
526
527 let mut full_font = font_data.to_vec();
528
529 full_font.extend_from_slice(post_data);
530 full_font.extend_from_slice(gpos_data);
531 full_font.extend_from_slice(&gdef_data);
532 full_font.extend_from_slice(cff2_data);
533
534 let font = FontRef::new(&full_font).unwrap();
535
536 assert!(!font.table_directory_sorted);
537
538 assert!(font.table_data(Tag::new(b"CFF2")).is_some());
539 assert!(font.table_data(Tag::new(b"GDEF")).is_some());
540 assert!(font.table_data(Tag::new(b"GPOS")).is_some());
541 assert!(font.table_data(Tag::new(b"post")).is_some());
542 }
543}