diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-09-19 17:44:40 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-09-19 17:44:40 +0200 |
| commit | e29f55bb294cc298daad97accf6d8a76976b409c (patch) | |
| tree | cc4db3b61fa23e13f781e992c63427d36e77ef8c /src/font.rs | |
| parent | 59f67b79c7ff50f0bc9a27373d0fa36d1523e08a (diff) | |
Remove font store
Diffstat (limited to 'src/font.rs')
| -rw-r--r-- | src/font.rs | 975 |
1 files changed, 0 insertions, 975 deletions
diff --git a/src/font.rs b/src/font.rs deleted file mode 100644 index 1fa86ba6..00000000 --- a/src/font.rs +++ /dev/null @@ -1,975 +0,0 @@ -//! Font handling. - -use std::cmp::Reverse; -use std::collections::{hash_map::Entry, BTreeMap, HashMap}; -use std::fmt::{self, Debug, Formatter}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use once_cell::sync::OnceCell; -use rex::font::MathHeader; -use serde::{Deserialize, Serialize}; -use ttf_parser::{name_id, GlyphId, PlatformId, Tag}; -use unicode_segmentation::UnicodeSegmentation; - -use crate::geom::Em; -use crate::loading::{Buffer, FileHash, Loader}; - -/// A unique identifier for a loaded font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct FontId(u32); - -impl FontId { - /// Create a font id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub const fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u32 { - self.0 - } -} - -/// Storage for loaded and parsed fonts. -pub struct FontStore { - loader: Arc<dyn Loader>, - failed: Vec<bool>, - fonts: Vec<Option<Font>>, - families: BTreeMap<String, Vec<FontId>>, - buffers: HashMap<FileHash, Buffer>, -} - -impl FontStore { - /// Create a new, empty font store. - pub fn new(loader: Arc<dyn Loader>) -> Self { - let mut fonts = vec![]; - let mut failed = vec![]; - let mut families = BTreeMap::<String, Vec<FontId>>::new(); - - let infos = loader.fonts(); - for (i, info) in infos.iter().enumerate() { - let id = FontId(i as u32); - fonts.push(None); - failed.push(false); - families.entry(info.family.to_lowercase()).or_default().push(id); - } - - for fonts in families.values_mut() { - fonts.sort_by_key(|id| infos[id.0 as usize].variant); - fonts.dedup_by_key(|id| infos[id.0 as usize].variant); - } - - Self { - loader, - fonts, - failed, - families, - buffers: HashMap::new(), - } - } - - /// An ordered iterator over all font families this loader 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 font's info. - let fonts = self.loader.fonts(); - self.families.values().map(|ids| { - let family = fonts[ids[0].0 as usize].family.as_str(); - let infos = ids.iter().map(|&id| &fonts[id.0 as usize]); - (family, infos) - }) - } - - /// Get a reference to a loaded font. - /// - /// This panics if the font with this `id` was not loaded. This function - /// should only be called with ids returned by this store's - /// [`select()`](Self::select) and - /// [`select_fallback()`](Self::select_fallback) methods. - #[track_caller] - pub fn get(&self, id: FontId) -> &Font { - self.fonts[id.0 as usize].as_ref().expect("font was not loaded") - } - - /// Try to find and load a font from the given `family` that matches - /// the given `variant` as closely as possible. - pub fn select(&mut self, family: &str, variant: FontVariant) -> Option<FontId> { - let ids = self.families.get(family)?; - let id = self.find_best_variant(None, variant, ids.iter().copied())?; - self.load(id) - } - - /// 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( - &mut self, - like: Option<FontId>, - variant: FontVariant, - text: &str, - ) -> Option<FontId> { - // Find the fonts that contain the text's first char ... - let c = text.chars().next()?; - let ids = self - .loader - .fonts() - .iter() - .enumerate() - .filter(|(_, info)| info.coverage.contains(c as u32)) - .map(|(i, _)| FontId(i as u32)); - - // ... and find the best variant among them. - let id = self.find_best_variant(like, variant, ids)?; - self.load(id) - } - - /// 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<FontId>, - variant: FontVariant, - ids: impl IntoIterator<Item = FontId>, - ) -> Option<FontId> { - let infos = self.loader.fonts(); - let like = like.map(|id| &infos[id.0 as usize]); - - let mut best = None; - let mut best_key = None; - - for id in ids { - let current = &infos[id.0 as usize]; - - let key = ( - like.map(|like| { - ( - current.monospaced != like.monospaced, - like.serif.is_some() && current.serif != like.serif, - Reverse(shared_prefix_words(¤t.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 - } - - /// Load the font with the given id. - /// - /// Returns `Some(id)` if the font was loaded successfully. - fn load(&mut self, id: FontId) -> Option<FontId> { - let idx = id.0 as usize; - let slot = &mut self.fonts[idx]; - if slot.is_some() { - return Some(id); - } - - if self.failed[idx] { - return None; - } - - let FontInfo { ref path, index, .. } = self.loader.fonts()[idx]; - self.failed[idx] = true; - - // Check the buffer cache since multiple fonts may refer to the same - // data (font collection). - let hash = self.loader.resolve(path).ok()?; - let buffer = match self.buffers.entry(hash) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - let buffer = self.loader.load(path).ok()?; - entry.insert(buffer) - } - }; - - let font = Font::new(buffer.clone(), index)?; - *slot = Some(font); - self.failed[idx] = false; - - Some(id) - } -} - -/// 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() -} - -/// An OpenType font. -pub struct Font { - /// 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: Buffer, - /// The font's index in the collection (zero if not a collection). - index: u32, - /// The underlying ttf-parser/rustybuzz face. - ttf: rustybuzz::Face<'static>, - /// The font's metrics. - metrics: FontMetrics, - /// The parsed ReX math header. - math: OnceCell<Option<MathHeader>>, -} - -impl Font { - /// Parse a font from data and collection index. - pub fn new(data: Buffer, 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 = rustybuzz::Face::from_slice(slice, index)?; - let metrics = FontMetrics::from_ttf(&ttf); - - Some(Self { - data, - index, - ttf, - metrics, - math: OnceCell::new(), - }) - } - - /// The underlying buffer. - pub fn buffer(&self) -> &Buffer { - &self.data - } - - /// The collection index. - pub fn index(&self) -> u32 { - self.index - } - - /// A reference to the underlying `ttf-parser` / `rustybuzz` face. - pub fn ttf(&self) -> &rustybuzz::Face<'_> { - // We can't implement Deref because that would leak the internal 'static - // lifetime. - &self.ttf - } - - /// The number of font units per one em. - pub fn units_per_em(&self) -> f64 { - self.metrics.units_per_em - } - - /// Access the font's metrics. - pub fn metrics(&self) -> &FontMetrics { - &self.metrics - } - - /// 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.ttf - .glyph_hor_advance(GlyphId(glyph)) - .map(|units| self.to_em(units)) - } - - /// Access the math header, if any. - pub fn math(&self) -> Option<&MathHeader> { - self.math - .get_or_init(|| { - let data = self.ttf().table_data(Tag::from_bytes(b"MATH"))?; - MathHeader::parse(data).ok() - }) - .as_ref() - } - - /// Lookup a name by id. - pub fn find_name(&self, name_id: u16) -> Option<String> { - find_name_ttf(&self.ttf, name_id) - } -} - -/// Metrics for 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)] -pub enum VerticalFontMetric { - /// The distance from the baseline to the typographic ascender. - /// - /// Corresponds to the typographic ascender from the `OS/2` table if present - /// and falls back to the ascender from the `hhea` table otherwise. - 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 distance from the baseline to the typographic descender. - /// - /// Corresponds to the typographic descender from the `OS/2` table if - /// present and falls back to the descender from the `hhea` table otherwise. - Descender, -} - -/// Properties of a single font. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct FontInfo { - /// The path to the font file. - pub path: PathBuf, - /// The collection index in the font file. - pub index: u32, - /// 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, - /// Whether the font is monospaced. - pub monospaced: bool, - /// Whether the font has serifs (if known). - pub serif: Option<bool>, - /// The unicode coverage of the font. - pub coverage: Coverage, -} - -impl FontInfo { - /// Compute metadata for all fonts in the given data. - pub fn from_data<'a>( - path: &'a Path, - data: &'a [u8], - ) -> impl Iterator<Item = FontInfo> + 'a { - let count = ttf_parser::fonts_in_collection(data).unwrap_or(1); - (0 .. count).filter_map(move |index| { - let ttf = ttf_parser::Face::from_slice(data, index).ok()?; - Self::from_ttf(path, index, &ttf) - }) - } - - /// Compute metadata for a single ttf-parser face. - pub fn from_ttf(path: &Path, index: u32, 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(ttf, name_id::FAMILY)?; - if family.starts_with("Noto") { - family = find_name_ttf(ttf, name_id::FULL_NAME)?; - } - trim_styles(&family).to_string() - }; - - let variant = { - let mut full = find_name_ttf(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 = FontWeight::from_number(ttf.weight().to_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)); - } - } - - // Determine whether this is a serif or sans-serif font. - let mut serif = None; - if let Some(panose) = ttf - .table_data(Tag::from_bytes(b"OS/2")) - .and_then(|os2| os2.get(32 .. 45)) - { - match panose { - [2, 2 ..= 10, ..] => serif = Some(true), - [2, 11 ..= 15, ..] => serif = Some(false), - _ => {} - } - } - - Some(FontInfo { - path: path.to_owned(), - index, - family, - variant, - monospaced: ttf.is_monospaced(), - serif, - coverage: Coverage::from_vec(codepoints), - }) - } -} - -/// Try to find and decode the name with the given id. -fn find_name_ttf(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. -fn trim_styles(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" - ]; - - // 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 = match SUFFIXES.iter().find_map(|s| trimmed.strip_suffix(s)) { - Some(t) => t, - None => 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; - } - } - } - - &family[.. len] -} - -/// 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)] -#[serde(rename_all = "kebab-case")] -pub enum FontStyle { - /// The default style. - Normal, - /// A cursive style. - Italic, - /// A slanted 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).abs() as u16 - } -} - -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) - } -} - -/// 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: f32) -> Self { - Self((ratio.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) -> f32 { - self.0 as f32 / 1000.0 - } - - /// The absolute ratio distance between this and another font stretch. - pub fn distance(self, other: Self) -> f32 { - (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 { - write!(f, "{}%", 100.0 * self.to_ratio()) - } -} - -/// 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, 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 - } -} - -#[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%") - } - - #[test] - fn test_trim_styles() { - assert_eq!(trim_styles("Atma Light"), "Atma"); - assert_eq!(trim_styles("eras bold"), "eras"); - assert_eq!(trim_styles("footlight mt light"), "footlight mt"); - assert_eq!(trim_styles("times new roman"), "times new roman"); - assert_eq!(trim_styles("noto sans mono cond sembd"), "noto sans mono"); - assert_eq!(trim_styles("noto serif SEMCOND sembd"), "noto serif"); - assert_eq!(trim_styles("crimson text"), "crimson text"); - assert_eq!(trim_styles("footlight light"), "footlight"); - assert_eq!(trim_styles("Noto Sans"), "Noto Sans"); - assert_eq!(trim_styles("Noto Sans Light"), "Noto Sans"); - assert_eq!(trim_styles("Noto Sans Semicondensed Heavy"), "Noto Sans"); - assert_eq!(trim_styles("Familx"), "Familx"); - assert_eq!(trim_styles("Font Ultra"), "Font Ultra"); - assert_eq!(trim_styles("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], - ) - } -} |
