diff options
Diffstat (limited to 'src/font/mod.rs')
| -rw-r--r-- | src/font/mod.rs | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/src/font/mod.rs b/src/font/mod.rs new file mode 100644 index 00000000..0c41690c --- /dev/null +++ b/src/font/mod.rs @@ -0,0 +1,387 @@ +//! Font loading and transforming. +//! +//! # Font handling +//! To do the typesetting, the typesetting engine needs font data. 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::File; +use std::io::{self, Cursor, Read, Seek, BufReader}; +use std::path::PathBuf; + +use opentype::{Error as OpentypeError, OpenTypeReader}; +use opentype::tables::{Header, Name, CharMap, HorizontalMetrics, Post, OS2}; +use opentype::types::{MacStyleFlags, NameEntry}; + +pub use self::loader::{FontLoader, FontQuery}; +use self::subset::Subsetter; +use crate::size::Size; + +mod loader; +mod subset; + + +/// A loaded and parsed font program. +#[derive(Debug, Clone)] +pub struct Font { + /// The base name of the font. + pub name: String, + /// The raw bytes of the font program. + pub program: Vec<u8>, + /// A mapping from character codes to glyph ids. + pub mapping: HashMap<char, u16>, + /// The widths of the glyphs indexed by glyph id. + pub widths: Vec<Size>, + /// The fallback glyph. + pub default_glyph: u16, + /// The typesetting-relevant metrics of this font. + pub metrics: FontMetrics, +} + +impl Font { + /// Create a new font from a raw font program. + pub fn new(program: Vec<u8>) -> FontResult<Font> { + // Create an OpentypeReader to parse the font tables. + let cursor = Cursor::new(&program); + let mut reader = OpenTypeReader::new(cursor); + + // Read the relevant tables + // (all of these are required by the OpenType specification, so we expect them). + let head = reader.read_table::<Header>()?; + let name = reader.read_table::<Name>()?; + let os2 = reader.read_table::<OS2>()?; + let cmap = reader.read_table::<CharMap>()?; + let hmtx = reader.read_table::<HorizontalMetrics>()?; + let post = reader.read_table::<Post>()?; + + // 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 as f32); + + // Find out the name of the font. + let font_name = name.get_decoded(NameEntry::PostScriptName) + .unwrap_or_else(|| "unknown".to_owned()); + + // Convert the widths from font units to sizes. + let widths = hmtx.metrics.iter().map(|m| font_unit_to_size(m.advance_width)).collect(); + + // Calculate the typesetting-relevant metrics. + 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), + font_unit_to_size(head.y_min), + font_unit_to_size(head.x_max), + font_unit_to_size(head.y_max), + ], + ascender: font_unit_to_size(os2.s_typo_ascender), + descender: font_unit_to_size(os2.s_typo_descender), + cap_height: font_unit_to_size(os2.s_cap_height.unwrap_or(os2.s_typo_ascender)), + 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, + }) + } + + /// Map a character to it's glyph index. + #[inline] + pub fn map(&self, c: char) -> u16 { + self.mapping.get(&c).map(|&g| g).unwrap_or(self.default_glyph) + } + + /// Encode the given text for this font (into glyph ids). + #[inline] + pub fn encode(&self, text: &str) -> Vec<u8> { + // Each glyph id takes two bytes that we encode in big endian. + let mut bytes = Vec::with_capacity(2 * text.len()); + for glyph in text.chars().map(|c| self.map(c)) { + bytes.push((glyph >> 8) as u8); + bytes.push((glyph & 0xff) as u8); + } + bytes + } + + /// Generate a subsetted version of this font including only the chars listed in `chars`. + /// + /// All needed tables will be included (returning an error if a table was not present in the + /// source font) and optional tables will be included if they were present in the source font. + /// All other tables will be dropped. + #[inline] + pub fn subsetted<C, I, S>(&self, chars: C, needed_tables: I, optional_tables: I) + -> Result<Font, FontError> + where + C: IntoIterator<Item=char>, + I: IntoIterator<Item=S>, + S: AsRef<str> + { + Subsetter::subset(self, chars, needed_tables, optional_tables) + } +} + +/// 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. + pub italic_angle: f32, + /// The glyph bounding box: [x_min, y_min, x_max, y_max], + pub bounding_box: [Size; 4], + /// The typographics ascender. + pub ascender: Size, + /// The typographics descender. + pub descender: Size, + /// The approximate height of capital letters. + pub cap_height: Size, + /// The weight class of the font. + pub weight_class: u16, +} + +/// Categorizes a font. +/// +/// Can be constructed conveniently with the [`font`] macro. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FontInfo { + /// The font families this font is part of. + pub classes: Vec<FontClass>, +} + +impl FontInfo { + /// Create a new font info from an iterator of classes. + pub fn new<I>(classes: I) -> FontInfo where I: IntoIterator<Item=FontClass> { + FontInfo { classes: classes.into_iter().collect() } + } +} + +/// A class of fonts. +#[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 +/// The font _Noto Sans_ in regular typeface. +/// ``` +/// # use typeset::font; +/// font!["NotoSans", "Noto", Regular, SansSerif]; +/// ``` +/// +/// The font _Noto Serif_ in italics and boldface. +/// ``` +/// # use typeset::font; +/// font!["NotoSerif", "Noto", Bold, Italic, Serif]; +/// ``` +/// +/// The font _Arial_ in italics. +/// ``` +/// # use typeset::font; +/// font!["Arial", Italic, SansSerif]; +/// ``` +/// +/// The font _Noto Emoji_, which works with all base families. 🙂 +/// ``` +/// # use typeset::font; +/// 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 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<Box<dyn FontData>>; + + /// The available fonts this provider can serve. While these should generally be retrievable + /// through the `get` method, it does not have to be guaranteed that a font info, that is + /// contained, here yields a `Some` value when passed into `get`. + 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<dyn Read + Seek>`. Automatically implemented for all types that are [`Read`] and [`Seek`]. +pub trait FontData: Read + Seek {} +impl<T> FontData for T where T: Read + Seek {} + +/// A font provider serving fonts from a folder on the local file system. +#[derive(Debug)] +pub struct FileSystemFontProvider { + /// The root folder. + base: PathBuf, + /// Paths of the fonts relative to the `base` path. + paths: Vec<PathBuf>, + /// The information for the font with the same index in `paths`. + infos: Vec<FontInfo>, +} + +impl FileSystemFontProvider { + /// Create a new provider from a folder and an iterator of pairs of font paths and font infos. + /// + /// # 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]), + /// ]); + /// ``` + #[inline] + pub fn new<B, I, P>(base: B, infos: I) -> FileSystemFontProvider + where + B: Into<PathBuf>, + I: IntoIterator<Item = (P, FontInfo)>, + P: Into<PathBuf>, + { + // Find out how long the iterator is at least, to reserve the correct capacity for the + // vectors. + let iter = infos.into_iter(); + let min = iter.size_hint().0; + + // Split the iterator into two seperated vectors. + 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, + } + } +} + +impl FontProvider for FileSystemFontProvider { + #[inline] + fn get(&self, info: &FontInfo) -> Option<Box<dyn FontData>> { + // Find the index of the font in both arrays (early exit if there is no match). + let index = self.infos.iter().position(|i| i == info)?; + + // Open the file and return a boxed reader operating on it. + let path = &self.paths[index]; + let file = File::open(self.base.join(path)).ok()?; + Some(Box::new(BufReader::new(file)) as Box<FontData>) + } + + #[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), + /// A requested table was not present in the source font. + MissingTable(String), + /// The table is unknown to the subsetting engine. + UnsupportedTable(String), + /// A character requested for subsetting was not present in the source font. + MissingCharacter(char), + /// 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::MissingTable(table) => write!(f, "missing table: {}", table), + FontError::UnsupportedTable(table) => write!(f, "unsupported table: {}", table), + FontError::MissingCharacter(c) => write!(f, "missing character: '{}'", c), + 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), + _ => panic!("unexpected extensible variant"), + }), +} + + +#[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()) + ], + }); + } +} |
