From 98336bfafb947f0b4d55a79c422b915bb417c185 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 22 Mar 2021 14:08:50 +0100 Subject: [PATCH] =?UTF-8?q?Better=20font=20family=20definitions=20?= =?UTF-8?q?=E2=9C=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bench/src/bench.rs | 2 +- src/env.rs | 10 +---- src/exec/context.rs | 5 +-- src/exec/state.rs | 81 ++++++++++++++++++++++++++++++++----- src/layout/shaping.rs | 51 ++++++++++++----------- src/layout/text.rs | 7 ++-- src/library/font.rs | 72 ++++++++++----------------------- src/library/mod.rs | 2 +- src/main.rs | 2 +- tests/ref/library/font.png | Bin 10697 -> 12348 bytes tests/typ/library/font.typ | 9 ++++- tests/typeset.rs | 2 +- 12 files changed, 139 insertions(+), 104 deletions(-) diff --git a/bench/src/bench.rs b/bench/src/bench.rs index 36b8b1376..c3758dc5d 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use fontdock::fs::FsIndex; +use fontdock::FsIndex; use typst::env::{Env, FsIndexExt, ResourceLoader}; use typst::eval::eval; diff --git a/src/env.rs b/src/env.rs index 3db71e087..75e2853a5 100644 --- a/src/env.rs +++ b/src/env.rs @@ -7,13 +7,13 @@ use std::fs; use std::io::Cursor; use std::path::{Path, PathBuf}; -use fontdock::{ContainsChar, FaceFromVec, FaceId, FontSource}; +use fontdock::{FaceFromVec, FaceId, FontSource}; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; use ttf_parser::Face; #[cfg(feature = "fs")] -use fontdock::fs::{FsIndex, FsSource}; +use fontdock::{FsIndex, FsSource}; /// Encapsulates all environment dependencies (fonts, resources). #[derive(Debug)] @@ -83,12 +83,6 @@ impl FaceFromVec for FaceBuf { } } -impl ContainsChar for FaceBuf { - fn contains_char(&self, c: char) -> bool { - self.get().glyph_index(c).is_some() - } -} - /// Simplify font loader construction from an [`FsIndex`]. #[cfg(feature = "fs")] pub trait FsIndexExt { diff --git a/src/exec/context.rs b/src/exec/context.rs index f19f6561b..6ba5b25f5 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use fontdock::FontStyle; -use super::{Exec, State}; +use super::{Exec, FontFamily, State}; use crate::diag::{Diag, DiagSet, Pass}; use crate::env::Env; use crate::eval::TemplateValue; @@ -74,8 +74,7 @@ impl<'a> ExecContext<'a> { /// Set the font to monospace. pub fn set_monospace(&mut self) { let families = self.state.font.families_mut(); - families.list.insert(0, "monospace".to_string()); - families.flatten(); + families.list.insert(0, FontFamily::Monospace); } /// Push a layout node into the active paragraph. diff --git a/src/exec/state.rs b/src/exec/state.rs index f66694fd7..0322c4376 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -1,6 +1,7 @@ +use std::fmt::{self, Display, Formatter}; use std::rc::Rc; -use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; +use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight}; use crate::color::{Color, RgbaColor}; use crate::geom::*; @@ -98,8 +99,8 @@ impl Default for ParState { /// Defines font properties. #[derive(Debug, Clone, PartialEq)] pub struct FontState { - /// A tree of font family names and generic class names. - pub families: Rc, + /// A list of font families with generic class definitions. + pub families: Rc, /// The selected font variant. pub variant: FontVariant, /// The font size. @@ -122,7 +123,7 @@ pub struct FontState { impl FontState { /// Access the `families` mutably. - pub fn families_mut(&mut self) -> &mut FallbackTree { + pub fn families_mut(&mut self) -> &mut FamilyMap { Rc::make_mut(&mut self.families) } @@ -135,12 +136,7 @@ impl FontState { impl Default for FontState { fn default() -> Self { Self { - // The default tree of font fallbacks. - families: Rc::new(fallback! { - list: [], - classes: { "monospace" => ["inconsolata"] }, - base: ["eb garamond", "twitter color emoji"], - }), + families: Rc::new(FamilyMap::default()), variant: FontVariant { style: FontStyle::Normal, weight: FontWeight::REGULAR, @@ -156,3 +152,68 @@ impl Default for FontState { } } } + +/// Font family definitions. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct FamilyMap { + /// The user-defined list of font families. + pub list: Vec, + /// Definition of serif font families. + pub serif: Vec, + /// Definition of sans-serif font families. + pub sans_serif: Vec, + /// Definition of monospace font families used for raw text. + pub monospace: Vec, + /// Base fonts that are tried if the list has no match. + pub base: Vec, +} + +impl FamilyMap { + /// Flat iterator over this map's family names. + pub fn iter(&self) -> impl Iterator { + self.list + .iter() + .flat_map(move |family: &FontFamily| { + match family { + FontFamily::Named(name) => std::slice::from_ref(name), + FontFamily::Serif => &self.serif, + FontFamily::SansSerif => &self.sans_serif, + FontFamily::Monospace => &self.monospace, + } + }) + .chain(&self.base) + .map(String::as_str) + } +} + +impl Default for FamilyMap { + fn default() -> Self { + Self { + list: vec![FontFamily::Serif], + serif: vec!["eb garamond".into()], + sans_serif: vec![/* TODO */], + monospace: vec!["inconsolata".into()], + base: vec!["twitter color emoji".into()], + } + } +} + +/// A generic or named font family. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum FontFamily { + Serif, + SansSerif, + Monospace, + Named(String), +} + +impl Display for FontFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Serif => "serif", + Self::SansSerif => "sans-serif", + Self::Monospace => "monospace", + Self::Named(s) => s, + }) + } +} diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index df27e2871..fd10b41ed 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -6,10 +6,11 @@ use std::fmt::{self, Debug, Display, Formatter}; -use fontdock::{FaceId, FaceQuery, FallbackTree, FontVariant}; +use fontdock::{FaceId, FontVariant}; use ttf_parser::{Face, GlyphId}; use crate::env::FontLoader; +use crate::exec::FamilyMap; use crate::geom::{Dir, Length, Point, Size}; use crate::layout::{Element, Fill, Frame}; @@ -40,7 +41,7 @@ impl Shaped { glyphs: vec![], offsets: vec![], font_size, - color: color, + color, } } @@ -98,7 +99,7 @@ impl Display for VerticalFontMetric { pub fn shape( text: &str, dir: Dir, - fallback: &FallbackTree, + families: &FamilyMap, variant: FontVariant, font_size: Length, top_edge: VerticalFontMetric, @@ -122,31 +123,33 @@ pub fn shape( }; for c in chars { - let query = FaceQuery { fallback: fallback.iter(), variant, c }; - if let Some(id) = loader.query(query) { - let face = loader.face(id).get(); - let (glyph, glyph_width) = match lookup_glyph(face, c) { - Some(v) => v, - None => continue, - }; + for family in families.iter() { + if let Some(id) = loader.query(family, variant) { + let face = loader.face(id).get(); + let (glyph, glyph_width) = match lookup_glyph(face, c) { + Some(v) => v, + None => continue, + }; - let units_per_em = f64::from(face.units_per_em().unwrap_or(1000)); - let convert = |units| units / units_per_em * font_size; + let units_per_em = f64::from(face.units_per_em().unwrap_or(1000)); + let convert = |units| units / units_per_em * font_size; - // Flush the buffer and reset the metrics if we use a new font face. - if shaped.face != id { - place(&mut frame, shaped, width, top, bottom); + // Flush the buffer and reset the metrics if we use a new font face. + if shaped.face != id { + place(&mut frame, shaped, width, top, bottom); - shaped = Shaped::new(id, font_size, color); - width = Length::ZERO; - top = convert(f64::from(lookup_metric(face, top_edge))); - bottom = convert(f64::from(lookup_metric(face, bottom_edge))); + shaped = Shaped::new(id, font_size, color); + width = Length::ZERO; + top = convert(f64::from(lookup_metric(face, top_edge))); + bottom = convert(f64::from(lookup_metric(face, bottom_edge))); + } + + shaped.text.push(c); + shaped.glyphs.push(glyph); + shaped.offsets.push(width); + width += convert(f64::from(glyph_width)); + break; } - - shaped.text.push(c); - shaped.glyphs.push(glyph); - shaped.offsets.push(width); - width += convert(f64::from(glyph_width)); } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 7f8f97cc6..2239afac1 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -1,9 +1,10 @@ use std::fmt::{self, Debug, Formatter}; use std::rc::Rc; -use fontdock::{FallbackTree, FontVariant}; +use fontdock::FontVariant; use super::*; +use crate::exec::FamilyMap; /// A consecutive, styled run of text. #[derive(Clone, PartialEq)] @@ -14,8 +15,8 @@ pub struct TextNode { pub dir: Dir, /// How to align this text node in its parent. pub aligns: LayoutAligns, - /// The families used for font fallback. - pub families: Rc, + /// The list of font families for shaping. + pub families: Rc, /// The font variant, pub variant: FontVariant, /// The font size. diff --git a/src/library/font.rs b/src/library/font.rs index ed2c0ef3c..6afc617b0 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -17,9 +17,9 @@ use super::*; /// - Top edge of the font: `top-edge`, of type `vertical-font-metric`. /// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`. /// - Color the glyphs: `color`, of type `color`. -/// - Serif family definition: `serif`, of type `font-familiy-list`. -/// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`. -/// - Monospace family definition: `monospace`, of type `font-familiy-list`. +/// - Serif family definition: `serif`, of type `font-family-definition`. +/// - Sans-serif family definition: `sans-serif`, of type `font-family-definition`. +/// - Monospace family definition: `monospace`, of type `font-family-definition`. /// /// # Return value /// A template that configures font properties. The effect is scoped to the body @@ -31,10 +31,9 @@ use super::*; /// - `sans-serif` /// - `monospace` /// - coerces from `string` -/// - Type `font-family-list` +/// - Type `font-family-definition` /// - coerces from `string` /// - coerces from `array` -/// - coerces from `font-family` /// - Type `font-style` /// - `normal` /// - `italic` @@ -58,7 +57,7 @@ use super::*; /// - `descender` pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let size = args.find::(ctx); - let list: Vec<_> = args.filter::(ctx).map(|f| f.to_string()).collect(); + let list: Vec<_> = args.filter::(ctx).collect(); let style = args.get(ctx, "style"); let weight = args.get(ctx, "weight"); let stretch = args.get(ctx, "stretch"); @@ -83,9 +82,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } if !list.is_empty() { - let families = ctx.state.font.families_mut(); - families.list = list.clone(); - families.flatten(); + ctx.state.font.families_mut().list = list.clone(); } if let Some(style) = style { @@ -112,17 +109,16 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { ctx.state.font.color = Fill::Color(color); } - for (variant, arg) in &[ - (FontFamily::Serif, &serif), - (FontFamily::SansSerif, &sans_serif), - (FontFamily::Monospace, &monospace), - ] { - if let Some(FontFamilies(list)) = arg { - let strings = list.into_iter().map(|f| f.to_string()).collect(); - let families = ctx.state.font.families_mut(); - families.update_class_list(variant.to_string(), strings); - families.flatten(); - } + if let Some(FontFamilies(serif)) = &serif { + ctx.state.font.families_mut().serif = serif.clone(); + } + + if let Some(FontFamilies(sans_serif)) = &sans_serif { + ctx.state.font.families_mut().sans_serif = sans_serif.clone(); + } + + if let Some(FontFamilies(monospace)) = &monospace { + ctx.state.font.families_mut().monospace = monospace.clone(); } if let Some(body) = &body { @@ -132,45 +128,19 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { }) } -/// A list of font families. +/// A list of font family names. #[derive(Debug, Clone, PartialEq)] -struct FontFamilies(Vec); - -/// A single font family. -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(super) enum FontFamily { - Serif, - SansSerif, - Monospace, - Named(String), -} - -impl FontFamily { - pub fn as_str(&self) -> &str { - match self { - Self::Serif => "serif", - Self::SansSerif => "sans-serif", - Self::Monospace => "monospace", - Self::Named(s) => s, - } - } -} - -impl Display for FontFamily { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(self.as_str()) - } -} +struct FontFamilies(Vec); typify! { - FontFamilies: "font family or array of font families", - Value::Str(string) => Self(vec![FontFamily::Named(string.to_lowercase())]), + FontFamilies: "string or array of strings", + Value::Str(string) => Self(vec![string.to_lowercase()]), Value::Array(values) => Self(values .into_iter() .filter_map(|v| v.cast().ok()) + .map(|string: String| string.to_lowercase()) .collect() ), - #(family: FontFamily) => Self(vec![family]), } typify! { diff --git a/src/library/mod.rs b/src/library/mod.rs index 62640ef48..58e62d566 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -31,7 +31,7 @@ use fontdock::{FontStyle, FontWeight}; use crate::eval::{AnyValue, FuncValue, Scope}; use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value}; -use crate::exec::{Exec, ExecContext}; +use crate::exec::{Exec, ExecContext, FontFamily}; use crate::geom::*; use crate::layout::VerticalFontMetric; use crate::syntax::{Node, Spanned}; diff --git a/src/main.rs b/src/main.rs index 74ec743df..9d72440ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::fs; use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, Context}; -use fontdock::fs::FsIndex; +use fontdock::FsIndex; use typst::diag::Pass; use typst::env::{Env, FsIndexExt, ResourceLoader}; diff --git a/tests/ref/library/font.png b/tests/ref/library/font.png index 1cfbc49970abf51d9fbf483be7be1ba85ce94069..0e24bb5d3534a76b3a2c08d3e3545440fa5419aa 100644 GIT binary patch delta 11626 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 diff --git a/tests/typ/library/font.typ b/tests/typ/library/font.typ index 3e818f197..13f711250 100644 --- a/tests/typ/library/font.typ +++ b/tests/typ/library/font.typ @@ -48,6 +48,13 @@ Emoji: 🐪, 🌋, 🏞 #try(cap-height, baseline) #try(x-height, baseline) +--- +// Test class definitions. +#font(sans-serif: "PT Sans") +#font(sans-serif)[Sans-serif.] \ +#font(monospace)[Monospace.] \ +#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] \ + --- // Ref: false @@ -56,7 +63,7 @@ Emoji: 🐪, 🌋, 🏞 // Error: 3:14-3:18 expected font style, found font weight // Error: 2:28-2:34 expected font weight, found string -// Error: 1:43-1:44 expected font family or array of font families, found integer +// Error: 1:43-1:44 expected string or array of strings, found integer #font(style: bold, weight: "thin", serif: 0) // Warning: 15-19 should be between 100 and 900 diff --git a/tests/typeset.rs b/tests/typeset.rs index 0a22c96be..5a59a129c 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -5,7 +5,7 @@ use std::fs; use std::path::Path; use std::rc::Rc; -use fontdock::fs::FsIndex; +use fontdock::FsIndex; use image::{GenericImageView, Rgba}; use tiny_skia::{ Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect,