summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/content.rs7
-rw-r--r--src/eval/styles.rs13
-rw-r--r--src/eval/value.rs8
-rw-r--r--src/export/pdf.rs9
-rw-r--r--src/export/render.rs4
-rw-r--r--src/font.rs605
-rw-r--r--src/library/math/mod.rs28
-rw-r--r--src/library/mod.rs3
-rw-r--r--src/library/structure/heading.rs8
-rw-r--r--src/library/text/deco.rs10
-rw-r--r--src/library/text/mod.rs70
-rw-r--r--src/library/text/raw.rs34
-rw-r--r--src/library/text/shaping.rs408
-rw-r--r--src/loading/fs.rs33
-rw-r--r--src/loading/mem.rs2
15 files changed, 773 insertions, 469 deletions
diff --git a/src/eval/content.rs b/src/eval/content.rs
index 5f8c5861..1cdd4bb0 100644
--- a/src/eval/content.rs
+++ b/src/eval/content.rs
@@ -13,7 +13,7 @@ use crate::diag::StrResult;
use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing};
use crate::library::prelude::*;
use crate::library::structure::{ListItem, ListKind, ListNode, ORDERED, UNORDERED};
-use crate::library::text::{DecoNode, ParChild, ParNode, TextNode, UNDERLINE};
+use crate::library::text::{DecoNode, ParChild, ParNode, UNDERLINE};
use crate::util::EcoString;
/// Composable representation of styled content.
@@ -133,11 +133,6 @@ impl Content {
Self::Styled(Arc::new((self, styles)))
}
- /// Style this content in monospace.
- pub fn monospaced(self) -> Self {
- self.styled(TextNode::MONOSPACED, true)
- }
-
/// Underline this content.
pub fn underlined(self) -> Self {
Self::show(DecoNode::<UNDERLINE>(self))
diff --git a/src/eval/styles.rs b/src/eval/styles.rs
index 7fcaf734..a0dc263c 100644
--- a/src/eval/styles.rs
+++ b/src/eval/styles.rs
@@ -6,7 +6,7 @@ use std::sync::Arc;
use super::{Args, Content, Func, Span, Value};
use crate::diag::{At, TypResult};
use crate::library::layout::PageNode;
-use crate::library::text::ParNode;
+use crate::library::text::{FontFamily, ParNode, TextNode};
use crate::Context;
/// A map of style properties.
@@ -48,6 +48,17 @@ impl StyleMap {
}
}
+ /// Set a font family composed of a preferred family and existing families
+ /// from a style chain.
+ pub fn set_family(&mut self, family: FontFamily, existing: StyleChain) {
+ self.set(
+ TextNode::FAMILY,
+ std::iter::once(family)
+ .chain(existing.get_ref(TextNode::FAMILY).iter().cloned())
+ .collect(),
+ );
+ }
+
/// Set a recipe.
pub fn set_recipe(&mut self, node: TypeId, func: Func, span: Span) {
self.recipes.push(Recipe { node, func, span });
diff --git a/src/eval/value.rs b/src/eval/value.rs
index a76b377d..300444de 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -7,6 +7,7 @@ use std::sync::Arc;
use super::{ops, Args, Array, Content, Context, Dict, Func, Layout, StrExt};
use crate::diag::{with_alternative, At, StrResult, TypResult};
use crate::geom::{Angle, Color, Fractional, Length, Linear, Relative, RgbaColor};
+use crate::library::text::RawNode;
use crate::syntax::{Span, Spanned};
use crate::util::EcoString;
@@ -115,9 +116,10 @@ impl Value {
Value::Float(v) => Content::Text(format_eco!("{}", v)),
Value::Str(v) => Content::Text(v),
Value::Content(v) => v,
- // For values which can't be shown "naturally", we print the
- // representation in monospace.
- v => Content::Text(v.repr()).monospaced(),
+
+ // For values which can't be shown "naturally", we return the raw
+ // representation.
+ v => Content::show(RawNode { text: v.repr(), block: false }),
}
}
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index b39f47ea..2550519b 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -90,9 +90,10 @@ impl<'a> PdfExporter<'a> {
let glyphs = &self.glyph_sets[&face_id];
let face = self.fonts.get(face_id);
+ let metrics = face.metrics();
let ttf = face.ttf();
- let postscript_name = find_name(ttf.names(), name_id::POST_SCRIPT_NAME)
+ let postscript_name = find_name(ttf, name_id::POST_SCRIPT_NAME)
.unwrap_or_else(|| "unknown".to_string());
let base_font = format_eco!("ABCDEF+{}", postscript_name);
@@ -155,9 +156,9 @@ impl<'a> PdfExporter<'a> {
);
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
- let ascender = face.ascender.to_font_units();
- let descender = face.descender.to_font_units();
- let cap_height = face.cap_height.to_font_units();
+ let ascender = metrics.ascender.to_font_units();
+ let descender = metrics.descender.to_font_units();
+ let cap_height = metrics.cap_height.to_font_units();
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
// Write the font descriptor (contains metrics about the font).
diff --git a/src/export/render.rs b/src/export/render.rs
index ff35390e..89a17eea 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -160,7 +160,7 @@ fn render_svg_glyph(
// If there's no viewbox defined, use the em square for our scale
// transformation ...
- let upem = face.units_per_em as f32;
+ let upem = face.units_per_em() as f32;
let (mut width, mut height) = (upem, upem);
// ... but if there's a viewbox or width, use that.
@@ -232,7 +232,7 @@ fn render_outline_glyph(
// Flip vertically because font design coordinate
// system is Y-up.
- let scale = text.size.to_f32() / face.units_per_em as f32;
+ let scale = text.size.to_f32() / face.units_per_em() as f32;
let ts = ts.pre_scale(scale, -scale);
canvas.fill_path(&path, &paint, rule, ts, mask)?;
return Some(());
diff --git a/src/font.rs b/src/font.rs
index d8fc0f45..ff00bbef 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -1,12 +1,14 @@
//! 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 serde::{Deserialize, Serialize};
-use ttf_parser::{name_id, GlyphId, PlatformId};
+use ttf_parser::{name_id, GlyphId, PlatformId, Tag};
+use unicode_segmentation::UnicodeSegmentation;
use crate::geom::{Em, Length, Linear};
use crate::loading::{FileHash, Loader};
@@ -47,13 +49,19 @@ impl FontStore {
let mut failed = vec![];
let mut families = BTreeMap::<String, Vec<FaceId>>::new();
- for (i, info) in loader.faces().iter().enumerate() {
+ let infos = loader.faces();
+ for (i, info) in infos.iter().enumerate() {
let id = FaceId(i as u32);
faces.push(None);
failed.push(false);
families.entry(info.family.to_lowercase()).or_default().push(id);
}
+ for faces in families.values_mut() {
+ faces.sort_by_key(|id| infos[id.0 as usize].variant);
+ faces.dedup_by_key(|id| infos[id.0 as usize].variant);
+ }
+
Self {
loader,
faces,
@@ -63,33 +71,88 @@ impl FontStore {
}
}
- /// Query for and load the font face from the given `family` that most
- /// closely matches the given `variant`.
+ /// Try to find and load a font face from the given `family` that matches
+ /// the given `variant` as closely as possible.
pub fn select(&mut self, family: &str, variant: FontVariant) -> Option<FaceId> {
- // Check whether a family with this name exists.
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 face `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<FaceId>,
+ variant: FontVariant,
+ text: &str,
+ ) -> Option<FaceId> {
+ // Find the faces that contain the text's first char ...
+ let c = text.chars().next()?;
+ let ids = self
+ .loader
+ .faces()
+ .iter()
+ .enumerate()
+ .filter(|(_, info)| info.coverage.contains(c as u32))
+ .map(|(i, _)| FaceId(i as u32));
+
+ // ... and find the best variant among them.
+ let id = self.find_best_variant(like, variant, ids)?;
+ self.load(id)
+ }
+
+ /// Find the face in the passed iterator that
+ /// - is closest to the face `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 face:
+ /// - Are both faces (not) monospaced.
+ /// - Do both faces (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<FaceId>,
+ variant: FontVariant,
+ ids: impl IntoIterator<Item = FaceId>,
+ ) -> Option<FaceId> {
let infos = self.loader.faces();
+ let like = like.map(|id| &infos[id.0 as usize]);
let mut best = None;
let mut best_key = None;
// Find the best matching variant of this font.
- for &id in ids {
- let current = infos[id.0 as usize].variant;
+ for id in ids {
+ let current = &infos[id.0 as usize];
- // This is a perfect match, no need to search further.
- if current == variant {
- best = Some(id);
- break;
- }
-
- // If this is not a perfect match, we compute a key that we want to
- // minimize among all variants. This key prioritizes style, then
- // stretch distance and then weight distance.
let key = (
- current.style != variant.style,
- current.stretch.distance(variant.stretch),
- current.weight.distance(variant.weight),
+ 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) {
@@ -98,59 +161,79 @@ impl FontStore {
}
}
- let id = best?;
+ best
+ }
+
+ /// Load the face with the given id.
+ ///
+ /// Returns `Some(id)` if the face was loaded successfully.
+ fn load(&mut self, id: FaceId) -> Option<FaceId> {
let idx = id.0 as usize;
let slot = &mut self.faces[idx];
+ if slot.is_some() {
+ return Some(id);
+ }
+
if self.failed[idx] {
return None;
}
- // Load the face if it's not already loaded.
- if slot.is_none() {
- let FaceInfo { ref path, index, .. } = infos[idx];
- self.failed[idx] = true;
-
- // Check the buffer cache since multiple faces 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(Arc::new(buffer))
- }
- };
+ let FaceInfo { ref path, index, .. } = self.loader.faces()[idx];
+ self.failed[idx] = true;
+
+ // Check the buffer cache since multiple faces 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(Arc::new(buffer))
+ }
+ };
- let face = Face::new(Arc::clone(buffer), index)?;
- *slot = Some(face);
- self.failed[idx] = false;
- }
+ let face = Face::new(Arc::clone(buffer), index)?;
+ *slot = Some(face);
+ self.failed[idx] = false;
Some(id)
}
/// Get a reference to a loaded face.
///
- /// This panics if no face with this `id` was loaded. This function should
- /// only be called with ids returned by this store's
- /// [`select()`](Self::select) method.
+ /// This panics if the face 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: FaceId) -> &Face {
self.faces[id.0 as usize].as_ref().expect("font face was not loaded")
}
- /// Returns an ordered iterator over all font family names this loader
- /// knows.
- pub fn families(&self) -> impl Iterator<Item = &str> + '_ {
+ /// An ordered iterator over all font families this loader knows and details
+ /// about the faces that are part of them.
+ pub fn families(
+ &self,
+ ) -> impl Iterator<Item = (&str, impl Iterator<Item = &FaceInfo>)> + '_ {
// Since the keys are lowercased, we instead use the family field of the
// first face's info.
let faces = self.loader.faces();
- self.families
- .values()
- .map(move |id| faces[id[0].0 as usize].family.as_str())
+ self.families.values().map(|ids| {
+ let family = faces[ids[0].0 as usize].family.as_str();
+ let infos = ids.iter().map(|&id| &faces[id.0 as usize]);
+ (family, infos)
+ })
}
}
+/// 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 font face.
pub struct Face {
/// The raw face data, possibly shared with other faces from the same
@@ -161,6 +244,71 @@ pub struct Face {
index: u32,
/// The underlying ttf-parser/rustybuzz face.
ttf: rustybuzz::Face<'static>,
+ /// The faces metrics.
+ metrics: FaceMetrics,
+}
+
+impl Face {
+ /// Parse a font face from a buffer and collection index.
+ pub fn new(buffer: Arc<Vec<u8>>, 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(buffer.as_ptr(), buffer.len()) };
+
+ let ttf = rustybuzz::Face::from_slice(slice, index)?;
+ let metrics = FaceMetrics::from_ttf(&ttf);
+
+ Some(Self { buffer, index, ttf, metrics })
+ }
+
+ /// The underlying buffer.
+ pub fn buffer(&self) -> &Arc<Vec<u8>> {
+ &self.buffer
+ }
+
+ /// 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 units per em.
+ pub fn units_per_em(&self) -> f64 {
+ self.metrics.units_per_em
+ }
+
+ /// Access the face's metrics.
+ pub fn metrics(&self) -> &FaceMetrics {
+ &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))
+ }
+}
+
+/// Metrics for a font face.
+#[derive(Debug, Copy, Clone)]
+pub struct FaceMetrics {
/// How many font units represent one em unit.
pub units_per_em: f64,
/// The distance from the baseline to the typographic ascender.
@@ -179,30 +327,10 @@ pub struct Face {
pub overline: LineMetrics,
}
-/// 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,
-}
-
-impl Face {
- /// Parse a font face from a buffer and collection index.
- pub fn new(buffer: Arc<Vec<u8>>, 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(buffer.as_ptr(), buffer.len()) };
-
- let ttf = rustybuzz::Face::from_slice(slice, index)?;
- let units_per_em = f64::from(ttf.units_per_em());
+impl FaceMetrics {
+ /// Extract the face's metrics.
+ pub fn from_ttf(ttf: &ttf_parser::Face) -> Self {
+ let units_per_em = f64::from(ttf.units_per_em().unwrap_or(0));
let to_em = |units| Em::from_units(units, units_per_em);
let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender()));
@@ -231,10 +359,7 @@ impl Face {
thickness: underline.thickness,
};
- Some(Self {
- buffer,
- index,
- ttf,
+ Self {
units_per_em,
ascender,
cap_height,
@@ -243,40 +368,11 @@ impl Face {
strikethrough,
underline,
overline,
- })
- }
-
- /// The underlying buffer.
- pub fn buffer(&self) -> &Arc<Vec<u8>> {
- &self.buffer
- }
-
- /// 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
- }
-
- /// 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))
+ }
}
/// Look up a vertical metric at the given font size.
- pub fn vertical_metric(&self, metric: VerticalFontMetric, size: Length) -> Length {
+ pub fn vertical(&self, metric: VerticalFontMetric, size: Length) -> Length {
match metric {
VerticalFontMetric::Ascender => self.ascender.resolve(size),
VerticalFontMetric::CapHeight => self.cap_height.resolve(size),
@@ -288,6 +384,16 @@ impl Face {
}
}
+/// 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 {
@@ -323,50 +429,112 @@ pub struct FaceInfo {
pub family: String,
/// Properties that distinguish this face from other faces in the same
/// family.
- #[serde(flatten)]
pub variant: FontVariant,
+ /// Whether the face is monospaced.
+ pub monospaced: bool,
+ /// Whether the face has serifs (if known).
+ pub serif: Option<bool>,
+ /// The unicode coverage of the face.
+ pub coverage: Coverage,
}
impl FaceInfo {
- /// Determine metadata about all faces that are found in the given data.
- pub fn parse<'a>(
+ /// Compute metadata for all faces in the given data.
+ pub fn from_data<'a>(
path: &'a Path,
data: &'a [u8],
) -> impl Iterator<Item = FaceInfo> + 'a {
let count = ttf_parser::fonts_in_collection(data).unwrap_or(1);
(0 .. count).filter_map(move |index| {
let face = ttf_parser::Face::from_slice(data, index).ok()?;
- let mut family = find_name(face.names(), name_id::TYPOGRAPHIC_FAMILY)
- .or_else(|| find_name(face.names(), name_id::FAMILY))?;
+ Self::from_ttf(path, index, &face)
+ })
+ }
- // Remove weird leading dot appearing in some fonts.
- if let Some(undotted) = family.strip_prefix('.') {
- family = undotted.to_string();
+ /// 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 some faces 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 = find_name(ttf, name_id::FULL_NAME)?;
}
+ trim_styles(&family).to_string()
+ };
- let variant = FontVariant {
- style: match (face.is_italic(), face.is_oblique()) {
- (false, false) => FontStyle::Normal,
- (true, _) => FontStyle::Italic,
- (_, true) => FontStyle::Oblique,
- },
- weight: FontWeight::from_number(face.weight().to_number()),
- stretch: FontStretch::from_number(face.width().to_number()),
+ 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,
};
- Some(FaceInfo {
- path: path.to_owned(),
- index,
- family,
- variant,
- })
+ 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.character_mapping_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(FaceInfo {
+ path: path.to_owned(),
+ index,
+ family,
+ variant,
+ monospaced: ttf.is_monospaced(),
+ serif,
+ coverage: Coverage::from_vec(codepoints),
})
}
}
-/// Find a decodable entry in a name table iterator.
-pub fn find_name(mut names: ttf_parser::Names<'_>, name_id: u16) -> Option<String> {
- names.find_map(|entry| {
+/// Try to find and decode the name with the given id.
+pub fn find_name(ttf: &ttf_parser::Face, name_id: u16) -> Option<String> {
+ ttf.names().find_map(|entry| {
if entry.name_id() == name_id {
if let Some(string) = entry.to_string() {
return Some(string);
@@ -381,8 +549,63 @@ pub fn find_name(mut names: ttf_parser::Names<'_>, name_id: u16) -> Option<Strin
})
}
+/// 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 face from other faces in the same family.
-#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
pub struct FontVariant {
/// The style of the face (normal / italic / oblique).
@@ -419,6 +642,19 @@ pub enum FontStyle {
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
@@ -572,6 +808,66 @@ impl Debug for FontStretch {
}
}
+/// 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::*;
@@ -589,4 +885,47 @@ mod tests {
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],
+ )
+ }
}
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs
index f20d6543..666e40a7 100644
--- a/src/library/math/mod.rs
+++ b/src/library/math/mod.rs
@@ -1,6 +1,7 @@
//! Mathematical formulas.
use crate::library::prelude::*;
+use crate::library::text::FontFamily;
/// A mathematical formula.
#[derive(Debug, Hash)]
@@ -13,6 +14,10 @@ pub struct MathNode {
#[node(showable)]
impl MathNode {
+ /// The raw text's font family. Just the normal text family if `none`.
+ pub const FAMILY: Smart<FontFamily> =
+ Smart::Custom(FontFamily::new("Latin Modern Math"));
+
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::show(Self {
formula: args.expect("formula")?,
@@ -23,17 +28,24 @@ impl MathNode {
impl Show for MathNode {
fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> {
- Ok(styles
+ let mut content = styles
.show(self, ctx, [
Value::Str(self.formula.clone()),
Value::Bool(self.display),
])?
- .unwrap_or_else(|| {
- let mut content = Content::Text(self.formula.trim().into());
- if self.display {
- content = Content::Block(content.pack());
- }
- content.monospaced()
- }))
+ .unwrap_or_else(|| Content::Text(self.formula.trim().into()));
+
+ let mut map = StyleMap::new();
+ if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) {
+ map.set_family(family, styles);
+ }
+
+ content = content.styled_with_map(map);
+
+ if self.display {
+ content = Content::Block(content.pack());
+ }
+
+ Ok(content)
}
}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 528a2ce7..bba002de 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -119,9 +119,6 @@ pub fn new() -> Scope {
std.def_const("top", Align::Top);
std.def_const("horizon", Align::Horizon);
std.def_const("bottom", Align::Bottom);
- std.def_const("serif", text::FontFamily::Serif);
- std.def_const("sans-serif", text::FontFamily::SansSerif);
- std.def_const("monospace", text::FontFamily::Monospace);
std
}
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs
index f1bc795f..7d3273f5 100644
--- a/src/library/structure/heading.rs
+++ b/src/library/structure/heading.rs
@@ -63,12 +63,7 @@ impl Show for HeadingNode {
map.set(TextNode::SIZE, resolve!(Self::SIZE));
if let Smart::Custom(family) = resolve!(Self::FAMILY) {
- map.set(
- TextNode::FAMILY,
- std::iter::once(family)
- .chain(styles.get_ref(TextNode::FAMILY).iter().cloned())
- .collect(),
- );
+ map.set_family(family, styles);
}
if let Smart::Custom(fill) = resolve!(Self::FILL) {
@@ -101,6 +96,7 @@ impl Show for HeadingNode {
}
let mut content = Content::sequence(seq).styled_with_map(map);
+
if resolve!(Self::BLOCK) {
content = Content::block(content);
}
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs
index 29c04b2d..b98eb0b2 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -94,10 +94,11 @@ pub fn decorate(
width: Length,
) {
let face = fonts.get(text.face_id);
+ let face_metrics = face.metrics();
let metrics = match deco.line {
- STRIKETHROUGH => face.strikethrough,
- OVERLINE => face.overline,
- UNDERLINE | _ => face.underline,
+ STRIKETHROUGH => face_metrics.strikethrough,
+ OVERLINE => face_metrics.overline,
+ UNDERLINE | _ => face_metrics.underline,
};
let evade = deco.evade && deco.line != STRIKETHROUGH;
@@ -146,7 +147,8 @@ pub fn decorate(
for glyph in text.glyphs.iter() {
let dx = glyph.x_offset.resolve(text.size) + x;
- let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw());
+ let mut builder =
+ BezPathBuilder::new(face_metrics.units_per_em, text.size, dx.to_raw());
let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
let path = builder.finish();
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
index eef7f6fb..2c163a59 100644
--- a/src/library/text/mod.rs
+++ b/src/library/text/mod.rs
@@ -29,13 +29,7 @@ pub struct TextNode;
impl TextNode {
/// A prioritized sequence of font families.
#[variadic]
- pub const FAMILY: Vec<FontFamily> = vec![FontFamily::SansSerif];
- /// The serif font family/families.
- pub const SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")];
- /// The sans-serif font family/families.
- pub const SANS_SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")];
- /// The monospace font family/families.
- pub const MONOSPACE: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")];
+ pub const FAMILY: Vec<FontFamily> = vec![FontFamily::new("IBM Plex Sans")];
/// Whether to allow font fallback when the primary font list contains no
/// match.
pub const FALLBACK: bool = true;
@@ -100,9 +94,6 @@ impl TextNode {
#[skip]
#[fold(bool::bitxor)]
pub const EMPH: bool = false;
- /// Whether a monospace font should be preferred.
- #[skip]
- pub const MONOSPACED: bool = false;
/// The case transformation that should be applied to the next.
#[skip]
pub const CASE: Option<Case> = None;
@@ -160,50 +151,11 @@ impl Show for EmphNode {
}
}
-/// A generic or named font family.
-#[derive(Clone, Eq, PartialEq, Hash)]
-pub enum FontFamily {
- /// A family that has "serifs", small strokes attached to letters.
- Serif,
- /// A family in which glyphs do not have "serifs", small attached strokes.
- SansSerif,
- /// A family in which (almost) all glyphs are of equal width.
- Monospace,
- /// A specific font family like "Arial".
- Named(NamedFamily),
-}
-
-impl Debug for FontFamily {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Serif => f.pad("serif"),
- Self::SansSerif => f.pad("sans-serif"),
- Self::Monospace => f.pad("monospace"),
- Self::Named(s) => s.fmt(f),
- }
- }
-}
-
-dynamic! {
- FontFamily: "font family",
- Value::Str(string) => Self::Named(NamedFamily::new(&string)),
-}
-
-castable! {
- Vec<FontFamily>,
- Expected: "string, generic family or array thereof",
- Value::Str(string) => vec![FontFamily::Named(NamedFamily::new(&string))],
- Value::Array(values) => {
- values.into_iter().filter_map(|v| v.cast().ok()).collect()
- },
- @family: FontFamily => vec![family.clone()],
-}
-
-/// A specific font family like "Arial".
+/// A font family like "Arial".
#[derive(Clone, Eq, PartialEq, Hash)]
-pub struct NamedFamily(EcoString);
+pub struct FontFamily(EcoString);
-impl NamedFamily {
+impl FontFamily {
/// Create a named font family variant.
pub fn new(string: &str) -> Self {
Self(string.to_lowercase().into())
@@ -215,20 +167,26 @@ impl NamedFamily {
}
}
-impl Debug for NamedFamily {
+impl Debug for FontFamily {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
castable! {
- Vec<NamedFamily>,
+ FontFamily,
+ Expected: "string",
+ Value::Str(string) => Self::new(&string),
+}
+
+castable! {
+ Vec<FontFamily>,
Expected: "string or array of strings",
- Value::Str(string) => vec![NamedFamily::new(&string)],
+ Value::Str(string) => vec![FontFamily::new(&string)],
Value::Array(values) => values
.into_iter()
.filter_map(|v| v.cast().ok())
- .map(|string: EcoString| NamedFamily::new(&string))
+ .map(|string: EcoString| FontFamily::new(&string))
.collect(),
}
diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs
index e225803f..5c2133c2 100644
--- a/src/library/text/raw.rs
+++ b/src/library/text/raw.rs
@@ -3,8 +3,8 @@ use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
use syntect::parsing::SyntaxSet;
+use super::{FontFamily, TextNode};
use crate::library::prelude::*;
-use crate::library::text::TextNode;
use crate::source::SourceId;
use crate::syntax::{self, RedNode};
@@ -26,6 +26,8 @@ pub struct RawNode {
#[node(showable)]
impl RawNode {
+ /// The raw text's font family. Just the normal text family if `none`.
+ pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
/// The language to syntax-highlight in.
pub const LANG: Option<EcoString> = None;
@@ -40,8 +42,14 @@ impl RawNode {
impl Show for RawNode {
fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> {
let lang = styles.get_ref(Self::LANG).as_ref();
+ let foreground = THEME
+ .settings
+ .foreground
+ .map(Color::from)
+ .unwrap_or(Color::BLACK)
+ .into();
- if let Some(content) = styles.show(self, ctx, [
+ let mut content = if let Some(content) = styles.show(self, ctx, [
Value::Str(self.text.clone()),
match lang {
Some(lang) => Value::Str(lang.clone()),
@@ -49,17 +57,8 @@ impl Show for RawNode {
},
Value::Bool(self.block),
])? {
- return Ok(content);
- }
-
- let foreground = THEME
- .settings
- .foreground
- .map(Color::from)
- .unwrap_or(Color::BLACK)
- .into();
-
- let mut content = if matches!(
+ content
+ } else if matches!(
lang.map(|s| s.to_lowercase()).as_deref(),
Some("typ" | "typst")
) {
@@ -93,11 +92,18 @@ impl Show for RawNode {
Content::Text(self.text.clone())
};
+ let mut map = StyleMap::new();
+ if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) {
+ map.set_family(family, styles);
+ }
+
+ content = content.styled_with_map(map);
+
if self.block {
content = Content::Block(content.pack());
}
- Ok(content.monospaced())
+ Ok(content)
}
}
diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs
index 29141331..6087032f 100644
--- a/src/library/text/shaping.rs
+++ b/src/library/text/shaping.rs
@@ -236,6 +236,18 @@ impl<'a> ShapedText<'a> {
}
}
+/// Holds shaping results and metadata common to all shaped segments.
+struct ShapingContext<'a> {
+ fonts: &'a mut FontStore,
+ glyphs: Vec<ShapedGlyph>,
+ used: Vec<FaceId>,
+ styles: StyleChain<'a>,
+ variant: FontVariant,
+ tags: Vec<rustybuzz::Feature>,
+ fallback: bool,
+ dir: Dir,
+}
+
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
fonts: &mut FontStore,
@@ -248,28 +260,24 @@ pub fn shape<'a>(
None => Cow::Borrowed(text),
};
- let mut glyphs = vec![];
+ let mut ctx = ShapingContext {
+ fonts,
+ glyphs: vec![],
+ used: vec![],
+ styles,
+ variant: variant(styles),
+ tags: tags(styles),
+ fallback: styles.get(TextNode::FALLBACK),
+ dir,
+ };
+
if !text.is_empty() {
- shape_segment(
- fonts,
- &mut glyphs,
- 0,
- &text,
- variant(styles),
- families(styles),
- None,
- dir,
- &tags(styles),
- );
- }
-
- track_and_space(
- &mut glyphs,
- styles.get(TextNode::TRACKING),
- styles.get(TextNode::SPACING),
- );
+ shape_segment(&mut ctx, 0, &text, families(styles));
+ }
+
+ track_and_space(&mut ctx);
- let (size, baseline) = measure(fonts, &glyphs, styles);
+ let (size, baseline) = measure(ctx.fonts, &ctx.glyphs, styles);
ShapedText {
text,
@@ -277,186 +285,62 @@ pub fn shape<'a>(
styles,
size,
baseline,
- glyphs: Cow::Owned(glyphs),
+ glyphs: Cow::Owned(ctx.glyphs),
}
}
-/// Resolve the font variant with `STRONG` and `EMPH` factored in.
-fn variant(styles: StyleChain) -> FontVariant {
- let mut variant = FontVariant::new(
- styles.get(TextNode::STYLE),
- styles.get(TextNode::WEIGHT),
- styles.get(TextNode::STRETCH),
- );
-
- if styles.get(TextNode::STRONG) {
- variant.weight = variant.weight.thicken(300);
- }
-
- if styles.get(TextNode::EMPH) {
- variant.style = match variant.style {
- FontStyle::Normal => FontStyle::Italic,
- FontStyle::Italic => FontStyle::Normal,
- FontStyle::Oblique => FontStyle::Normal,
- }
- }
-
- variant
-}
-
-/// Resolve a prioritized iterator over the font families.
-fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
- let head = if styles.get(TextNode::MONOSPACED) {
- styles.get_ref(TextNode::MONOSPACE).as_slice()
- } else {
- &[]
- };
-
- let core = styles.get_ref(TextNode::FAMILY).iter().flat_map(move |family| {
- match family {
- FontFamily::Named(name) => std::slice::from_ref(name),
- FontFamily::Serif => styles.get_ref(TextNode::SERIF),
- FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF),
- FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE),
- }
- });
-
- let tail: &[&str] = if styles.get(TextNode::FALLBACK) {
- &["ibm plex sans", "latin modern math", "twitter color emoji"]
- } else {
- &[]
- };
-
- head.iter()
- .chain(core)
- .map(|named| named.as_str())
- .chain(tail.iter().copied())
-}
-
-/// Collect the tags of the OpenType features to apply.
-fn tags(styles: StyleChain) -> Vec<Feature> {
- let mut tags = vec![];
- let mut feat = |tag, value| {
- tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
- };
-
- // Features that are on by default in Harfbuzz are only added if disabled.
- if !styles.get(TextNode::KERNING) {
- feat(b"kern", 0);
- }
-
- // Features that are off by default in Harfbuzz are only added if enabled.
- if styles.get(TextNode::SMALLCAPS) {
- feat(b"smcp", 1);
- }
-
- if styles.get(TextNode::ALTERNATES) {
- feat(b"salt", 1);
- }
-
- let storage;
- if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
- storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
- feat(&storage, 1);
- }
-
- if !styles.get(TextNode::LIGATURES) {
- feat(b"liga", 0);
- feat(b"clig", 0);
- }
-
- if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
- feat(b"dlig", 1);
- }
-
- if styles.get(TextNode::HISTORICAL_LIGATURES) {
- feat(b"hilg", 1);
- }
-
- match styles.get(TextNode::NUMBER_TYPE) {
- Smart::Auto => {}
- Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
- Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
- }
-
- match styles.get(TextNode::NUMBER_WIDTH) {
- Smart::Auto => {}
- Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
- Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
- }
-
- match styles.get(TextNode::NUMBER_POSITION) {
- NumberPosition::Normal => {}
- NumberPosition::Subscript => feat(b"subs", 1),
- NumberPosition::Superscript => feat(b"sups", 1),
- }
-
- if styles.get(TextNode::SLASHED_ZERO) {
- feat(b"zero", 1);
- }
-
- if styles.get(TextNode::FRACTIONS) {
- feat(b"frac", 1);
- }
-
- for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
- tags.push(Feature::new(tag, value, ..))
- }
-
- tags
-}
-
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
- fonts: &mut FontStore,
- glyphs: &mut Vec<ShapedGlyph>,
+ ctx: &mut ShapingContext,
base: usize,
text: &str,
- variant: FontVariant,
mut families: impl Iterator<Item = &'a str> + Clone,
- mut first_face: Option<FaceId>,
- dir: Dir,
- tags: &[rustybuzz::Feature],
) {
- // No font has newlines.
- if text.chars().all(|c| c == '\n') {
+ // Fonts dont have newlines and tabs.
+ if text.chars().all(|c| c == '\n' || c == '\t') {
return;
}
- // Select the font family.
- let (face_id, fallback) = loop {
- // Try to load the next available font family.
- match families.next() {
- Some(family) => {
- if let Some(id) = fonts.select(family, variant) {
- break (id, true);
- }
- }
- // We're out of families, so we don't do any more fallback and just
- // shape the tofus with the first face we originally used.
- None => match first_face {
- Some(id) => break (id, false),
- None => return,
- },
+ // Find the next available family.
+ let mut selection = families.find_map(|family| {
+ ctx.fonts
+ .select(family, ctx.variant)
+ .filter(|id| !ctx.used.contains(id))
+ });
+
+ // Do font fallback if the families are exhausted and fallback is enabled.
+ if selection.is_none() && ctx.fallback {
+ let first = ctx.used.first().copied();
+ selection = ctx
+ .fonts
+ .select_fallback(first, ctx.variant, text)
+ .filter(|id| !ctx.used.contains(id));
+ }
+
+ // Extract the face id or shape notdef glyphs if we couldn't find any face.
+ let face_id = if let Some(id) = selection {
+ id
+ } else {
+ if let Some(&face_id) = ctx.used.first() {
+ shape_tofus(ctx, base, text, face_id);
}
+ return;
};
- // Remember the id if this the first available face since we use that one to
- // shape tofus.
- first_face.get_or_insert(face_id);
+ ctx.used.push(face_id);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
- buffer.set_direction(match dir {
+ buffer.set_direction(match ctx.dir {
Dir::LTR => rustybuzz::Direction::LeftToRight,
Dir::RTL => rustybuzz::Direction::RightToLeft,
- _ => unimplemented!(),
+ _ => unimplemented!("vertical text layout"),
});
// Shape!
- let mut face = fonts.get(face_id);
- let buffer = rustybuzz::shape(face.ttf(), tags, buffer);
+ let mut face = ctx.fonts.get(face_id);
+ let buffer = rustybuzz::shape(face.ttf(), &ctx.tags, buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
@@ -467,10 +351,10 @@ fn shape_segment<'a>(
let info = &infos[i];
let cluster = info.cluster as usize;
- if info.glyph_id != 0 || !fallback {
+ if info.glyph_id != 0 {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
- glyphs.push(ShapedGlyph {
+ ctx.glyphs.push(ShapedGlyph {
face_id,
glyph_id: info.glyph_id as u16,
x_advance: face.to_em(pos[i].x_advance),
@@ -506,7 +390,7 @@ fn shape_segment<'a>(
// Glyphs: E C _ _ A
// Clusters: 8 6 4 2 0
// k=2 i=3
- let ltr = dir.is_positive();
+ let ltr = ctx.dir.is_positive();
let first = if ltr { k } else { i };
let start = infos[first].cluster as usize;
let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
@@ -517,33 +401,50 @@ fn shape_segment<'a>(
start .. end
};
+ // Trim half-baked cluster.
+ let remove = base + range.start .. base + range.end;
+ while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.cluster)) {
+ ctx.glyphs.pop();
+ }
+
// Recursively shape the tofu sequence with the next family.
- shape_segment(
- fonts,
- glyphs,
- base + range.start,
- &text[range],
- variant,
- families.clone(),
- first_face,
- dir,
- tags,
- );
-
- face = fonts.get(face_id);
+ shape_segment(ctx, base + range.start, &text[range], families.clone());
+
+ face = ctx.fonts.get(face_id);
}
i += 1;
}
+
+ ctx.used.pop();
+}
+
+/// Shape the text with tofus from the given face.
+fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, face_id: FaceId) {
+ let face = ctx.fonts.get(face_id);
+ let x_advance = face.advance(0).unwrap_or_default();
+ for (cluster, c) in text.char_indices() {
+ ctx.glyphs.push(ShapedGlyph {
+ face_id,
+ glyph_id: 0,
+ x_advance,
+ x_offset: Em::zero(),
+ cluster: base + cluster,
+ safe_to_break: true,
+ c,
+ });
+ }
}
/// Apply tracking and spacing to a slice of shaped glyphs.
-fn track_and_space(glyphs: &mut [ShapedGlyph], tracking: Em, spacing: Relative) {
+fn track_and_space(ctx: &mut ShapingContext) {
+ let tracking = ctx.styles.get(TextNode::TRACKING);
+ let spacing = ctx.styles.get(TextNode::SPACING);
if tracking.is_zero() && spacing.is_one() {
return;
}
- let mut glyphs = glyphs.iter_mut().peekable();
+ let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
if glyph.is_space() {
glyph.x_advance *= spacing.get();
@@ -572,8 +473,9 @@ fn measure(
// Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| {
- top.set_max(face.vertical_metric(top_edge, size));
- bottom.set_max(-face.vertical_metric(bottom_edge, size));
+ let metrics = face.metrics();
+ top.set_max(metrics.vertical(top_edge, size));
+ bottom.set_max(-metrics.vertical(bottom_edge, size));
};
if glyphs.is_empty() {
@@ -599,3 +501,117 @@ fn measure(
(Size::new(width, top + bottom), top)
}
+
+/// Resolve the font variant with `STRONG` and `EMPH` factored in.
+fn variant(styles: StyleChain) -> FontVariant {
+ let mut variant = FontVariant::new(
+ styles.get(TextNode::STYLE),
+ styles.get(TextNode::WEIGHT),
+ styles.get(TextNode::STRETCH),
+ );
+
+ if styles.get(TextNode::STRONG) {
+ variant.weight = variant.weight.thicken(300);
+ }
+
+ if styles.get(TextNode::EMPH) {
+ variant.style = match variant.style {
+ FontStyle::Normal => FontStyle::Italic,
+ FontStyle::Italic => FontStyle::Normal,
+ FontStyle::Oblique => FontStyle::Normal,
+ }
+ }
+
+ variant
+}
+
+/// Resolve a prioritized iterator over the font families.
+fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
+ const FALLBACKS: &[&str] = &[
+ "ibm plex sans",
+ "twitter color emoji",
+ "noto color emoji",
+ "apple color emoji",
+ "segoe ui emoji",
+ ];
+
+ let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] };
+ styles
+ .get_ref(TextNode::FAMILY)
+ .iter()
+ .map(|family| family.as_str())
+ .chain(tail.iter().copied())
+}
+
+/// Collect the tags of the OpenType features to apply.
+fn tags(styles: StyleChain) -> Vec<Feature> {
+ let mut tags = vec![];
+ let mut feat = |tag, value| {
+ tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
+ };
+
+ // Features that are on by default in Harfbuzz are only added if disabled.
+ if !styles.get(TextNode::KERNING) {
+ feat(b"kern", 0);
+ }
+
+ // Features that are off by default in Harfbuzz are only added if enabled.
+ if styles.get(TextNode::SMALLCAPS) {
+ feat(b"smcp", 1);
+ }
+
+ if styles.get(TextNode::ALTERNATES) {
+ feat(b"salt", 1);
+ }
+
+ let storage;
+ if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
+ storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
+ feat(&storage, 1);
+ }
+
+ if !styles.get(TextNode::LIGATURES) {
+ feat(b"liga", 0);
+ feat(b"clig", 0);
+ }
+
+ if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
+ feat(b"dlig", 1);
+ }
+
+ if styles.get(TextNode::HISTORICAL_LIGATURES) {
+ feat(b"hilg", 1);
+ }
+
+ match styles.get(TextNode::NUMBER_TYPE) {
+ Smart::Auto => {}
+ Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
+ Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
+ }
+
+ match styles.get(TextNode::NUMBER_WIDTH) {
+ Smart::Auto => {}
+ Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
+ Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
+ }
+
+ match styles.get(TextNode::NUMBER_POSITION) {
+ NumberPosition::Normal => {}
+ NumberPosition::Subscript => feat(b"subs", 1),
+ NumberPosition::Superscript => feat(b"sups", 1),
+ }
+
+ if styles.get(TextNode::SLASHED_ZERO) {
+ feat(b"zero", 1);
+ }
+
+ if styles.get(TextNode::FRACTIONS) {
+ feat(b"frac", 1);
+ }
+
+ for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
+ tags.push(Feature::new(tag, value, ..))
+ }
+
+ tags
+}
diff --git a/src/loading/fs.rs b/src/loading/fs.rs
index 58f2b1e2..3398ebd3 100644
--- a/src/loading/fs.rs
+++ b/src/loading/fs.rs
@@ -116,7 +116,7 @@ impl FsLoader {
let path = path.strip_prefix(".").unwrap_or(path);
if let Ok(file) = File::open(path) {
if let Ok(mmap) = unsafe { Mmap::map(&file) } {
- self.faces.extend(FaceInfo::parse(path, &mmap));
+ self.faces.extend(FaceInfo::from_data(path, &mmap));
}
}
}
@@ -142,34 +142,3 @@ impl Loader for FsLoader {
fs::read(path)
}
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_index_font_dir() {
- let faces = FsLoader::new().with_path("fonts").faces;
- let mut paths: Vec<_> = faces.into_iter().map(|info| info.path).collect();
- paths.sort();
-
- assert_eq!(paths, [
- Path::new("fonts/CMU-Serif-Bold.ttf"),
- Path::new("fonts/CMU-Serif-Regular.ttf"),
- Path::new("fonts/IBMPlexMono-Regular.ttf"),
- Path::new("fonts/IBMPlexSans-Bold.ttf"),
- Path::new("fonts/IBMPlexSans-BoldItalic.ttf"),
- Path::new("fonts/IBMPlexSans-Italic.ttf"),
- Path::new("fonts/IBMPlexSans-Regular.ttf"),
- Path::new("fonts/IBMPlexSerif-Regular.ttf"),
- Path::new("fonts/LatinModernMath.otf"),
- Path::new("fonts/NotoSansArabic-Regular.ttf"),
- Path::new("fonts/NotoSerifCJKsc-Regular.otf"),
- Path::new("fonts/NotoSerifHebrew-Bold.ttf"),
- Path::new("fonts/NotoSerifHebrew-Regular.ttf"),
- Path::new("fonts/PTSans-Regular.ttf"),
- Path::new("fonts/Roboto-Regular.ttf"),
- Path::new("fonts/TwitterColorEmoji.ttf"),
- ]);
- }
-}
diff --git a/src/loading/mem.rs b/src/loading/mem.rs
index 5e3e78d1..d4c0e7e4 100644
--- a/src/loading/mem.rs
+++ b/src/loading/mem.rs
@@ -49,7 +49,7 @@ impl MemLoader {
{
let path = path.as_ref().normalize();
let data = data.into();
- self.faces.extend(FaceInfo::parse(&path, &data));
+ self.faces.extend(FaceInfo::from_data(&path, &data));
self.files.insert(path, data);
}
}