summaryrefslogtreecommitdiff
path: root/src/font.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/font.rs')
-rw-r--r--src/font.rs496
1 files changed, 426 insertions, 70 deletions
diff --git a/src/font.rs b/src/font.rs
index 1ac8fea3..52912664 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -1,27 +1,60 @@
//! Font handling.
-use std::fmt::{self, Display, Formatter};
+use std::fmt::{self, Debug, Display, Formatter};
-use fontdock::FaceFromVec;
+use serde::{Deserialize, Serialize};
+use crate::env::Buffer;
use crate::geom::Length;
-/// An owned font face.
-pub struct FaceBuf {
- data: Box<[u8]>,
+/// A font face.
+pub struct Face {
+ buffer: Buffer,
index: u32,
- inner: rustybuzz::Face<'static>,
+ ttf: rustybuzz::Face<'static>,
units_per_em: f64,
- ascender: f64,
- cap_height: f64,
- x_height: f64,
- descender: f64,
+ ascender: Em,
+ cap_height: Em,
+ x_height: Em,
+ descender: Em,
}
-impl FaceBuf {
- /// The raw face data.
- pub fn data(&self) -> &[u8] {
- &self.data
+impl Face {
+ /// Parse a font face from a buffer and collection index.
+ pub fn new(buffer: 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 haved a strong ref to the `Rc`.
+ // - 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(buffer.as_ptr(), buffer.len()) };
+
+ let ttf = rustybuzz::Face::from_slice(slice, index)?;
+
+ // Look up some metrics we may need often.
+ let units_per_em = f64::from(ttf.units_per_em());
+ let ascender = ttf.typographic_ascender().unwrap_or(ttf.ascender());
+ let cap_height = ttf.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
+ let x_height = ttf.x_height().filter(|&h| h > 0).unwrap_or(ascender);
+ let descender = ttf.typographic_descender().unwrap_or(ttf.descender());
+
+ Some(Self {
+ buffer,
+ index,
+ ttf,
+ units_per_em,
+ ascender: Em::from_units(ascender, units_per_em),
+ cap_height: Em::from_units(cap_height, units_per_em),
+ x_height: Em::from_units(x_height, units_per_em),
+ descender: Em::from_units(descender, units_per_em),
+ })
+ }
+
+ /// The underlying buffer.
+ pub fn buffer(&self) -> &Buffer {
+ &self.buffer
}
/// The collection index.
@@ -29,74 +62,27 @@ impl FaceBuf {
self.index
}
- /// Get a reference to the underlying ttf-parser/rustybuzz face.
+ /// 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.inner
+ &self.ttf
}
/// Look up a vertical metric.
- pub fn vertical_metric(&self, metric: VerticalFontMetric) -> EmLength {
- self.convert(match metric {
+ pub fn vertical_metric(&self, metric: VerticalFontMetric) -> Em {
+ match metric {
VerticalFontMetric::Ascender => self.ascender,
VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height,
- VerticalFontMetric::Baseline => 0.0,
+ VerticalFontMetric::Baseline => Em::ZERO,
VerticalFontMetric::Descender => self.descender,
- })
- }
-
- /// Convert from font units to an em length length.
- pub fn convert(&self, units: impl Into<f64>) -> EmLength {
- EmLength(units.into() / self.units_per_em)
- }
-}
-
-impl FaceFromVec for FaceBuf {
- fn from_vec(vec: Vec<u8>, index: u32) -> Option<Self> {
- let data = vec.into_boxed_slice();
-
- // SAFETY: The slices's location is stable in memory since we don't
- // touch it and it can't be touched from outside this type.
- let slice: &'static [u8] =
- unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
-
- let inner = rustybuzz::Face::from_slice(slice, index)?;
-
- // Look up some metrics we may need often.
- let units_per_em = inner.units_per_em();
- let ascender = inner.typographic_ascender().unwrap_or(inner.ascender());
- let cap_height = inner.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
- let x_height = inner.x_height().filter(|&h| h > 0).unwrap_or(ascender);
- let descender = inner.typographic_descender().unwrap_or(inner.descender());
-
- Some(Self {
- data,
- index,
- inner,
- units_per_em: f64::from(units_per_em),
- ascender: f64::from(ascender),
- cap_height: f64::from(cap_height),
- x_height: f64::from(x_height),
- descender: f64::from(descender),
- })
+ }
}
-}
-/// A length in resolved em units.
-#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
-pub struct EmLength(f64);
-
-impl EmLength {
- /// Convert to a length at the given font size.
- pub fn scale(self, size: Length) -> Length {
- self.0 * size
- }
-
- /// Get the number of em units.
- pub fn get(self) -> f64 {
- self.0
+ /// 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)
}
}
@@ -132,3 +118,373 @@ impl Display for VerticalFontMetric {
})
}
}
+
+/// A length in em units.
+///
+/// `1em` is the same as the font size.
+#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
+pub struct Em(f64);
+
+impl Em {
+ /// The zero length.
+ pub const ZERO: Self = Self(0.0);
+
+ /// Create an em length.
+ pub fn new(em: f64) -> Self {
+ Self(em)
+ }
+
+ /// Convert units to an em length at the given units per em.
+ pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self {
+ Self(units.into() / units_per_em)
+ }
+
+ /// The number of em units.
+ pub fn get(self) -> f64 {
+ self.0
+ }
+
+ /// Convert to a length at the given font size.
+ pub fn to_length(self, font_size: Length) -> Length {
+ self.0 * font_size
+ }
+}
+
+/// Properties of a single font face.
+#[derive(Debug, Clone, PartialEq)]
+pub struct FaceInfo {
+ /// The typographic font family this face is part of.
+ pub family: String,
+ /// Properties that distinguish this face from other faces in the same
+ /// family.
+ pub variant: FontVariant,
+ /// The collection index in the font file.
+ pub index: u32,
+}
+
+/// Properties that distinguish a face from other faces in the same family.
+#[derive(Default, Debug, Copy, Clone, PartialEq)]
+pub struct FontVariant {
+ /// The style of the face (normal / italic / oblique).
+ pub style: FontStyle,
+ /// How heavy the face is (100 - 900).
+ pub weight: FontWeight,
+ /// How condensed or expanded the face 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 }
+ }
+}
+
+/// The style of a font face.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Serialize, Deserialize)]
+pub enum FontStyle {
+ /// The default style.
+ Normal,
+ /// A cursive style.
+ Italic,
+ /// A slanted style.
+ Oblique,
+}
+
+impl FontStyle {
+ /// Create a font style from a lowercase name like `italic`.
+ pub fn from_str(name: &str) -> Option<FontStyle> {
+ Some(match name {
+ "normal" => Self::Normal,
+ "italic" => Self::Italic,
+ "oblique" => Self::Oblique,
+ _ => return None,
+ })
+ }
+
+ /// The lowercase string representation of this style.
+ pub fn to_str(self) -> &'static str {
+ match self {
+ Self::Normal => "normal",
+ Self::Italic => "italic",
+ Self::Oblique => "oblique",
+ }
+ }
+}
+
+impl Default for FontStyle {
+ fn default() -> Self {
+ Self::Normal
+ }
+}
+
+impl Display for FontStyle {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.to_str())
+ }
+}
+
+/// The weight of a font face.
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, 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))
+ }
+
+ /// Create a font weight from a lowercase name like `light`.
+ pub fn from_str(name: &str) -> Option<Self> {
+ Some(match name {
+ "thin" => Self::THIN,
+ "extralight" => Self::EXTRALIGHT,
+ "light" => Self::LIGHT,
+ "regular" => Self::REGULAR,
+ "medium" => Self::MEDIUM,
+ "semibold" => Self::SEMIBOLD,
+ "bold" => Self::BOLD,
+ "extrabold" => Self::EXTRABOLD,
+ "black" => Self::BLACK,
+ _ => return None,
+ })
+ }
+
+ /// The number between 100 and 900.
+ pub fn to_number(self) -> u16 {
+ self.0
+ }
+
+ /// The lowercase string representation of this weight if it is divisible by
+ /// 100.
+ pub fn to_str(self) -> Option<&'static str> {
+ Some(match self {
+ Self::THIN => "thin",
+ Self::EXTRALIGHT => "extralight",
+ Self::LIGHT => "light",
+ Self::REGULAR => "regular",
+ Self::MEDIUM => "medium",
+ Self::SEMIBOLD => "semibold",
+ Self::BOLD => "bold",
+ Self::EXTRABOLD => "extrabold",
+ Self::BLACK => "black",
+ _ => return None,
+ })
+ }
+
+ /// 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 Display for FontWeight {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.to_str() {
+ Some(name) => f.pad(name),
+ None => write!(f, "{}", self.0),
+ }
+ }
+}
+
+impl Debug for FontWeight {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.pad(match *self {
+ Self::THIN => "Thin",
+ Self::EXTRALIGHT => "Extralight",
+ Self::LIGHT => "Light",
+ Self::REGULAR => "Regular",
+ Self::MEDIUM => "Medium",
+ Self::SEMIBOLD => "Semibold",
+ Self::BOLD => "Bold",
+ Self::EXTRABOLD => "Extrabold",
+ Self::BLACK => "Black",
+ _ => return write!(f, "{}", self.0),
+ })
+ }
+}
+
+/// The width of a font face.
+#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
+pub struct FontStretch(f32);
+
+impl FontStretch {
+ /// Ultra-condensed stretch (50%).
+ pub const ULTRA_CONDENSED: Self = Self(0.5);
+
+ /// Extra-condensed stretch weight (62.5%).
+ pub const EXTRA_CONDENSED: Self = Self(0.625);
+
+ /// Condensed stretch (75%).
+ pub const CONDENSED: Self = Self(0.75);
+
+ /// Semi-condensed stretch (87.5%).
+ pub const SEMI_CONDENSED: Self = Self(0.875);
+
+ /// Normal stretch (100%).
+ pub const NORMAL: Self = Self(1.0);
+
+ /// Semi-expanded stretch (112.5%).
+ pub const SEMI_EXPANDED: Self = Self(1.125);
+
+ /// Expanded stretch (125%).
+ pub const EXPANDED: Self = Self(1.25);
+
+ /// Extra-expanded stretch (150%).
+ pub const EXTRA_EXPANDED: Self = Self(1.5);
+
+ /// Ultra-expanded stretch (200%).
+ pub const ULTRA_EXPANDED: Self = Self(2.0);
+
+ /// 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))
+ }
+
+ /// 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,
+ }
+ }
+
+ /// Create a font stretch from a lowercase name like `extra-expanded`.
+ pub fn from_str(name: &str) -> Option<Self> {
+ Some(match name {
+ "ultra-condensed" => Self::ULTRA_CONDENSED,
+ "extra-condensed" => Self::EXTRA_CONDENSED,
+ "condensed" => Self::CONDENSED,
+ "semi-condensed" => Self::SEMI_CONDENSED,
+ "normal" => Self::NORMAL,
+ "semi-expanded" => Self::SEMI_EXPANDED,
+ "expanded" => Self::EXPANDED,
+ "extra-expanded" => Self::EXTRA_EXPANDED,
+ "ultra-expanded" => Self::ULTRA_EXPANDED,
+ _ => return None,
+ })
+ }
+
+ /// The ratio between 0.5 and 2.0 corresponding to this stretch.
+ pub fn to_ratio(self) -> f32 {
+ self.0
+ }
+
+ /// The lowercase string representation of this stretch is one of the named
+ /// ones.
+ pub fn to_str(self) -> Option<&'static str> {
+ Some(match self {
+ s if s == Self::ULTRA_CONDENSED => "ultra-condensed",
+ s if s == Self::EXTRA_CONDENSED => "extra-condensed",
+ s if s == Self::CONDENSED => "condensed",
+ s if s == Self::SEMI_CONDENSED => "semi-condensed",
+ s if s == Self::NORMAL => "normal",
+ s if s == Self::SEMI_EXPANDED => "semi-expanded",
+ s if s == Self::EXPANDED => "expanded",
+ s if s == Self::EXTRA_EXPANDED => "extra-expanded",
+ s if s == Self::ULTRA_EXPANDED => "ultra-expanded",
+ _ => return None,
+ })
+ }
+
+ /// The absolute ratio distance between this and another font stretch.
+ pub fn distance(self, other: Self) -> f32 {
+ (self.0 - other.0).abs()
+ }
+}
+
+impl Default for FontStretch {
+ fn default() -> Self {
+ Self::NORMAL
+ }
+}
+
+impl Display for FontStretch {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.to_str() {
+ Some(name) => f.pad(name),
+ None => write!(f, "{}", self.0),
+ }
+ }
+}
+
+impl Debug for FontStretch {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(match *self {
+ s if s == Self::ULTRA_CONDENSED => "UltraCondensed",
+ s if s == Self::EXTRA_CONDENSED => "ExtraCondensed",
+ s if s == Self::CONDENSED => "Condensed",
+ s if s == Self::SEMI_CONDENSED => "SemiCondensed",
+ s if s == Self::NORMAL => "Normal",
+ s if s == Self::SEMI_EXPANDED => "SemiExpanded",
+ s if s == Self::EXPANDED => "Expanded",
+ s if s == Self::EXTRA_EXPANDED => "ExtraExpanded",
+ s if s == Self::ULTRA_EXPANDED => "UltraExpanded",
+ _ => return write!(f, "{}", self.0),
+ })
+ }
+}
+
+#[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);
+ }
+}