//! Font loading and subsetting. //! //! # Font handling //! To do the typesetting, the engine needs font data. However, to be highly portable the engine //! itself assumes nothing about the environment. To still work with fonts, the consumer of this //! library has to add _font providers_ to their typesetting instance. These can be queried for font //! data given flexible font filters specifying required font families and styles. A font provider //! is a type implementing the [`FontProvider`](crate::font::FontProvider) trait. //! //! There is one [included font provider](crate::font::FileSystemFontProvider) that serves fonts //! from a folder on the file system. use std::collections::HashMap; use std::fs::{self, File}; use std::io::{self, Cursor, Read, Seek, BufReader}; use std::path::{Path, PathBuf}; use opentype::{Error as OpentypeError, OpenTypeReader}; use opentype::tables::{Header, Name, CharMap, HorizontalMetrics, Post, OS2}; use opentype::types::{MacStyleFlags, NameEntry}; use toml::map::Map as TomlMap; use toml::value::Value as TomlValue; use self::subset::Subsetter; use crate::size::Size; mod loader; mod subset; pub use loader::{FontLoader, FontQuery}; /// A parsed _OpenType_ font program. #[derive(Debug, Clone)] pub struct Font { /// The name of the font. pub name: String, /// The complete, raw bytes of the font program. pub program: Vec, /// The mapping from character codes to glyph ids. pub mapping: HashMap, /// The widths of the glyphs indexed by glyph id. pub widths: Vec, /// The id of the fallback glyph. pub default_glyph: u16, /// The typesetting or exporting-relevant metrics of this font. pub metrics: FontMetrics, } /// Font metrics relevant to the typesetting or exporting processes. #[derive(Debug, Copy, Clone)] pub struct FontMetrics { /// Whether the font is italic. pub italic: bool, /// Whether font is monospace. pub monospace: bool, /// The angle of text in italics (in counter-clockwise degrees from vertical). pub italic_angle: f32, /// The extremal values [x_min, y_min, x_max, y_max] for all glyph bounding boxes. pub bounding_box: [Size; 4], /// The typographic ascender. pub ascender: Size, /// The typographic descender. pub descender: Size, /// The approximate height of capital letters. pub cap_height: Size, /// The weight class of the font (from 100 for thin to 900 for heavy). pub weight_class: u16, } impl Font { /// Create a `Font` from a raw font program. pub fn new(program: Vec) -> FontResult { let cursor = Cursor::new(&program); let mut reader = OpenTypeReader::new(cursor); // All of these tables are required by the OpenType specification, // so we do not really have to handle the case that they are missing. let head = reader.read_table::
()?; let name = reader.read_table::()?; let os2 = reader.read_table::()?; let cmap = reader.read_table::()?; let hmtx = reader.read_table::()?; let post = reader.read_table::()?; // Create a conversion function between font units and sizes. let font_unit_ratio = 1.0 / (head.units_per_em as f32); let font_unit_to_size = |x| Size::pt(font_unit_ratio * x); let font_name = name .get_decoded(NameEntry::PostScriptName) .unwrap_or_else(|| "unknown".to_owned()); let widths = hmtx.metrics.iter() .map(|m| font_unit_to_size(m.advance_width as f32)).collect(); let metrics = FontMetrics { italic: head.mac_style.contains(MacStyleFlags::ITALIC), monospace: post.is_fixed_pitch, italic_angle: post.italic_angle.to_f32(), bounding_box: [ font_unit_to_size(head.x_min as f32), font_unit_to_size(head.y_min as f32), font_unit_to_size(head.x_max as f32), font_unit_to_size(head.y_max as f32), ], ascender: font_unit_to_size(os2.s_typo_ascender as f32), descender: font_unit_to_size(os2.s_typo_descender as f32), cap_height: font_unit_to_size(os2.s_cap_height.unwrap_or(os2.s_typo_ascender) as f32), weight_class: os2.us_weight_class, }; Ok(Font { name: font_name, program, mapping: cmap.mapping, widths, default_glyph: os2.us_default_char.unwrap_or(0), metrics, }) } /// Encode a character into it's glyph id. #[inline] pub fn encode(&self, character: char) -> u16 { self.mapping.get(&character).map(|&g| g).unwrap_or(self.default_glyph) } /// Encode the given text into a vector of glyph ids. #[inline] pub fn encode_text(&self, text: &str) -> Vec { const BYTES_PER_GLYPH: usize = 2; let mut bytes = Vec::with_capacity(BYTES_PER_GLYPH * text.len()); for c in text.chars() { let glyph = self.encode(c); bytes.push((glyph >> 8) as u8); bytes.push((glyph & 0xff) as u8); } bytes } /// Generate a subsetted version of this font. /// /// This version includes only the given `chars` and _OpenType_ `tables`. #[inline] pub fn subsetted(&self, chars: C, tables: I) -> Result where C: IntoIterator, I: IntoIterator, S: AsRef { Subsetter::subset(self, chars, tables) } } /// A type that provides fonts. pub trait FontProvider { /// Returns a font with the given info if this provider has one. fn get(&self, info: &FontInfo) -> Option>; /// The available fonts this provider can serve. While these should generally /// be retrievable through the `get` method, this is not guaranteed. fn available<'p>(&'p self) -> &'p [FontInfo]; } /// A wrapper trait around `Read + Seek`. /// /// This type is needed because currently you can't make a trait object with two traits, like /// `Box`. Automatically implemented for all types that are [`Read`] and [`Seek`]. pub trait FontData: Read + Seek {} impl FontData for T {} /// Classifies a font by listing the font classes it is part of. /// /// All fonts with the same [`FontInfo`] are part of the same intersection /// of [font classes](FontClass). /// /// This structure can be constructed conveniently through the [`font`] macro. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct FontInfo { /// The font classes this font is part of. pub classes: Vec, } impl FontInfo { /// Create a new font info from a collection of classes. #[inline] pub fn new(classes: I) -> FontInfo where I: IntoIterator { FontInfo { classes: classes.into_iter().collect() } } } /// A class of fonts. /// /// The set of all fonts can be classified into subsets of font classes like /// _serif_ or _bold_. This enum lists such subclasses. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum FontClass { Serif, SansSerif, Monospace, Regular, Bold, Italic, /// A custom family like _Arial_ or _Times_. Family(String), } /// A macro to create [FontInfos](crate::font::FontInfo) easily. /// /// Accepts an ordered list of font classes. Strings expressions are parsed /// into custom `Family`-variants and others can be named directly. /// /// # Examples /// ``` /// # use typeset::font; /// // Noto Sans in regular typeface. /// font!["NotoSans", "Noto", Regular, SansSerif]; /// /// // Noto Serif in italics and boldface. /// font!["NotoSerif", "Noto", Bold, Italic, Serif]; /// /// // Arial in italics. /// font!["Arial", Italic, SansSerif]; /// /// // Noto Emoji, which works in sans-serif and serif contexts. /// font!["NotoEmoji", "Noto", Regular, SansSerif, Serif, Monospace]; /// ``` #[macro_export] macro_rules! font { // Parse class list one by one. (@__cls $v:expr) => {}; (@__cls $v:expr, $c:ident) => { $v.push($crate::font::FontClass::$c); }; (@__cls $v:expr, $c:ident, $($tts:tt)*) => { font!(@__cls $v, $c); font!(@__cls $v, $($tts)*) }; (@__cls $v:expr, $f:expr) => { $v.push( $crate::font::FontClass::Family($f.to_string())); }; (@__cls $v:expr, $f:expr, $($tts:tt)*) => { font!(@__cls $v, $f); font!(@__cls $v, $($tts)*) }; // Entry point ($($tts:tt)*) => {{ let mut classes = Vec::new(); font!(@__cls classes, $($tts)*); $crate::font::FontInfo { classes } }}; } /// A font provider serving fonts from a folder on the local file system. #[derive(Debug)] pub struct FileSystemFontProvider { /// The base folder all other paths are relative to. base: PathBuf, /// Paths of the fonts relative to the `base` path. paths: Vec, /// The info for the font with the same index in `paths`. infos: Vec, } impl FileSystemFontProvider { /// Create a new provider serving fonts from a base path. The `fonts` iterator /// should contain paths of fonts relative to the base alongside matching /// infos for these fonts. /// /// # Example /// Serve the two fonts `NotoSans-Regular` and `NotoSans-Italic` from the local folder /// `../fonts`. /// ``` /// # use typeset::{font::FileSystemFontProvider, font}; /// FileSystemFontProvider::new("../fonts", vec![ /// ("NotoSans-Regular.ttf", font!["NotoSans", Regular, SansSerif]), /// ("NotoSans-Italic.ttf", font!["NotoSans", Italic, SansSerif]), /// ]); /// ``` pub fn new(base: B, fonts: I) -> FileSystemFontProvider where B: Into, I: IntoIterator, P: Into, { let iter = fonts.into_iter(); // Find out how long the iterator is at least, to reserve the correct // capacity for the vectors. let min = iter.size_hint().0; let mut paths = Vec::with_capacity(min); let mut infos = Vec::with_capacity(min); for (path, info) in iter { paths.push(path.into()); infos.push(info); } FileSystemFontProvider { base: base.into(), paths, infos, } } /// Create a new provider from a font listing file. pub fn from_listing>(file: P) -> FontResult { fn inv(message: S) -> FontError { FontError::InvalidListing(message.to_string()) } let file = file.as_ref(); let base = file.parent() .ok_or_else(|| inv("expected listings file"))?; let bytes = fs::read(file)?; let map: TomlMap = toml::de::from_slice(&bytes) .map_err(|err| inv(err))?; let mut paths = Vec::new(); let mut infos = Vec::new(); for value in map.values() { if let TomlValue::Table(table) = value { // Parse the string file key. paths.push(match table.get("file") { Some(TomlValue::String(s)) => PathBuf::from(s), _ => return Err(inv("expected file name")), }); // Parse the array classes key. infos.push(if let Some(TomlValue::Array(array)) = table.get("classes") { let mut classes = Vec::with_capacity(array.len()); for class in array { classes.push(match class { TomlValue::String(class) => match class.as_str() { "Serif" => FontClass::Serif, "SansSerif" => FontClass::SansSerif, "Monospace" => FontClass::Monospace, "Regular" => FontClass::Regular, "Bold" => FontClass::Bold, "Italic" => FontClass::Italic, _ => FontClass::Family(class.to_string()), }, _ => return Err(inv("expect font class string")), }) } FontInfo { classes } } else { return Err(inv("expected font classes")); }); } else { return Err(inv("expected file/classes table")); } } Ok(FileSystemFontProvider { base: base.to_owned(), paths, infos, }) } } impl FontProvider for FileSystemFontProvider { #[inline] fn get(&self, info: &FontInfo) -> Option> { let index = self.infos.iter().position(|c| c == info)?; let path = &self.paths[index]; let full_path = self.base.join(path); let file = File::open(full_path).ok()?; Some(Box::new(BufReader::new(file)) as Box) } #[inline] fn available<'p>(&'p self) -> &'p [FontInfo] { &self.infos } } /// The error type for font operations. pub enum FontError { /// The font file is incorrect. InvalidFont(String), /// The font listing is incorrect. InvalidListing(String), /// A character requested for subsetting was not present in the source font. MissingCharacter(char), /// A requested or required table was not present. MissingTable(String), /// The table is unknown to the subsetting engine. UnsupportedTable(String), /// The font is not supported by the subsetting engine. UnsupportedFont(String), /// An I/O Error occured while reading the font program. Io(io::Error), } error_type! { err: FontError, res: FontResult, show: f => match err { FontError::InvalidFont(message) => write!(f, "invalid font: {}", message), FontError::InvalidListing(message) => write!(f, "invalid font listing: {}", message), FontError::MissingCharacter(c) => write!(f, "missing character: '{}'", c), FontError::MissingTable(table) => write!(f, "missing table: '{}'", table), FontError::UnsupportedTable(table) => write!(f, "unsupported table: {}", table), FontError::UnsupportedFont(message) => write!(f, "unsupported font: {}", message), FontError::Io(err) => write!(f, "io error: {}", err), }, source: match err { FontError::Io(err) => Some(err), _ => None, }, from: (io::Error, FontError::Io(err)), from: (OpentypeError, match err { OpentypeError::InvalidFont(message) => FontError::InvalidFont(message), OpentypeError::MissingTable(tag) => FontError::MissingTable(tag.to_string()), OpentypeError::Io(err) => FontError::Io(err), }), } #[cfg(test)] mod tests { use super::*; /// Tests the font info macro. #[test] fn font_macro() { use FontClass::*; assert_eq!(font!["NotoSans", "Noto", Regular, SansSerif], FontInfo { classes: vec![ Family("NotoSans".to_owned()), Family("Noto".to_owned()), Regular, SansSerif ] }); assert_eq!(font!["NotoSerif", Serif, Italic, "Noto"], FontInfo { classes: vec![ Family("NotoSerif".to_owned()), Serif, Italic, Family("Noto".to_owned()) ], }); } }