summaryrefslogtreecommitdiff
path: root/src/font.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-09-19 17:44:40 +0200
committerLaurenz <laurmaedje@gmail.com>2022-09-19 17:44:40 +0200
commite29f55bb294cc298daad97accf6d8a76976b409c (patch)
treecc4db3b61fa23e13f781e992c63427d36e77ef8c /src/font.rs
parent59f67b79c7ff50f0bc9a27373d0fa36d1523e08a (diff)
Remove font store
Diffstat (limited to 'src/font.rs')
-rw-r--r--src/font.rs975
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(&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
- }
-
- /// 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],
- )
- }
-}