summaryrefslogtreecommitdiff
path: root/src/font
diff options
context:
space:
mode:
Diffstat (limited to 'src/font')
-rw-r--r--src/font/book.rs546
-rw-r--r--src/font/mod.rs249
-rw-r--r--src/font/variant.rs270
3 files changed, 0 insertions, 1065 deletions
diff --git a/src/font/book.rs b/src/font/book.rs
deleted file mode 100644
index 2b7742bf..00000000
--- a/src/font/book.rs
+++ /dev/null
@@ -1,546 +0,0 @@
-use std::cmp::Reverse;
-use std::collections::BTreeMap;
-
-use serde::{Deserialize, Serialize};
-use ttf_parser::{name_id, PlatformId, Tag};
-use unicode_segmentation::UnicodeSegmentation;
-
-use super::{Font, FontStretch, FontStyle, FontVariant, FontWeight};
-
-/// Metadata about a collection of fonts.
-#[derive(Default, Clone, Hash)]
-pub struct FontBook {
- /// Maps from lowercased family names to font indices.
- families: BTreeMap<String, Vec<usize>>,
- /// Metadata about each font in the collection.
- infos: Vec<FontInfo>,
-}
-
-impl FontBook {
- /// Create a new, empty font book.
- pub fn new() -> Self {
- Self { families: BTreeMap::new(), infos: vec![] }
- }
-
- /// Create a font book for a collection of fonts.
- pub fn from_fonts<'a>(fonts: impl IntoIterator<Item = &'a Font>) -> Self {
- let mut book = Self::new();
- for font in fonts {
- book.push(font.info().clone());
- }
- book
- }
-
- /// Insert metadata into the font book.
- pub fn push(&mut self, info: FontInfo) {
- let index = self.infos.len();
- let family = info.family.to_lowercase();
- self.families.entry(family).or_default().push(index);
- self.infos.push(info);
- }
-
- /// Get the font info for the given index.
- pub fn info(&self, index: usize) -> Option<&FontInfo> {
- self.infos.get(index)
- }
-
- /// An ordered iterator over all font families this book knows and details
- /// about the fonts that are part of them.
- pub fn families(
- &self,
- ) -> impl Iterator<Item = (&str, impl Iterator<Item = &FontInfo>)> + '_ {
- // Since the keys are lowercased, we instead use the family field of the
- // first face's info.
- self.families.values().map(|ids| {
- let family = self.infos[ids[0]].family.as_str();
- let infos = ids.iter().map(|&id| &self.infos[id]);
- (family, infos)
- })
- }
-
- /// Try to find a font from the given `family` that matches the given
- /// `variant` as closely as possible.
- ///
- /// The `family` should be all lowercase.
- pub fn select(&self, family: &str, variant: FontVariant) -> Option<usize> {
- let ids = self.families.get(family)?;
- self.find_best_variant(None, variant, ids.iter().copied())
- }
-
- /// Iterate over all variants of a family.
- pub fn select_family(&self, family: &str) -> impl Iterator<Item = usize> + '_ {
- self.families
- .get(family)
- .map(|vec| vec.as_slice())
- .unwrap_or_default()
- .iter()
- .copied()
- }
-
- /// Try to find and load a fallback font that
- /// - is as close as possible to the font `like` (if any)
- /// - is as close as possible to the given `variant`
- /// - is suitable for shaping the given `text`
- pub fn select_fallback(
- &self,
- like: Option<&FontInfo>,
- variant: FontVariant,
- text: &str,
- ) -> Option<usize> {
- // Find the fonts that contain the text's first char ...
- let c = text.chars().next()?;
- let ids = self
- .infos
- .iter()
- .enumerate()
- .filter(|(_, info)| info.coverage.contains(c as u32))
- .map(|(index, _)| index);
-
- // ... and find the best variant among them.
- self.find_best_variant(like, variant, ids)
- }
-
- /// Find the font in the passed iterator that
- /// - is closest to the font `like` (if any)
- /// - is closest to the given `variant`
- ///
- /// To do that we compute a key for all variants and select the one with the
- /// minimal key. This key prioritizes:
- /// - If `like` is some other font:
- /// - Are both fonts (not) monospaced?
- /// - Do both fonts (not) have serifs?
- /// - How many words do the families share in their prefix? E.g. "Noto
- /// Sans" and "Noto Sans Arabic" share two words, whereas "IBM Plex
- /// Arabic" shares none with "Noto Sans", so prefer "Noto Sans Arabic"
- /// if `like` is "Noto Sans". In case there are two equally good
- /// matches, we prefer the shorter one because it is less special (e.g.
- /// if `like` is "Noto Sans Arabic", we prefer "Noto Sans" over "Noto
- /// Sans CJK HK".)
- /// - The style (normal / italic / oblique). If we want italic or oblique
- /// but it doesn't exist, the other one of the two is still better than
- /// normal.
- /// - The absolute distance to the target stretch.
- /// - The absolute distance to the target weight.
- fn find_best_variant(
- &self,
- like: Option<&FontInfo>,
- variant: FontVariant,
- ids: impl IntoIterator<Item = usize>,
- ) -> Option<usize> {
- let mut best = None;
- let mut best_key = None;
-
- for id in ids {
- let current = &self.infos[id];
- let key = (
- like.map(|like| {
- (
- current.flags.contains(FontFlags::MONOSPACE)
- != like.flags.contains(FontFlags::MONOSPACE),
- current.flags.contains(FontFlags::SERIF)
- != like.flags.contains(FontFlags::SERIF),
- Reverse(shared_prefix_words(&current.family, &like.family)),
- current.family.len(),
- )
- }),
- current.variant.style.distance(variant.style),
- current.variant.stretch.distance(variant.stretch),
- current.variant.weight.distance(variant.weight),
- );
-
- if best_key.map_or(true, |b| key < b) {
- best = Some(id);
- best_key = Some(key);
- }
- }
-
- best
- }
-}
-
-/// Properties of a single font.
-#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
-pub struct FontInfo {
- /// The typographic font family this font is part of.
- pub family: String,
- /// Properties that distinguish this font from other fonts in the same
- /// family.
- pub variant: FontVariant,
- /// Properties of the font.
- pub flags: FontFlags,
- /// The unicode coverage of the font.
- pub coverage: Coverage,
-}
-
-bitflags::bitflags! {
- /// Bitflags describing characteristics of a font.
- #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
- #[derive(Serialize, Deserialize)]
- #[serde(transparent)]
- pub struct FontFlags: u32 {
- /// All glyphs have the same width.
- const MONOSPACE = 1 << 0;
- /// Glyphs have short strokes at their stems.
- const SERIF = 1 << 1;
- }
-}
-
-impl FontInfo {
- /// Compute metadata for all fonts in the given data.
- pub fn iter(data: &[u8]) -> impl Iterator<Item = FontInfo> + '_ {
- let count = ttf_parser::fonts_in_collection(data).unwrap_or(1);
- (0..count).filter_map(move |index| {
- let ttf = ttf_parser::Face::parse(data, index).ok()?;
- Self::from_ttf(&ttf)
- })
- }
-
- /// Compute metadata for a single ttf-parser face.
- pub(super) fn from_ttf(ttf: &ttf_parser::Face) -> Option<Self> {
- // We cannot use Name ID 16 "Typographic Family", because for some
- // fonts it groups together more than just Style / Weight / Stretch
- // variants (e.g. Display variants of Noto fonts) and then some
- // variants become inaccessible from Typst. And even though the
- // fsSelection bit WWS should help us decide whether that is the
- // case, it's wrong for some fonts (e.g. for certain variants of "Noto
- // Sans Display").
- //
- // So, instead we use Name ID 1 "Family" and trim many common
- // suffixes for which know that they just describe styling (e.g.
- // "ExtraBold").
- //
- // Also, for Noto fonts we use Name ID 4 "Full Name" instead,
- // because Name ID 1 "Family" sometimes contains "Display" and
- // sometimes doesn't for the Display variants and that mixes things
- // up.
- let family = {
- let mut family = find_name(ttf, name_id::FAMILY)?;
- if family.starts_with("Noto")
- || family.starts_with("NewCM")
- || family.starts_with("NewComputerModern")
- {
- family = find_name(ttf, name_id::FULL_NAME)?;
- }
- typographic_family(&family).to_string()
- };
-
- let variant = {
- let mut full = find_name(ttf, name_id::FULL_NAME).unwrap_or_default();
- full.make_ascii_lowercase();
-
- // Some fonts miss the relevant bits for italic or oblique, so
- // we also try to infer that from the full name.
- let italic = ttf.is_italic() || full.contains("italic");
- let oblique =
- ttf.is_oblique() || full.contains("oblique") || full.contains("slanted");
-
- let style = match (italic, oblique) {
- (false, false) => FontStyle::Normal,
- (true, _) => FontStyle::Italic,
- (_, true) => FontStyle::Oblique,
- };
-
- let weight = {
- let mut number = ttf.weight().to_number();
- if (family.starts_with("NewCM")
- || family.starts_with("New Computer Modern"))
- && full.contains("book")
- {
- number += 50;
- }
- FontWeight::from_number(number)
- };
-
- let stretch = FontStretch::from_number(ttf.width().to_number());
- FontVariant { style, weight, stretch }
- };
-
- // Determine the unicode coverage.
- let mut codepoints = vec![];
- for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
- if subtable.is_unicode() {
- subtable.codepoints(|c| codepoints.push(c));
- }
- }
-
- let mut flags = FontFlags::empty();
- flags.set(FontFlags::MONOSPACE, ttf.is_monospaced());
-
- // Determine whether this is a serif or sans-serif font.
- if let Some(panose) = ttf
- .raw_face()
- .table(Tag::from_bytes(b"OS/2"))
- .and_then(|os2| os2.get(32..45))
- {
- if matches!(panose, [2, 2..=10, ..]) {
- flags.insert(FontFlags::SERIF);
- }
- }
-
- Some(FontInfo {
- family,
- variant,
- flags,
- coverage: Coverage::from_vec(codepoints),
- })
- }
-}
-
-/// Try to find and decode the name with the given id.
-pub(super) fn find_name(ttf: &ttf_parser::Face, name_id: u16) -> Option<String> {
- ttf.names().into_iter().find_map(|entry| {
- if entry.name_id == name_id {
- if let Some(string) = entry.to_string() {
- return Some(string);
- }
-
- if entry.platform_id == PlatformId::Macintosh && entry.encoding_id == 0 {
- return Some(decode_mac_roman(entry.name));
- }
- }
-
- None
- })
-}
-
-/// Decode mac roman encoded bytes into a string.
-fn decode_mac_roman(coded: &[u8]) -> String {
- #[rustfmt::skip]
- const TABLE: [char; 128] = [
- 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è',
- 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü',
- '†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø',
- '∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø',
- '¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{a0}', 'À', 'Ã', 'Õ', 'Œ', 'œ',
- '–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '€', '‹', '›', 'fi', 'fl',
- '‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô',
- '\u{f8ff}', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ',
- ];
-
- fn char_from_mac_roman(code: u8) -> char {
- if code < 128 {
- code as char
- } else {
- TABLE[(code - 128) as usize]
- }
- }
-
- coded.iter().copied().map(char_from_mac_roman).collect()
-}
-
-/// Trim style naming from a family name and fix bad names.
-fn typographic_family(mut family: &str) -> &str {
- // Separators between names, modifiers and styles.
- const SEPARATORS: [char; 3] = [' ', '-', '_'];
-
- // Modifiers that can appear in combination with suffixes.
- const MODIFIERS: &[&str] =
- &["extra", "ext", "ex", "x", "semi", "sem", "sm", "demi", "dem", "ultra"];
-
- // Style suffixes.
- #[rustfmt::skip]
- const SUFFIXES: &[&str] = &[
- "normal", "italic", "oblique", "slanted",
- "thin", "th", "hairline", "light", "lt", "regular", "medium", "med",
- "md", "bold", "bd", "demi", "extb", "black", "blk", "bk", "heavy",
- "narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp"
- ];
-
- let mut extra = [].as_slice();
- let newcm = family.starts_with("NewCM") || family.starts_with("NewComputerModern");
- if newcm {
- extra = &["book"];
- }
-
- // Trim spacing and weird leading dots in Apple fonts.
- family = family.trim().trim_start_matches('.');
-
- // Lowercase the string so that the suffixes match case-insensitively.
- let lower = family.to_ascii_lowercase();
- let mut len = usize::MAX;
- let mut trimmed = lower.as_str();
-
- // Trim style suffixes repeatedly.
- while trimmed.len() < len {
- len = trimmed.len();
-
- // Find style suffix.
- let mut t = trimmed;
- let mut shortened = false;
- while let Some(s) = SUFFIXES.iter().chain(extra).find_map(|s| t.strip_suffix(s)) {
- shortened = true;
- t = s;
- }
-
- if !shortened {
- break;
- }
-
- // Strip optional separator.
- if let Some(s) = t.strip_suffix(SEPARATORS) {
- trimmed = s;
- t = s;
- }
-
- // Also allow an extra modifier, but apply it only if it is separated it
- // from the text before it (to prevent false positives).
- if let Some(t) = MODIFIERS.iter().find_map(|s| t.strip_suffix(s)) {
- if let Some(stripped) = t.strip_suffix(SEPARATORS) {
- trimmed = stripped;
- }
- }
- }
-
- // Apply style suffix trimming.
- family = &family[..len];
-
- if newcm {
- family = family.trim_end_matches("10");
- }
-
- // Fix bad names.
- match family {
- "Noto Sans Symbols2" => "Noto Sans Symbols 2",
- "NewComputerModern" => "New Computer Modern",
- "NewComputerModernMono" => "New Computer Modern Mono",
- "NewComputerModernSans" => "New Computer Modern Sans",
- "NewComputerModernMath" => "New Computer Modern Math",
- "NewCMUncial" | "NewComputerModernUncial" => "New Computer Modern Uncial",
- other => other,
- }
-}
-
-/// How many words the two strings share in their prefix.
-fn shared_prefix_words(left: &str, right: &str) -> usize {
- left.unicode_words()
- .zip(right.unicode_words())
- .take_while(|(l, r)| l == r)
- .count()
-}
-
-/// A compactly encoded set of codepoints.
-///
-/// The set is represented by alternating specifications of how many codepoints
-/// are not in the set and how many are in the set.
-///
-/// For example, for the set `{2, 3, 4, 9, 10, 11, 15, 18, 19}`, there are:
-/// - 2 codepoints not inside (0, 1)
-/// - 3 codepoints inside (2, 3, 4)
-/// - 4 codepoints not inside (5, 6, 7, 8)
-/// - 3 codepoints inside (9, 10, 11)
-/// - 3 codepoints not inside (12, 13, 14)
-/// - 1 codepoint inside (15)
-/// - 2 codepoints not inside (16, 17)
-/// - 2 codepoints inside (18, 19)
-///
-/// So the resulting encoding is `[2, 3, 4, 3, 3, 1, 2, 2]`.
-#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
-#[serde(transparent)]
-pub struct Coverage(Vec<u32>);
-
-impl Coverage {
- /// Encode a vector of codepoints.
- pub fn from_vec(mut codepoints: Vec<u32>) -> Self {
- codepoints.sort();
- codepoints.dedup();
-
- let mut runs = Vec::new();
- let mut next = 0;
-
- for c in codepoints {
- if let Some(run) = runs.last_mut().filter(|_| c == next) {
- *run += 1;
- } else {
- runs.push(c - next);
- runs.push(1);
- }
-
- next = c + 1;
- }
-
- Self(runs)
- }
-
- /// Whether the codepoint is covered.
- pub fn contains(&self, c: u32) -> bool {
- let mut inside = false;
- let mut cursor = 0;
-
- for &run in &self.0 {
- if (cursor..cursor + run).contains(&c) {
- return inside;
- }
- cursor += run;
- inside = !inside;
- }
-
- false
- }
-
- /// Iterate over all covered codepoints.
- pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
- let mut inside = false;
- let mut cursor = 0;
- self.0.iter().flat_map(move |run| {
- let range = if inside { cursor..cursor + run } else { 0..0 };
- inside = !inside;
- cursor += run;
- range
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_trim_styles() {
- assert_eq!(typographic_family("Atma Light"), "Atma");
- assert_eq!(typographic_family("eras bold"), "eras");
- assert_eq!(typographic_family("footlight mt light"), "footlight mt");
- assert_eq!(typographic_family("times new roman"), "times new roman");
- assert_eq!(typographic_family("noto sans mono cond sembd"), "noto sans mono");
- assert_eq!(typographic_family("noto serif SEMCOND sembd"), "noto serif");
- assert_eq!(typographic_family("crimson text"), "crimson text");
- assert_eq!(typographic_family("footlight light"), "footlight");
- assert_eq!(typographic_family("Noto Sans"), "Noto Sans");
- assert_eq!(typographic_family("Noto Sans Light"), "Noto Sans");
- assert_eq!(typographic_family("Noto Sans Semicondensed Heavy"), "Noto Sans");
- assert_eq!(typographic_family("Familx"), "Familx");
- assert_eq!(typographic_family("Font Ultra"), "Font Ultra");
- assert_eq!(typographic_family("Font Ultra Bold"), "Font");
- }
-
- #[test]
- fn test_coverage() {
- #[track_caller]
- fn test(set: &[u32], runs: &[u32]) {
- let coverage = Coverage::from_vec(set.to_vec());
- assert_eq!(coverage.0, runs);
-
- let max = 5 + set.iter().copied().max().unwrap_or_default();
- for c in 0..max {
- assert_eq!(set.contains(&c), coverage.contains(c));
- }
- }
-
- test(&[], &[]);
- test(&[0], &[0, 1]);
- test(&[1], &[1, 1]);
- test(&[0, 1], &[0, 2]);
- test(&[0, 1, 3], &[0, 2, 1, 1]);
- test(
- // {2, 3, 4, 9, 10, 11, 15, 18, 19}
- &[18, 19, 2, 4, 9, 11, 15, 3, 3, 10],
- &[2, 3, 4, 3, 3, 1, 2, 2],
- )
- }
-
- #[test]
- fn test_coverage_iter() {
- let codepoints = vec![2, 3, 7, 8, 9, 14, 15, 19, 21];
- let coverage = Coverage::from_vec(codepoints.clone());
- assert_eq!(coverage.iter().collect::<Vec<_>>(), codepoints);
- }
-}
diff --git a/src/font/mod.rs b/src/font/mod.rs
deleted file mode 100644
index 2353e51c..00000000
--- a/src/font/mod.rs
+++ /dev/null
@@ -1,249 +0,0 @@
-//! Font handling.
-
-mod book;
-mod variant;
-
-pub use self::book::{Coverage, FontBook, FontFlags, FontInfo};
-pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight};
-
-use std::fmt::{self, Debug, Formatter};
-use std::hash::{Hash, Hasher};
-use std::sync::Arc;
-
-use ttf_parser::GlyphId;
-
-use self::book::find_name;
-use crate::eval::Cast;
-use crate::geom::Em;
-use crate::util::Bytes;
-
-/// An OpenType font.
-///
-/// Values of this type are cheap to clone and hash.
-#[derive(Clone)]
-pub struct Font(Arc<Repr>);
-
-/// The internal representation of a font.
-struct Repr {
- /// The raw font data, possibly shared with other fonts from the same
- /// collection. The vector's allocation must not move, because `ttf` points
- /// into it using unsafe code.
- data: Bytes,
- /// The font's index in the buffer.
- index: u32,
- /// Metadata about the font.
- info: FontInfo,
- /// The font's metrics.
- metrics: FontMetrics,
- /// The underlying ttf-parser face.
- ttf: ttf_parser::Face<'static>,
- /// The underlying rustybuzz face.
- rusty: rustybuzz::Face<'static>,
-}
-
-impl Font {
- /// Parse a font from data and collection index.
- pub fn new(data: Bytes, index: u32) -> Option<Self> {
- // Safety:
- // - The slices's location is stable in memory:
- // - We don't move the underlying vector
- // - Nobody else can move it since we have a strong ref to the `Arc`.
- // - The internal 'static lifetime is not leaked because its rewritten
- // to the self-lifetime in `ttf()`.
- let slice: &'static [u8] =
- unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
-
- let ttf = ttf_parser::Face::parse(slice, index).ok()?;
- let rusty = rustybuzz::Face::from_slice(slice, index)?;
- let metrics = FontMetrics::from_ttf(&ttf);
- let info = FontInfo::from_ttf(&ttf)?;
-
- Some(Self(Arc::new(Repr { data, index, info, metrics, ttf, rusty })))
- }
-
- /// Parse all fonts in the given data.
- pub fn iter(data: Bytes) -> impl Iterator<Item = Self> {
- let count = ttf_parser::fonts_in_collection(&data).unwrap_or(1);
- (0..count).filter_map(move |index| Self::new(data.clone(), index))
- }
-
- /// The underlying buffer.
- pub fn data(&self) -> &Bytes {
- &self.0.data
- }
-
- /// The font's index in the buffer.
- pub fn index(&self) -> u32 {
- self.0.index
- }
-
- /// The font's metadata.
- pub fn info(&self) -> &FontInfo {
- &self.0.info
- }
-
- /// The font's metrics.
- pub fn metrics(&self) -> &FontMetrics {
- &self.0.metrics
- }
-
- /// The number of font units per one em.
- pub fn units_per_em(&self) -> f64 {
- self.0.metrics.units_per_em
- }
-
- /// Convert from font units to an em length.
- pub fn to_em(&self, units: impl Into<f64>) -> Em {
- Em::from_units(units, self.units_per_em())
- }
-
- /// Look up the horizontal advance width of a glyph.
- pub fn advance(&self, glyph: u16) -> Option<Em> {
- self.0
- .ttf
- .glyph_hor_advance(GlyphId(glyph))
- .map(|units| self.to_em(units))
- }
-
- /// Lookup a name by id.
- pub fn find_name(&self, id: u16) -> Option<String> {
- find_name(&self.0.ttf, id)
- }
-
- /// A reference to the underlying `ttf-parser` face.
- pub fn ttf(&self) -> &ttf_parser::Face<'_> {
- // We can't implement Deref because that would leak the
- // internal 'static lifetime.
- &self.0.ttf
- }
-
- /// A reference to the underlying `rustybuzz` face.
- pub fn rusty(&self) -> &rustybuzz::Face<'_> {
- // We can't implement Deref because that would leak the
- // internal 'static lifetime.
- &self.0.rusty
- }
-}
-
-impl Hash for Font {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.0.data.hash(state);
- self.0.index.hash(state);
- }
-}
-
-impl Debug for Font {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Font({})", self.info().family)
- }
-}
-
-impl Eq for Font {}
-
-impl PartialEq for Font {
- fn eq(&self, other: &Self) -> bool {
- self.0.data == other.0.data && self.0.index == other.0.index
- }
-}
-
-/// Metrics of a font.
-#[derive(Debug, Copy, Clone)]
-pub struct FontMetrics {
- /// How many font units represent one em unit.
- pub units_per_em: f64,
- /// The distance from the baseline to the typographic ascender.
- pub ascender: Em,
- /// The approximate height of uppercase letters.
- pub cap_height: Em,
- /// The approximate height of non-ascending lowercase letters.
- pub x_height: Em,
- /// The distance from the baseline to the typographic descender.
- pub descender: Em,
- /// Recommended metrics for a strikethrough line.
- pub strikethrough: LineMetrics,
- /// Recommended metrics for an underline.
- pub underline: LineMetrics,
- /// Recommended metrics for an overline.
- pub overline: LineMetrics,
-}
-
-impl FontMetrics {
- /// Extract the font's metrics.
- pub fn from_ttf(ttf: &ttf_parser::Face) -> Self {
- let units_per_em = f64::from(ttf.units_per_em());
- let to_em = |units| Em::from_units(units, units_per_em);
-
- let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender()));
- let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
- let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
- let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
- let strikeout = ttf.strikeout_metrics();
- let underline = ttf.underline_metrics();
-
- let strikethrough = LineMetrics {
- position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
- thickness: strikeout
- .or(underline)
- .map_or(Em::new(0.06), |s| to_em(s.thickness)),
- };
-
- let underline = LineMetrics {
- position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
- thickness: underline
- .or(strikeout)
- .map_or(Em::new(0.06), |s| to_em(s.thickness)),
- };
-
- let overline = LineMetrics {
- position: cap_height + Em::new(0.1),
- thickness: underline.thickness,
- };
-
- Self {
- units_per_em,
- ascender,
- cap_height,
- x_height,
- descender,
- strikethrough,
- underline,
- overline,
- }
- }
-
- /// Look up a vertical metric.
- pub fn vertical(&self, metric: VerticalFontMetric) -> Em {
- match metric {
- VerticalFontMetric::Ascender => self.ascender,
- VerticalFontMetric::CapHeight => self.cap_height,
- VerticalFontMetric::XHeight => self.x_height,
- VerticalFontMetric::Baseline => Em::zero(),
- VerticalFontMetric::Descender => self.descender,
- }
- }
-}
-
-/// Metrics for a decorative line.
-#[derive(Debug, Copy, Clone)]
-pub struct LineMetrics {
- /// The vertical offset of the line from the baseline. Positive goes
- /// upwards, negative downwards.
- pub position: Em,
- /// The thickness of the line.
- pub thickness: Em,
-}
-
-/// Identifies a vertical metric of a font.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum VerticalFontMetric {
- /// The font's ascender, which typically exceeds the height of all glyphs.
- Ascender,
- /// The approximate height of uppercase letters.
- CapHeight,
- /// The approximate height of non-ascending lowercase letters.
- XHeight,
- /// The baseline on which the letters rest.
- Baseline,
- /// The font's ascender, which typically exceeds the depth of all glyphs.
- Descender,
-}
diff --git a/src/font/variant.rs b/src/font/variant.rs
deleted file mode 100644
index d4508a59..00000000
--- a/src/font/variant.rs
+++ /dev/null
@@ -1,270 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-
-use serde::{Deserialize, Serialize};
-
-use crate::eval::{cast, Cast, IntoValue};
-use crate::geom::Ratio;
-
-/// Properties that distinguish a font from other fonts in the same family.
-#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[derive(Serialize, Deserialize)]
-pub struct FontVariant {
- /// The style of the font (normal / italic / oblique).
- pub style: FontStyle,
- /// How heavy the font is (100 - 900).
- pub weight: FontWeight,
- /// How condensed or expanded the font is (0.5 - 2.0).
- pub stretch: FontStretch,
-}
-
-impl FontVariant {
- /// Create a variant from its three components.
- pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self {
- Self { style, weight, stretch }
- }
-}
-
-impl Debug for FontVariant {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch)
- }
-}
-
-/// The style of a font.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[derive(Serialize, Deserialize, Cast)]
-#[serde(rename_all = "kebab-case")]
-pub enum FontStyle {
- /// The default, typically upright style.
- Normal,
- /// A cursive style with custom letterform.
- Italic,
- /// Just a slanted version of the normal style.
- Oblique,
-}
-
-impl FontStyle {
- /// The conceptual distance between the styles, expressed as a number.
- pub fn distance(self, other: Self) -> u16 {
- if self == other {
- 0
- } else if self != Self::Normal && other != Self::Normal {
- 1
- } else {
- 2
- }
- }
-}
-
-impl Default for FontStyle {
- fn default() -> Self {
- Self::Normal
- }
-}
-
-/// The weight of a font.
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[derive(Serialize, Deserialize)]
-#[serde(transparent)]
-pub struct FontWeight(u16);
-
-impl FontWeight {
- /// Thin weight (100).
- pub const THIN: Self = Self(100);
-
- /// Extra light weight (200).
- pub const EXTRALIGHT: Self = Self(200);
-
- /// Light weight (300).
- pub const LIGHT: Self = Self(300);
-
- /// Regular weight (400).
- pub const REGULAR: Self = Self(400);
-
- /// Medium weight (500).
- pub const MEDIUM: Self = Self(500);
-
- /// Semibold weight (600).
- pub const SEMIBOLD: Self = Self(600);
-
- /// Bold weight (700).
- pub const BOLD: Self = Self(700);
-
- /// Extrabold weight (800).
- pub const EXTRABOLD: Self = Self(800);
-
- /// Black weight (900).
- pub const BLACK: Self = Self(900);
-
- /// Create a font weight from a number between 100 and 900, clamping it if
- /// necessary.
- pub fn from_number(weight: u16) -> Self {
- Self(weight.max(100).min(900))
- }
-
- /// The number between 100 and 900.
- pub fn to_number(self) -> u16 {
- self.0
- }
-
- /// Add (or remove) weight, saturating at the boundaries of 100 and 900.
- pub fn thicken(self, delta: i16) -> Self {
- Self((self.0 as i16).saturating_add(delta).max(100).min(900) as u16)
- }
-
- /// The absolute number distance between this and another font weight.
- pub fn distance(self, other: Self) -> u16 {
- (self.0 as i16 - other.0 as i16).unsigned_abs()
- }
-}
-
-impl Default for FontWeight {
- fn default() -> Self {
- Self::REGULAR
- }
-}
-
-impl Debug for FontWeight {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{}", self.0)
- }
-}
-
-cast! {
- FontWeight,
- self => IntoValue::into_value(match self {
- FontWeight::THIN => "thin",
- FontWeight::EXTRALIGHT => "extralight",
- FontWeight::LIGHT => "light",
- FontWeight::REGULAR => "regular",
- FontWeight::MEDIUM => "medium",
- FontWeight::SEMIBOLD => "semibold",
- FontWeight::BOLD => "bold",
- FontWeight::EXTRABOLD => "extrabold",
- FontWeight::BLACK => "black",
- _ => return self.to_number().into_value(),
- }),
- v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
- /// Thin weight (100).
- "thin" => Self::THIN,
- /// Extra light weight (200).
- "extralight" => Self::EXTRALIGHT,
- /// Light weight (300).
- "light" => Self::LIGHT,
- /// Regular weight (400).
- "regular" => Self::REGULAR,
- /// Medium weight (500).
- "medium" => Self::MEDIUM,
- /// Semibold weight (600).
- "semibold" => Self::SEMIBOLD,
- /// Bold weight (700).
- "bold" => Self::BOLD,
- /// Extrabold weight (800).
- "extrabold" => Self::EXTRABOLD,
- /// Black weight (900).
- "black" => Self::BLACK,
-}
-
-/// The width of a font.
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[derive(Serialize, Deserialize)]
-#[serde(transparent)]
-pub struct FontStretch(u16);
-
-impl FontStretch {
- /// Ultra-condensed stretch (50%).
- pub const ULTRA_CONDENSED: Self = Self(500);
-
- /// Extra-condensed stretch weight (62.5%).
- pub const EXTRA_CONDENSED: Self = Self(625);
-
- /// Condensed stretch (75%).
- pub const CONDENSED: Self = Self(750);
-
- /// Semi-condensed stretch (87.5%).
- pub const SEMI_CONDENSED: Self = Self(875);
-
- /// Normal stretch (100%).
- pub const NORMAL: Self = Self(1000);
-
- /// Semi-expanded stretch (112.5%).
- pub const SEMI_EXPANDED: Self = Self(1125);
-
- /// Expanded stretch (125%).
- pub const EXPANDED: Self = Self(1250);
-
- /// Extra-expanded stretch (150%).
- pub const EXTRA_EXPANDED: Self = Self(1500);
-
- /// Ultra-expanded stretch (200%).
- pub const ULTRA_EXPANDED: Self = Self(2000);
-
- /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if
- /// necessary.
- pub fn from_ratio(ratio: Ratio) -> Self {
- Self((ratio.get().max(0.5).min(2.0) * 1000.0) as u16)
- }
-
- /// Create a font stretch from an OpenType-style number between 1 and 9,
- /// clamping it if necessary.
- pub fn from_number(stretch: u16) -> Self {
- match stretch {
- 0 | 1 => Self::ULTRA_CONDENSED,
- 2 => Self::EXTRA_CONDENSED,
- 3 => Self::CONDENSED,
- 4 => Self::SEMI_CONDENSED,
- 5 => Self::NORMAL,
- 6 => Self::SEMI_EXPANDED,
- 7 => Self::EXPANDED,
- 8 => Self::EXTRA_EXPANDED,
- _ => Self::ULTRA_EXPANDED,
- }
- }
-
- /// The ratio between 0.5 and 2.0 corresponding to this stretch.
- pub fn to_ratio(self) -> Ratio {
- Ratio::new(self.0 as f64 / 1000.0)
- }
-
- /// The absolute ratio distance between this and another font stretch.
- pub fn distance(self, other: Self) -> Ratio {
- (self.to_ratio() - other.to_ratio()).abs()
- }
-}
-
-impl Default for FontStretch {
- fn default() -> Self {
- Self::NORMAL
- }
-}
-
-impl Debug for FontStretch {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.to_ratio().fmt(f)
- }
-}
-
-cast! {
- FontStretch,
- self => self.to_ratio().into_value(),
- v: Ratio => Self::from_ratio(v),
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_font_weight_distance() {
- let d = |a, b| FontWeight(a).distance(FontWeight(b));
- assert_eq!(d(500, 200), 300);
- assert_eq!(d(500, 500), 0);
- assert_eq!(d(500, 900), 400);
- assert_eq!(d(10, 100), 90);
- }
-
- #[test]
- fn test_font_stretch_debug() {
- assert_eq!(format!("{:?}", FontStretch::EXPANDED), "125%")
- }
-}