1use core::{fmt, mem, ops};
4
5#[cfg(feature = "woff2")]
6pub use self::woff2::Woff2Reader;
7pub(crate) use self::{
8 cmap::CmapTable,
9 fvar::FvarTable,
10 glyph::{GlyfTable, Glyph, GlyphWithMetrics},
11 head::HeadTable,
12 hhea::HheaTable,
13 hmtx::HmtxTable,
14 loca::LocaTable,
15 maxp::MaxpTable,
16 name::NameTable,
17 os2::Os2Table,
18 post::PostTable,
19 stat::StatTable,
20 types::{Cursor, OffsetFormat},
21};
22pub use self::{
23 fvar::{VariationAxis, VariationAxisTag},
24 name::FontNaming,
25 os2::{EmbeddingPermissions, FontCategory, UsagePermissions},
26 types::{Fixed, LongDateTime, TableTag},
27};
28use self::{hhea::HorizontalGlyphStats, types::BoundingBox};
29use crate::{
30 alloc::{format, BTreeSet, Box, Cow, Vec},
31 errors::{ParseError, ParseErrorKind, Warnings},
32 font::gvar::GvarTable,
33 subset::FontSubset,
34 utils::{Either, RangeConcat},
35};
36
37mod cmap;
38mod fvar;
39mod glyph;
40mod gvar;
41mod head;
42mod hhea;
43mod hmtx;
44mod loca;
45mod maxp;
46mod name;
47mod os2;
48mod post;
49mod stat;
50mod types;
51#[cfg(feature = "woff2")]
52mod woff2;
53
54#[derive(Debug, Clone)]
56#[non_exhaustive]
57pub struct FontMetrics {
58 pub units_per_em: u16,
60 pub monospace_advance_width: Option<u16>,
62 pub ascent: i16,
64 pub descent: i16,
66}
67
68#[derive(Debug, Clone)]
70pub struct OpenTypeReader<'a> {
71 tables: Vec<(TableTag, Cursor<'a>)>,
72}
73
74impl<'a> OpenTypeReader<'a> {
75 #[allow(clippy::missing_panics_doc)] #[cfg_attr(
84 feature = "tracing",
85 tracing::instrument(
86 level = "debug",
87 name = "OpenTypeReader::new",
88 err,
89 skip_all,
90 fields(bytes.len = bytes.len()),
91 )
92 )]
93 pub fn new(bytes: &'a [u8]) -> Result<Self, ParseError> {
94 let mut cursor = Cursor::new(bytes);
95 let font_bytes = bytes;
96 cursor.read_u32_checked(|sfnt_version| check_exact!(sfnt_version, Font::SFNT_VERSION))?;
97
98 let table_count = cursor.read_u16()?;
99 #[cfg(feature = "tracing")]
100 tracing::debug!(table_count, "read table count");
101
102 let expected_entry_selector = u16::try_from(table_count.ilog2()).unwrap();
103 let expected_search_range = 1 << (4 + expected_entry_selector);
104 cursor
105 .read_u16_checked(|search_range| check_exact!(search_range, expected_search_range))?;
106 cursor.read_u16_checked(|entry_selector| {
107 check_exact!(entry_selector, expected_entry_selector)
108 })?;
109 cursor.read_u16_checked(|range_shift| {
110 check_exact!(range_shift, 16 * table_count - expected_search_range)
111 })?;
112
113 let tables = (0..table_count)
114 .map(|_| Self::parse_table_record(&mut cursor, font_bytes))
115 .collect::<Result<Vec<_>, _>>()?;
116 Ok(Self { tables })
117 }
118
119 fn aligned_checksum(cursor: &Cursor<'_>) -> Result<u32, ParseError> {
120 if cursor.offset() % 4 != 0 {
121 return Err(cursor.err(ParseErrorKind::UnalignedTable));
122 }
123 Ok(Font::checksum(cursor.bytes()))
124 }
125
126 fn parse_table_record(
127 header_cursor: &mut Cursor<'_>,
128 font_bytes: &'a [u8],
129 ) -> Result<(TableTag, Cursor<'a>), ParseError> {
130 let tag = TableTag::from(header_cursor.read_u32()?);
131 let checksum = header_cursor.read_u32()?;
132 let offset = header_cursor.read_u32()? as usize;
133 let len = header_cursor.read_u32()? as usize;
134 let table_bytes = font_bytes.get(offset..(offset + len)).ok_or_else(|| {
135 header_cursor.err(ParseErrorKind::RangeOutOfBounds {
136 range: offset..(offset + len),
137 len: font_bytes.len(),
138 })
139 })?;
140 let cursor = Cursor::for_table(table_bytes, offset, tag);
141 let mut actual_checksum = Self::aligned_checksum(&cursor)?;
142 if tag == TableTag::HEAD {
143 let adjustment =
145 &table_bytes[Font::HEAD_CHECKSUM_OFFSET..Font::HEAD_CHECKSUM_OFFSET + 4];
146 let adjustment = u32::from_be_bytes(adjustment.try_into().unwrap());
147 actual_checksum = actual_checksum.wrapping_sub(adjustment);
148 }
149
150 if checksum != actual_checksum {
151 return Err(cursor.err(ParseErrorKind::Checksum {
152 expected: checksum,
153 actual: actual_checksum,
154 }));
155 }
156
157 #[cfg(feature = "tracing")]
158 tracing::debug!(?tag, checksum, offset, len, "read table record");
159
160 Ok((tag, cursor))
161 }
162
163 pub(crate) fn iter(&self) -> impl ExactSizeIterator<Item = (TableTag, Cursor<'a>)> + '_ {
165 self.tables.iter().copied()
166 }
167
168 #[cfg(test)]
169 pub(crate) fn table(&self, tag: TableTag) -> Cursor<'a> {
170 let cursor = self
171 .tables
172 .iter()
173 .find_map(|(actual_tag, cursor)| (*actual_tag == tag).then_some(*cursor));
174 cursor.unwrap_or_else(|| panic!("font does not contain `{tag}` table"))
175 }
176
177 pub fn raw_tables(&self) -> impl ExactSizeIterator<Item = (TableTag, &'a [u8])> + '_ {
179 self.tables
180 .iter()
181 .map(|(tag, cursor)| (*tag, cursor.bytes()))
182 }
183
184 pub fn read(&self) -> Result<Font<'a>, ParseError> {
190 Font::from_tables(self.iter())
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196#[non_exhaustive]
197pub enum FileFormat {
198 OpenType,
200 #[cfg(feature = "woff2")]
202 #[cfg_attr(docsrs, doc(cfg(feature = "woff2")))]
203 Woff2,
204}
205
206impl fmt::Display for FileFormat {
207 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208 formatter.write_str(match self {
209 Self::OpenType => "OpenType",
210 #[cfg(feature = "woff2")]
211 Self::Woff2 => "WOFF2",
212 })
213 }
214}
215
216#[derive(Debug, Clone)]
218#[non_exhaustive]
219pub enum FontReader<'a> {
220 OpenType(OpenTypeReader<'a>),
222 #[cfg(feature = "woff2")]
224 #[cfg_attr(docsrs, doc(cfg(feature = "woff2")))]
225 Woff2(Woff2Reader),
226}
227
228impl<'a> FontReader<'a> {
229 #[cfg_attr(
235 feature = "tracing",
236 tracing::instrument(level = "debug", name = "FontReader::new", skip_all,)
237 )]
238 pub fn new(bytes: &'a [u8]) -> Result<Self, ParseError> {
239 let format = Cursor::new(bytes).read_u32_checked(|signature| match signature {
240 Font::SFNT_VERSION => Ok(FileFormat::OpenType),
241 #[cfg(feature = "woff2")]
242 Font::WOFF2_SIGNATURE => Ok(FileFormat::Woff2),
243 _ => {
244 #[cfg(not(feature = "woff2"))]
245 let expected = format!("OpenType ({:x}) signature", Font::SFNT_VERSION);
246 #[cfg(feature = "woff2")]
247 let expected = format!(
248 "OpenType ({:x}) or WOFF2 ({:x}) signature",
249 Font::SFNT_VERSION,
250 Font::WOFF2_SIGNATURE
251 );
252
253 Err(ParseErrorKind::UnexpectedValue {
254 name: "signature",
255 expected,
256 actual: signature,
257 })
258 }
259 })?;
260 #[cfg(feature = "tracing")]
261 tracing::debug!(?format, "detected font file format");
262
263 match format {
264 FileFormat::OpenType => OpenTypeReader::new(bytes).map(Self::OpenType),
265 #[cfg(feature = "woff2")]
266 FileFormat::Woff2 => Woff2Reader::new(bytes).map(Self::Woff2),
267 }
268 }
269
270 pub fn format(&self) -> FileFormat {
272 match self {
273 Self::OpenType(_) => FileFormat::OpenType,
274 #[cfg(feature = "woff2")]
275 Self::Woff2(_) => FileFormat::Woff2,
276 }
277 }
278
279 pub fn raw_tables(&self) -> impl ExactSizeIterator<Item = (TableTag, &[u8])> + '_ {
281 #[cfg(not(feature = "woff2"))]
282 match self {
283 Self::OpenType(reader) => reader.raw_tables(),
284 }
285
286 #[cfg(feature = "woff2")]
287 match self {
288 Self::OpenType(reader) => Either::Left(reader.raw_tables()),
289 Self::Woff2(reader) => Either::Right(reader.raw_tables()),
290 }
291 }
292
293 pub fn read(&self) -> Result<Font<'_>, ParseError> {
299 match self {
300 Self::OpenType(reader) => reader.read(),
301 #[cfg(feature = "woff2")]
302 Self::Woff2(reader) => reader.read(),
303 }
304 }
305}
306
307#[derive(Debug, Clone)]
308pub(crate) struct VariableFontTables<'a> {
309 pub(crate) fvar: FvarTable<'a>,
310 pub(crate) gvar: GvarTable<'a>,
311 pub(crate) stat: StatTable<'a>,
312 pub(crate) unparsed: Vec<(TableTag, Cursor<'a>)>,
313}
314
315#[derive(Debug, Clone)]
317pub struct Font<'a> {
318 pub(crate) cmap: CmapTable<'a>,
319 pub(crate) head: HeadTable,
320 pub(crate) hhea: HheaTable,
321 pub(crate) hmtx: HmtxTable<'a>,
322 pub(crate) maxp: MaxpTable<'a>,
323 pub(crate) name: NameTable<'a>,
324 pub(crate) os2: Os2Table<'a>,
325 pub(crate) post: PostTable<'a>,
326 pub(crate) loca: LocaTable<'a>,
327 pub(crate) glyf: GlyfTable<'a>,
328 pub(crate) variable: Option<VariableFontTables<'a>>,
329 pub(crate) unparsed: Vec<(TableTag, Cursor<'a>)>,
331}
332
333impl<'a> Font<'a> {
334 pub(crate) const SFNT_VERSION: u32 = 0x_0001_0000;
335 pub(crate) const SFNT_CHECKSUM: u32 = 0x_b1b0_afba;
336 pub(crate) const SFNT_HEADER_LEN: usize = 12;
337 pub(crate) const TABLE_RECORD_LEN: usize = 16;
338
339 pub(crate) const HEAD_CHECKSUM_OFFSET: usize = 8;
341
342 pub fn opentype(bytes: &'a [u8]) -> Result<Self, ParseError> {
349 OpenTypeReader::new(bytes)?.read()
350 }
351
352 #[cfg_attr(
353 feature = "tracing",
354 tracing::instrument(level = "debug", err, skip_all)
355 )]
356 fn from_tables(
357 table_records: impl Iterator<Item = (TableTag, Cursor<'a>)>,
358 ) -> Result<Self, ParseError> {
359 let (mut cmap, mut head, mut hhea, mut maxp, mut hmtx) = (None, None, None, None, None);
360 let (mut name, mut os2, mut post, mut loca, mut glyf) = (None, None, None, None, None);
361 let (mut fvar, mut gvar, mut stat) = (None, None, None);
362 let (mut unparsed, mut unparsed_var) = (Vec::new(), Vec::new());
363 for (tag, table_cursor) in table_records {
364 match tag {
365 TableTag::CMAP => {
366 cmap = Some(CmapTable::parse(table_cursor)?);
367 }
368 TableTag::HEAD => head = Some(HeadTable::parse(table_cursor)?),
369 TableTag::HHEA => hhea = Some(HheaTable::parse(table_cursor)?),
370 TableTag::HMTX => hmtx = Some(table_cursor),
371 TableTag::MAXP => maxp = Some(MaxpTable::parse(table_cursor)?),
372 TableTag::NAME => name = Some(table_cursor),
373 TableTag::OS2 => os2 = Some(Os2Table::parse(table_cursor)?),
374 TableTag::POST => post = Some(table_cursor),
375 TableTag::LOCA => loca = Some(table_cursor),
376 TableTag::GLYF => glyf = Some(table_cursor),
377 TableTag::FVAR => fvar = Some(FvarTable::parse(table_cursor)?),
378 TableTag::GVAR => gvar = Some(table_cursor),
379 TableTag::STAT => stat = Some(table_cursor),
380 tag if tag.is_variable() => {
381 #[cfg(feature = "tracing")]
382 tracing::debug!(?tag, "unparsed variation table");
383 unparsed_var.push((tag, table_cursor));
384 }
385 _ => {
386 #[cfg(feature = "tracing")]
387 tracing::debug!(?tag, "unparsed table");
388 unparsed.push((tag, table_cursor));
389 }
390 }
391 }
392
393 let head = head.ok_or_else(|| ParseError::missing_table(TableTag::HEAD))?;
394 let maxp = maxp.ok_or_else(|| ParseError::missing_table(TableTag::MAXP))?;
395 let loca = loca.ok_or_else(|| ParseError::missing_table(TableTag::LOCA))?;
396 let loca = LocaTable::new(head.loca_format, maxp.glyph_count, loca)?;
397 let hhea = hhea.ok_or_else(|| ParseError::missing_table(TableTag::HHEA))?;
398 let hmtx = hmtx.ok_or_else(|| ParseError::missing_table(TableTag::HMTX))?;
399 let hmtx = HmtxTable::parse(hmtx, maxp.glyph_count, hhea.number_of_h_metrics)?;
400 let glyf = glyf.ok_or_else(|| ParseError::missing_table(TableTag::GLYF))?;
401 let post = post.ok_or_else(|| ParseError::missing_table(TableTag::POST))?;
402 let post = PostTable::new(post);
403
404 let name = name.ok_or_else(|| ParseError::missing_table(TableTag::NAME))?;
405 let additional_ids = fvar
406 .as_ref()
407 .map_or_else(Vec::new, FvarTable::axis_name_ids);
408 let name = NameTable::parse(name, &additional_ids)?;
409
410 let variable = if let Some(mut fvar) = fvar {
411 fvar.resolve_axe_names(&name);
412 let gvar = gvar
413 .map(|cursor| GvarTable::parse(cursor, maxp.glyph_count))
414 .ok_or_else(|| ParseError::missing_table(TableTag::GVAR))??;
415 let stat = stat
416 .map(StatTable::parse)
417 .ok_or_else(|| ParseError::missing_table(TableTag::STAT))??;
418
419 Some(VariableFontTables {
420 fvar,
421 gvar,
422 stat,
423 unparsed: unparsed_var,
424 })
425 } else {
426 None
427 };
428
429 Ok(Self {
430 cmap: cmap.ok_or_else(|| ParseError::missing_table(TableTag::CMAP))?,
431 head,
432 hhea,
433 hmtx,
434 maxp,
435 name,
436 os2: os2.ok_or_else(|| ParseError::missing_table(TableTag::OS2))?,
437 post,
438 loca,
439 glyf: GlyfTable::Parsed(glyf),
440 variable,
441 unparsed,
442 })
443 }
444
445 pub(crate) fn checksum(bytes: &[u8]) -> u32 {
446 bytes.chunks(4).fold(0_u32, |acc, chunk| {
447 debug_assert!(chunk.len() <= 4);
448 let mut u32_bytes = [0_u8; 4];
449 u32_bytes[..chunk.len()].copy_from_slice(chunk);
450 acc.wrapping_add(u32::from_be_bytes(u32_bytes))
451 })
452 }
453
454 pub fn naming(&self) -> FontNaming<'_> {
456 self.name.parsed()
457 }
458
459 pub fn permissions(&self) -> UsagePermissions {
461 self.os2.usage_permissions
462 }
463
464 pub fn created_at(&self) -> LongDateTime {
466 self.head.created
467 }
468
469 pub fn modified_at(&self) -> LongDateTime {
471 self.head.modified
472 }
473
474 pub fn category(&self) -> FontCategory {
476 self.os2.category()
477 }
478
479 pub fn metrics(&self) -> FontMetrics {
481 FontMetrics {
482 units_per_em: self.head.units_per_em,
483 ascent: self.hhea.ascender,
484 descent: self.hhea.descender,
485 monospace_advance_width: self.hmtx.monospace_advance(),
486 }
487 }
488
489 pub fn is_variable(&self) -> bool {
492 self.variable.is_some()
493 }
494
495 pub fn variation_axes(&self) -> Option<&[VariationAxis]> {
497 Some(self.variable.as_ref()?.fvar.axes())
498 }
499
500 pub(crate) fn map_char(&self, ch: char) -> Result<u16, ParseError> {
501 self.cmap.map_char(ch)
502 }
503
504 pub fn contains_char(&self, ch: char) -> bool {
506 self.cmap.map_char(ch).is_ok_and(|glyph_id| glyph_id != 0)
507 }
508
509 pub fn char_ranges(&self) -> impl Iterator<Item = ops::RangeInclusive<char>> + '_ {
511 RangeConcat::new(self.cmap.char_ranges())
512 }
513
514 pub fn glyph_count(&self) -> usize {
516 self.maxp.glyph_count.into()
517 }
518
519 pub(crate) fn glyph(&self, glyph_idx: u16) -> Result<GlyphWithMetrics<'a>, ParseError> {
520 match &self.glyf {
521 GlyfTable::Parsed(cursor) => {
522 let range = self.loca.glyph_range(glyph_idx)?;
523 let raw = cursor.read_range(range)?;
524 let inner = Glyph::new(raw)?;
525 let (advance, lsb) = self.hmtx.advance_and_lsb(glyph_idx)?;
526 Ok(GlyphWithMetrics {
527 inner,
528 advance,
529 lsb,
530 })
531 }
532 GlyfTable::Subset(glyphs) => Ok(glyphs[usize::from(glyph_idx)].clone()),
533 }
534 }
535
536 fn all_glyphs(
537 &self,
538 ) -> impl Iterator<Item = Result<Cow<'_, GlyphWithMetrics<'a>>, ParseError>> + '_ {
539 match &self.glyf {
540 &GlyfTable::Parsed(cursor) => {
541 Either::Left(self.loca.all_ranges().zip(self.hmtx.iter()).map(
542 move |(range, (advance, lsb))| {
543 let raw = cursor.read_range(range)?;
544 Ok(Cow::Owned(GlyphWithMetrics {
545 inner: Glyph::new(raw)?,
546 advance,
547 lsb,
548 }))
549 },
550 ))
551 }
552 GlyfTable::Subset(glyphs) => {
553 Either::Right(glyphs.iter().map(|glyph| Ok(Cow::Borrowed(glyph))))
554 }
555 }
556 }
557
558 pub fn drop_variation(&mut self) {
560 self.variable = None;
561 }
562
563 pub fn validate(&self) -> Result<Warnings, ParseError> {
570 let mut bounding_box = BoundingBox {
571 x_min: i16::MAX,
572 y_min: i16::MAX,
573 x_max: i16::MIN,
574 y_max: i16::MIN,
575 };
576 let mut horizontal_stats = HorizontalGlyphStats::default();
577
578 for glyph in self.all_glyphs() {
579 let glyph = glyph?;
580 if let Some(bbox) = glyph.inner.bounding_box() {
581 bounding_box = bounding_box.union(bbox);
582 }
583 horizontal_stats.update(&glyph);
584 }
585
586 let mut warnings = Warnings::empty();
587 {
589 let mut warnings = warnings.for_table(TableTag::HEAD);
590 warnings.check_match("x_min", bounding_box.x_min, self.head.bounding_box.x_min);
591 warnings.check_match("y_min", bounding_box.y_min, self.head.bounding_box.y_min);
592 warnings.check_match("x_max", bounding_box.x_max, self.head.bounding_box.x_max);
593 warnings.check_match("y_max", bounding_box.y_max, self.head.bounding_box.y_max);
594 }
595
596 {
598 let mut warnings = warnings.for_table(TableTag::OS2);
599 let actual_range = self.cmap.char_range();
600 let computed_first_char = u16::try_from(*actual_range.start()).unwrap_or(u16::MAX);
601 warnings.check_match(
602 "first_char_index",
603 computed_first_char,
604 self.os2.first_char_index,
605 );
606 let computed_last_char = u16::try_from(*actual_range.end()).unwrap_or(u16::MAX);
607 warnings.check_match(
608 "last_char_index",
609 computed_last_char,
610 self.os2.last_char_index,
611 );
612 }
613
614 {
616 let mut warnings = warnings.for_table(TableTag::HHEA);
617 warnings.check_match(
618 "advance_width_max",
619 horizontal_stats.advance_width_max,
620 self.hhea.advance_width_max,
621 );
622 warnings.check_match(
623 "x_max_extent",
624 horizontal_stats.x_max_extent,
625 self.hhea.x_max_extent,
626 );
627 warnings.check_match(
628 "min_left_side_bearing",
629 horizontal_stats.min_left_side_bearing,
630 self.hhea.min_left_side_bearing,
631 );
632 warnings.check_match(
633 "min_right_side_bearing",
634 horizontal_stats.min_right_side_bearing,
635 self.hhea.min_right_side_bearing,
636 );
637 }
638
639 Ok(warnings)
640 }
641
642 pub fn subset(&self, chars: &BTreeSet<char>) -> Result<Self, ParseError> {
648 FontSubset::subset(self, chars)
649 }
650}
651
652pub struct OwnedFont {
654 font: Font<'static>,
655 _bytes: Box<[u8]>,
657}
658
659impl fmt::Debug for OwnedFont {
660 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
661 formatter
662 .debug_tuple("OwnedFont")
663 .field(&self.font)
664 .finish_non_exhaustive()
665 }
666}
667
668impl OwnedFont {
669 pub fn new(bytes: Box<[u8]>) -> Result<Self, ParseError> {
675 let font_reader = FontReader::new(&bytes)?;
676 let font: Font<'_> = font_reader.read()?;
677 let font: Font<'static> = unsafe {
678 mem::transmute(font)
686 };
687
688 let bytes = match font_reader {
689 FontReader::OpenType(_) => bytes,
690 #[cfg(feature = "woff2")]
691 FontReader::Woff2(reader) => reader.into_table_data().into(),
692 };
693
694 Ok(Self {
695 font,
696 _bytes: bytes,
697 })
698 }
699
700 pub fn get(&self) -> &Font<'_> {
702 &self.font
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use std::collections::HashSet;
709
710 use allsorts::{binary::read::ReadScope, font::MatchingPresentation, font_data::FontData};
711 use test_casing::test_casing;
712
713 use super::*;
714 use crate::{testonly::TestFont, WarningKind};
715
716 #[test_casing(5, TestFont::ALL)]
717 fn reading_font(font: TestFont) {
718 let parsed_font = Font::opentype(font.bytes).unwrap();
719
720 let font_file = ReadScope::new(font.bytes).read::<FontData>().unwrap();
721 let font_provider = font_file.table_provider(0).unwrap();
722 let mut reference_font = allsorts::Font::new(font_provider).unwrap();
723
724 let char_count = parsed_font
725 .char_ranges()
726 .map(Iterator::count)
727 .sum::<usize>();
728 assert!(char_count > 100, "{char_count}");
729
730 for ch in parsed_font.char_ranges().flatten() {
731 assert!(parsed_font.contains_char(ch));
732
733 let glyph_id = parsed_font.map_char(ch).unwrap();
734 let (expected_id, _) =
735 reference_font.lookup_glyph_index(ch, MatchingPresentation::NotRequired, None);
736 assert_eq!(glyph_id, expected_id);
737 }
738
739 for range in parsed_font.char_ranges() {
740 if let Some(prev) = (char::MIN..*range.start()).next_back() {
741 assert!(!parsed_font.contains_char(prev));
742 }
743 if let Some(ch) = (*range.end()..).nth(1) {
744 assert!(!parsed_font.contains_char(ch));
745 }
746 }
747 }
748
749 #[test_casing(5, TestFont::ALL)]
750 fn parsing_permissions(font: TestFont) {
751 let font = Font::opentype(font.bytes).unwrap();
752 let permissions = font.permissions();
753 assert!(permissions.embedding.is_lenient());
754 assert!(!permissions.embed_only_bitmaps);
755 assert!(permissions.allow_subsetting);
756 }
757
758 #[test]
759 fn parsing_name_table() {
760 let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
761 let naming = font.naming();
762
763 assert_eq!(naming.family, Some("Fira Mono"));
764 assert_eq!(naming.subfamily, Some("Regular"));
765 assert_eq!(
766 naming.manufacturer,
767 Some("Carrois Corporate GbR & Edenspiekermann AG")
768 );
769 assert_eq!(
770 naming.designer,
771 Some("Carrois Corporate & Edenspiekermann AG")
772 );
773 assert_eq!(naming.designer_url, Some("http://www.carrois.com"));
774 assert_eq!(
775 naming.license,
776 Some("Licensed under the Open Font License, version 1.1 or later")
777 );
778 assert_eq!(naming.license_url, Some("http://scripts.sil.org/OFL"));
779 assert_eq!(
780 naming.copyright_notice,
781 Some(
782 "Digitized data copyright © 2012-2014, The Mozilla Foundation and Telefonica S.A."
783 )
784 );
785 }
786
787 #[test]
788 fn reading_metrics_for_fira_mono() {
789 let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
790 let metrics = font.metrics();
791 assert_eq!(metrics.units_per_em, 1_000);
792 assert_eq!(metrics.ascent, 1_050);
793 assert_eq!(metrics.descent, -350);
794 assert_eq!(metrics.monospace_advance_width, Some(600));
795 }
796
797 #[test]
798 fn reading_metrics_for_roboto_mono() {
799 let font = Font::opentype(TestFont::ROBOTO_MONO.bytes).unwrap();
800 let metrics = font.metrics();
801 assert_eq!(metrics.units_per_em, 2_048);
802 assert_eq!(metrics.ascent, 2_146);
803 assert_eq!(metrics.descent, -555);
804 assert_eq!(metrics.monospace_advance_width, Some(1_229));
805 }
806
807 #[test]
808 fn reading_metrics_for_roboto() {
809 let font = Font::opentype(TestFont::ROBOTO.bytes).unwrap();
810 let metrics = font.metrics();
811 assert_eq!(metrics.units_per_em, 2_048);
812 assert_eq!(metrics.ascent, 1_900);
813 assert_eq!(metrics.descent, -500);
814 assert_eq!(metrics.monospace_advance_width, None);
815 }
816
817 #[test]
818 fn getting_font_category() {
819 let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
820 assert_eq!(font.category(), FontCategory::Regular);
821 let font = Font::opentype(TestFont::FIRA_MONO_BOLD.bytes).unwrap();
822 assert_eq!(font.category(), FontCategory::Bold);
823 let font = Font::opentype(TestFont::ROBOTO_MONO_ITALIC.bytes).unwrap();
824 assert_eq!(font.category(), FontCategory::Italic);
825 }
826
827 #[test_casing(5, TestFont::ALL)]
828 fn validating_font(font: TestFont) {
829 let font = Font::opentype(font.bytes).unwrap();
830 font.validate().unwrap().into_result().unwrap();
831 }
832
833 #[test]
834 fn validating_font_with_mutations() {
835 let font = Font::opentype(TestFont::FIRA_MONO.bytes).unwrap();
836
837 let mut bogus_font = font.clone();
838 bogus_font.head.bounding_box.x_min -= 1;
839 bogus_font.head.bounding_box.y_max += 1;
840
841 let warnings = bogus_font
842 .validate()
843 .unwrap()
844 .into_result()
845 .expect_err("no warnings");
846 assert_eq!(warnings.len(), 2);
847 let field_names = warnings.iter().map(|warn| {
848 assert_eq!(warn.table(), Some(TableTag::HEAD));
849 match warn.kind() {
850 WarningKind::ValueMismatch { name, .. } => *name,
851 }
852 });
853 let field_names: HashSet<_> = field_names.collect();
854 assert_eq!(field_names, HashSet::from(["x_min", "y_max"]));
855
856 let mut bogus_font = font.clone();
857 bogus_font.os2.first_char_index = 0x7f;
858 bogus_font.os2.last_char_index = 0x7fff;
859 bogus_font.hhea.min_right_side_bearing += 1;
860
861 let warnings = bogus_font
862 .validate()
863 .unwrap()
864 .into_result()
865 .expect_err("no warnings");
866 assert_eq!(warnings.len(), 3);
867 let field_names = warnings.iter().map(|warn| match warn.kind() {
868 WarningKind::ValueMismatch { name, .. } => (warn.table().unwrap(), *name),
869 });
870 let fields: HashSet<_> = field_names.collect();
871 assert_eq!(
872 fields,
873 HashSet::from([
874 (TableTag::OS2, "first_char_index"),
875 (TableTag::OS2, "last_char_index"),
876 (TableTag::HHEA, "min_right_side_bearing"),
877 ])
878 );
879
880 warnings.into_result().unwrap_err();
881 }
882}