diff --git a/Cargo.lock b/Cargo.lock index 288d5dfff..5287d230a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,6 +833,7 @@ checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" name = "typst" version = "0.1.0" dependencies = [ + "bitflags", "bytemuck", "codespan-reporting", "csv", diff --git a/Cargo.toml b/Cargo.toml index 998177163..b2d9d5576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,14 @@ edition = "2021" [features] default = ["fs"] cli = ["fs", "pico-args", "codespan-reporting", "same-file"] -fs = ["dirs", "memmap2", "same-file", "walkdir"] +fs = ["dirs", "memmap2", "walkdir", "same-file"] [dependencies] # Workspace typst-macros = { path = "./macros" } # Utilities +bitflags = "1" bytemuck = "1" fxhash = "0.2" lipsum = { git = "https://github.com/reknih/lipsum" } diff --git a/benches/oneshot.rs b/benches/oneshot.rs index baaf18f08..c224d1bf8 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -1,10 +1,12 @@ +use std::io; use std::path::Path; use std::sync::Arc; use iai::{black_box, main, Iai}; use unscanny::Scanner; -use typst::loading::MemLoader; +use typst::font::{Font, FontBook}; +use typst::loading::{Buffer, FileHash, Loader}; use typst::parse::{TokenMode, Tokens}; use typst::source::SourceId; use typst::{Config, Context}; @@ -13,7 +15,7 @@ const SRC: &str = include_str!("bench.typ"); const FONT: &[u8] = include_bytes!("../fonts/IBMPlexSans-Regular.ttf"); fn context() -> (Context, SourceId) { - let loader = MemLoader::new().with(Path::new("font.ttf"), FONT); + let loader = BenchLoader::new(); let mut ctx = Context::new(Arc::new(loader), Config::default()); let id = ctx.sources.provide(Path::new("src.typ"), SRC.to_string()); (ctx, id) @@ -94,5 +96,36 @@ fn bench_layout(iai: &mut Iai) { fn bench_render(iai: &mut Iai) { let (mut ctx, id) = context(); let frames = typst::typeset(&mut ctx, id).unwrap(); - iai.run(|| typst::export::render(&mut ctx, &frames[0], 1.0)) + iai.run(|| typst::export::render(&frames[0], 1.0)) +} + +struct BenchLoader { + book: FontBook, + font: Font, +} + +impl BenchLoader { + fn new() -> Self { + let font = Font::new(FONT.into(), 0).unwrap(); + let book = FontBook::from_fonts([&font]); + Self { book, font } + } +} + +impl Loader for BenchLoader { + fn book(&self) -> &FontBook { + &self.book + } + + fn font(&self, _: usize) -> io::Result { + Ok(self.font.clone()) + } + + fn resolve(&self, _: &Path) -> io::Result { + Err(io::ErrorKind::NotFound.into()) + } + + fn file(&self, _: &Path) -> io::Result { + Err(io::ErrorKind::NotFound.into()) + } } diff --git a/src/export/pdf/font.rs b/src/export/pdf/font.rs index b3481c437..446d36bc7 100644 --- a/src/export/pdf/font.rs +++ b/src/export/pdf/font.rs @@ -9,7 +9,7 @@ use crate::util::SliceExt; /// Embed all used fonts into the PDF. pub fn write_fonts(ctx: &mut PdfContext) { - for &font_id in ctx.font_map.items() { + for font in ctx.font_map.items() { let type0_ref = ctx.alloc.bump(); let cid_ref = ctx.alloc.bump(); let descriptor_ref = ctx.alloc.bump(); @@ -17,8 +17,7 @@ pub fn write_fonts(ctx: &mut PdfContext) { let data_ref = ctx.alloc.bump(); ctx.font_refs.push(type0_ref); - let glyphs = &ctx.glyph_sets[&font_id]; - let font = ctx.fonts.get(font_id); + let glyphs = &ctx.glyph_sets[font]; let metrics = font.metrics(); let ttf = font.ttf(); @@ -161,7 +160,7 @@ pub fn write_fonts(ctx: &mut PdfContext) { .filter(Filter::FlateDecode); // Subset and write the font's bytes. - let data = font.buffer(); + let data = font.data(); let subsetted = { let glyphs: Vec<_> = glyphs.iter().copied().collect(); let profile = subsetter::Profile::pdf(&glyphs); diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs index 7468f7d74..a87f5c6be 100644 --- a/src/export/pdf/mod.rs +++ b/src/export/pdf/mod.rs @@ -14,12 +14,11 @@ use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; use self::outline::{Heading, HeadingNode}; use self::page::Page; -use crate::font::{FontId, FontStore}; +use crate::font::Font; use crate::frame::Frame; use crate::geom::{Dir, Em, Length}; use crate::image::Image; use crate::library::text::Lang; -use crate::Context; /// Export a collection of frames into a PDF file. /// @@ -28,8 +27,8 @@ use crate::Context; /// included in the PDF. /// /// Returns the raw bytes making up the PDF file. -pub fn pdf(ctx: &Context, frames: &[Frame]) -> Vec { - let mut ctx = PdfContext::new(ctx); +pub fn pdf(frames: &[Frame]) -> Vec { + let mut ctx = PdfContext::new(); page::construct_pages(&mut ctx, frames); font::write_fonts(&mut ctx); image::write_images(&mut ctx); @@ -43,9 +42,8 @@ const SRGB: Name<'static> = Name(b"srgb"); const D65_GRAY: Name<'static> = Name(b"d65gray"); /// Context for exporting a whole PDF document. -pub struct PdfContext<'a> { +pub struct PdfContext { writer: PdfWriter, - fonts: &'a FontStore, pages: Vec, page_heights: Vec, alloc: Ref, @@ -53,20 +51,19 @@ pub struct PdfContext<'a> { font_refs: Vec, image_refs: Vec, page_refs: Vec, - font_map: Remapper, + font_map: Remapper, image_map: Remapper, - glyph_sets: HashMap>, + glyph_sets: HashMap>, languages: HashMap, heading_tree: Vec, } -impl<'a> PdfContext<'a> { - fn new(ctx: &'a Context) -> Self { +impl PdfContext { + fn new() -> Self { let mut alloc = Ref::new(1); let page_tree_ref = alloc.bump(); Self { writer: PdfWriter::new(), - fonts: &ctx.fonts, pages: vec![], page_heights: vec![], alloc, diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 6df7531bd..708eafadf 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -5,7 +5,7 @@ use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; use super::{ deflate, EmExt, Heading, HeadingNode, LengthExt, PdfContext, RefExt, D65_GRAY, SRGB, }; -use crate::font::FontId; +use crate::font::Font; use crate::frame::{Destination, Element, Frame, Group, Role, Text}; use crate::geom::{ self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, @@ -154,8 +154,8 @@ pub struct Page { } /// An exporter for the contents of a single PDF page. -struct PageContext<'a, 'b> { - parent: &'a mut PdfContext<'b>, +struct PageContext<'a> { + parent: &'a mut PdfContext, page_ref: Ref, content: Content, state: State, @@ -169,14 +169,14 @@ struct PageContext<'a, 'b> { #[derive(Debug, Default, Clone)] struct State { transform: Transform, - font: Option<(FontId, Length)>, + font: Option<(Font, Length)>, fill: Option, fill_space: Option>, stroke: Option, stroke_space: Option>, } -impl<'a, 'b> PageContext<'a, 'b> { +impl<'a> PageContext<'a> { fn save_state(&mut self) { self.saves.push(self.state.clone()); self.content.save_state(); @@ -200,12 +200,12 @@ impl<'a, 'b> PageContext<'a, 'b> { ]); } - fn set_font(&mut self, font_id: FontId, size: Length) { - if self.state.font != Some((font_id, size)) { - self.parent.font_map.insert(font_id); - let name = format_eco!("F{}", self.parent.font_map.map(font_id)); + fn set_font(&mut self, font: &Font, size: Length) { + if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { + self.parent.font_map.insert(font.clone()); + let name = format_eco!("F{}", self.parent.font_map.map(font.clone())); self.content.set_font(Name(name.as_bytes()), size.to_f32()); - self.state.font = Some((font_id, size)); + self.state.font = Some((font.clone(), size)); } } @@ -328,17 +328,15 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &Text) { *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); ctx.parent .glyph_sets - .entry(text.font_id) + .entry(text.font.clone()) .or_default() .extend(text.glyphs.iter().map(|g| g.id)); - let font = ctx.parent.fonts.get(text.font_id); - ctx.set_fill(text.fill); - ctx.set_font(text.font_id, text.size); + ctx.set_font(&text.font, text.size); ctx.content.begin_text(); - // Position the text. + // Positiosn the text. ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); let mut positioned = ctx.content.show_positioned(); @@ -363,7 +361,7 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &Text) { encoded.push((glyph.id >> 8) as u8); encoded.push((glyph.id & 0xff) as u8); - if let Some(advance) = font.advance(glyph.id) { + if let Some(advance) = text.font.advance(glyph.id) { adjustment += glyph.x_advance - advance; } diff --git a/src/export/render.rs b/src/export/render.rs index 688cf9799..bf735ded8 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -13,7 +13,6 @@ use crate::geom::{ self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform, }; use crate::image::{DecodedImage, Image}; -use crate::Context; /// Export a frame into a rendered image. /// @@ -22,7 +21,7 @@ use crate::Context; /// /// In addition to the frame, you need to pass in the context used during /// compilation so that fonts and images can be rendered. -pub fn render(ctx: &Context, frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap { +pub fn render(frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap { let size = frame.size(); let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; @@ -31,7 +30,7 @@ pub fn render(ctx: &Context, frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap { canvas.fill(sk::Color::WHITE); let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); - render_frame(&mut canvas, ts, None, ctx, frame); + render_frame(&mut canvas, ts, None, frame); canvas } @@ -41,7 +40,6 @@ fn render_frame( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: Option<&sk::ClipMask>, - ctx: &Context, frame: &Frame, ) { for (pos, element) in frame.elements() { @@ -51,10 +49,10 @@ fn render_frame( match element { Element::Group(group) => { - render_group(canvas, ts, mask, ctx, group); + render_group(canvas, ts, mask, group); } Element::Text(text) => { - render_text(canvas, ts, mask, ctx, text); + render_text(canvas, ts, mask, text); } Element::Shape(shape) => { render_shape(canvas, ts, mask, shape); @@ -72,7 +70,6 @@ fn render_group( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: Option<&sk::ClipMask>, - ctx: &Context, group: &Group, ) { let ts = ts.pre_concat(group.transform.into()); @@ -107,7 +104,7 @@ fn render_group( } } - render_frame(canvas, ts, mask, ctx, &group.frame); + render_frame(canvas, ts, mask, &group.frame); } /// Render a text run into the canvas. @@ -115,7 +112,6 @@ fn render_text( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: Option<&sk::ClipMask>, - ctx: &Context, text: &Text, ) { let mut x = 0.0; @@ -124,9 +120,9 @@ fn render_text( let offset = x + glyph.x_offset.at(text.size).to_f32(); let ts = ts.pre_translate(offset, 0.0); - render_svg_glyph(canvas, ts, mask, ctx, text, id) - .or_else(|| render_bitmap_glyph(canvas, ts, mask, ctx, text, id)) - .or_else(|| render_outline_glyph(canvas, ts, mask, ctx, text, id)); + render_svg_glyph(canvas, ts, mask, text, id) + .or_else(|| render_bitmap_glyph(canvas, ts, mask, text, id)) + .or_else(|| render_outline_glyph(canvas, ts, mask, text, id)); x += glyph.x_advance.at(text.size).to_f32(); } @@ -137,12 +133,10 @@ fn render_svg_glyph( canvas: &mut sk::Pixmap, ts: sk::Transform, _: Option<&sk::ClipMask>, - ctx: &Context, text: &Text, id: GlyphId, ) -> Option<()> { - let font = ctx.fonts.get(text.font_id); - let mut data = font.ttf().glyph_svg_image(id)?; + let mut data = text.font.ttf().glyph_svg_image(id)?; // Decompress SVGZ. let mut decoded = vec![]; @@ -164,7 +158,7 @@ fn render_svg_glyph( // If there's no viewbox defined, use the em square for our scale // transformation ... - let upem = font.units_per_em() as f32; + let upem = text.font.units_per_em() as f32; let (mut width, mut height) = (upem, upem); // ... but if there's a viewbox or width, use that. @@ -188,14 +182,12 @@ fn render_bitmap_glyph( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: Option<&sk::ClipMask>, - ctx: &Context, text: &Text, id: GlyphId, ) -> Option<()> { let size = text.size.to_f32(); let ppem = size * ts.sy; - let font = ctx.fonts.get(text.font_id); - let raster = font.ttf().glyph_raster_image(id, ppem as u16)?; + let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?; let ext = match raster.format { ttf_parser::RasterImageFormat::PNG => "png", }; @@ -217,7 +209,6 @@ fn render_outline_glyph( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: Option<&sk::ClipMask>, - ctx: &Context, text: &Text, id: GlyphId, ) -> Option<()> { @@ -227,10 +218,9 @@ fn render_outline_glyph( // rasterization can't be used due to very large text size or weird // scale/skewing transforms. if ppem > 100.0 || ts.kx != 0.0 || ts.ky != 0.0 || ts.sx != ts.sy { - let font = ctx.fonts.get(text.font_id); let path = { let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); - font.ttf().outline_glyph(id, &mut builder)?; + text.font.ttf().outline_glyph(id, &mut builder)?; builder.0.finish()? }; @@ -239,7 +229,7 @@ fn render_outline_glyph( // Flip vertically because font design coordinate // system is Y-up. - let scale = text.size.to_f32() / font.units_per_em() as f32; + let scale = text.size.to_f32() / text.font.units_per_em() as f32; let ts = ts.pre_scale(scale, -scale); canvas.fill_path(&path, &paint, rule, ts, mask)?; return Some(()); @@ -248,9 +238,8 @@ fn render_outline_glyph( // Rasterize the glyph with `pixglyph`. // Try to retrieve a prepared glyph or prepare it from scratch if it // doesn't exist, yet. - let glyph = pixglyph::Glyph::load(ctx.fonts.get(text.font_id).ttf(), id)?; + let glyph = pixglyph::Glyph::load(text.font.ttf(), id)?; let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem); - let cw = canvas.width() as i32; let ch = canvas.height() as i32; let mw = bitmap.width as i32; diff --git a/src/font.rs b/src/font.rs deleted file mode 100644 index 1fa86ba62..000000000 --- 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, - failed: Vec, - fonts: Vec>, - families: BTreeMap>, - buffers: HashMap, -} - -impl FontStore { - /// Create a new, empty font store. - pub fn new(loader: Arc) -> Self { - let mut fonts = vec![]; - let mut failed = vec![]; - let mut families = BTreeMap::>::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)> + '_ { - // 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 { - 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, - variant: FontVariant, - text: &str, - ) -> Option { - // 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, - variant: FontVariant, - ids: impl IntoIterator, - ) -> Option { - let infos = self.loader.fonts(); - let like = like.map(|id| &infos[id.0 as usize]); - - let mut best = None; - let mut best_key = None; - - for id in ids { - let current = &infos[id.0 as usize]; - - let key = ( - like.map(|like| { - ( - current.monospaced != like.monospaced, - like.serif.is_some() && current.serif != like.serif, - Reverse(shared_prefix_words(¤t.family, &like.family)), - current.family.len(), - ) - }), - current.variant.style.distance(variant.style), - current.variant.stretch.distance(variant.stretch), - current.variant.weight.distance(variant.weight), - ); - - if best_key.map_or(true, |b| key < b) { - best = Some(id); - best_key = Some(key); - } - } - - best - } - - /// Load the font with the given id. - /// - /// Returns `Some(id)` if the font was loaded successfully. - fn load(&mut self, id: FontId) -> Option { - 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>, -} - -impl Font { - /// Parse a font from data and collection index. - pub fn new(data: Buffer, index: u32) -> Option { - // 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) -> 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 { - 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 { - 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, - /// 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 + '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 { - // 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 { - 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); - -impl Coverage { - /// Encode a vector of codepoints. - pub fn from_vec(mut codepoints: Vec) -> 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], - ) - } -} diff --git a/src/font/book.rs b/src/font/book.rs new file mode 100644 index 000000000..8f19faf78 --- /dev/null +++ b/src/font/book.rs @@ -0,0 +1,476 @@ +use std::cmp::Reverse; +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use ttf_parser::{name_id, PlatformId, Tag}; +use unicode_segmentation::UnicodeSegmentation; + +use super::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; + +/// Metadata about a collection of fonts. +#[derive(Default)] +pub struct FontBook { + /// Maps from lowercased family names to font indices. + families: BTreeMap>, + /// Metadata about each font in the collection. + infos: Vec, +} + +impl FontBook { + /// Create a new, empty font book. + pub fn new() -> Self { + Self { families: BTreeMap::new(), infos: vec![] } + } + + /// Create a font book for a collection of fonts. + pub fn from_fonts<'a>(fonts: impl IntoIterator) -> Self { + let mut book = Self::new(); + for font in fonts { + book.push(font.info().clone()); + } + book + } + + /// Insert metadata into the font book. + pub fn push(&mut self, info: FontInfo) { + let index = self.infos.len(); + let family = info.family.to_lowercase(); + self.families.entry(family).or_default().push(index); + self.infos.push(info); + } + + /// 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)> + '_ { + // Since the keys are lowercased, we instead use the family field of the + // first face's info. + self.families.values().map(|ids| { + let family = self.infos[ids[0]].family.as_str(); + let infos = ids.iter().map(|&id| &self.infos[id]); + (family, infos) + }) + } + + /// Try to find and load a font from the given `family` that matches + /// the given `variant` as closely as possible. + /// + /// The `family` should be all lowercase. + pub fn select(&self, family: &str, variant: FontVariant) -> Option { + let ids = self.families.get(family)?; + self.find_best_variant(None, variant, ids.iter().copied()) + } + + /// 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( + &self, + like: Option<&FontInfo>, + variant: FontVariant, + text: &str, + ) -> Option { + // Find the fonts that contain the text's first char ... + let c = text.chars().next()?; + let ids = self + .infos + .iter() + .enumerate() + .filter(|(_, info)| info.coverage.contains(c as u32)) + .map(|(index, _)| index); + + // ... and find the best variant among them. + self.find_best_variant(like, variant, ids) + } + + /// 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<&FontInfo>, + variant: FontVariant, + ids: impl IntoIterator, + ) -> Option { + let mut best = None; + let mut best_key = None; + + for id in ids { + let current = &self.infos[id]; + let key = ( + like.map(|like| { + ( + current.flags.contains(FontFlags::MONOSPACE) + != like.flags.contains(FontFlags::MONOSPACE), + current.flags.contains(FontFlags::SERIF) + != like.flags.contains(FontFlags::SERIF), + Reverse(shared_prefix_words(¤t.family, &like.family)), + current.family.len(), + ) + }), + current.variant.style.distance(variant.style), + current.variant.stretch.distance(variant.stretch), + current.variant.weight.distance(variant.weight), + ); + + if best_key.map_or(true, |b| key < b) { + best = Some(id); + best_key = Some(key); + } + } + + best + } +} + +/// Properties of a single font. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct FontInfo { + /// 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, + /// Properties of the font. + pub flags: FontFlags, + /// The unicode coverage of the font. + pub coverage: Coverage, +} + +bitflags::bitflags! { + /// Bitflags describing characteristics of a font. + #[derive(Serialize, Deserialize)] + pub struct FontFlags: u32 { + /// All glyphs have the same width. + const MONOSPACE = 1 << 0; + /// Glyphs have short strokes at their stems. + const SERIF = 1 << 1; + } +} + +impl FontInfo { + /// Compute metadata for all fonts in the given data. + pub fn from_data<'a>(data: &'a [u8]) -> impl Iterator + '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(&ttf) + }) + } + + /// Compute metadata for a single ttf-parser face. + pub fn from_ttf(ttf: &ttf_parser::Face) -> Option { + // 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, name_id::FAMILY)?; + if family.starts_with("Noto") { + family = find_name(ttf, name_id::FULL_NAME)?; + } + typographic_family(&family).to_string() + }; + + 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, + }; + + 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)); + } + } + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); + + // Determine whether this is a serif or sans-serif font. + if let Some(panose) = ttf + .table_data(Tag::from_bytes(b"OS/2")) + .and_then(|os2| os2.get(32 .. 45)) + { + if matches!(panose, [2, 2 ..= 10, ..]) { + flags.insert(FontFlags::SERIF); + } + } + + Some(FontInfo { + family, + variant, + flags, + coverage: Coverage::from_vec(codepoints), + }) + } +} + +/// Try to find and decode the name with the given id. +pub(super) fn find_name(ttf: &ttf_parser::Face, name_id: u16) -> Option { + 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 typographic_family(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] +} + +/// 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 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); + +impl Coverage { + /// Encode a vector of codepoints. + pub fn from_vec(mut codepoints: Vec) -> 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_trim_styles() { + assert_eq!(typographic_family("Atma Light"), "Atma"); + assert_eq!(typographic_family("eras bold"), "eras"); + assert_eq!(typographic_family("footlight mt light"), "footlight mt"); + assert_eq!(typographic_family("times new roman"), "times new roman"); + assert_eq!( + typographic_family("noto sans mono cond sembd"), + "noto sans mono" + ); + assert_eq!(typographic_family("noto serif SEMCOND sembd"), "noto serif"); + assert_eq!(typographic_family("crimson text"), "crimson text"); + assert_eq!(typographic_family("footlight light"), "footlight"); + assert_eq!(typographic_family("Noto Sans"), "Noto Sans"); + assert_eq!(typographic_family("Noto Sans Light"), "Noto Sans"); + assert_eq!( + typographic_family("Noto Sans Semicondensed Heavy"), + "Noto Sans" + ); + assert_eq!(typographic_family("Familx"), "Familx"); + assert_eq!(typographic_family("Font Ultra"), "Font Ultra"); + assert_eq!(typographic_family("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/font/mod.rs b/src/font/mod.rs new file mode 100644 index 000000000..917fea620 --- /dev/null +++ b/src/font/mod.rs @@ -0,0 +1,256 @@ +//! Font handling. + +mod book; +mod variant; + +pub use book::*; +pub use variant::*; + +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use once_cell::sync::OnceCell; +use rex::font::MathHeader; +use ttf_parser::{GlyphId, Tag}; + +use crate::geom::Em; +use crate::loading::Buffer; + +/// An OpenType font. +#[derive(Clone)] +pub struct Font(Arc); + +/// The internal representation of a font. +struct Repr { + /// 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 buffer. + index: u32, + /// Metadata about the font. + info: FontInfo, + /// The font's metrics. + metrics: FontMetrics, + /// The underlying ttf-parser/rustybuzz face. + ttf: rustybuzz::Face<'static>, + /// The parsed ReX math header. + math: OnceCell>, +} + +impl Font { + /// Parse a font from data and collection index. + pub fn new(data: Buffer, index: u32) -> Option { + // 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); + let info = FontInfo::from_ttf(&ttf)?; + + Some(Self(Arc::new(Repr { + data, + index, + info, + ttf, + metrics, + math: OnceCell::new(), + }))) + } + + /// The underlying buffer. + pub fn data(&self) -> &Buffer { + &self.0.data + } + + /// The font's index in the buffer. + pub fn index(&self) -> u32 { + self.0.index + } + + /// The font's metadata. + pub fn info(&self) -> &FontInfo { + &self.0.info + } + + /// The font's metrics. + pub fn metrics(&self) -> &FontMetrics { + &self.0.metrics + } + + /// The number of font units per one em. + pub fn units_per_em(&self) -> f64 { + self.0.metrics.units_per_em + } + + /// Convert from font units to an em length. + pub fn to_em(&self, units: impl Into) -> 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 { + self.0 + .ttf + .glyph_hor_advance(GlyphId(glyph)) + .map(|units| self.to_em(units)) + } + + /// Lookup a name by id. + pub fn find_name(&self, id: u16) -> Option { + find_name(&self.0.ttf, id) + } + + /// 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.0.ttf + } + + /// Access the math header, if any. + pub fn math(&self) -> Option<&MathHeader> { + self.0 + .math + .get_or_init(|| { + let data = self.ttf().table_data(Tag::from_bytes(b"MATH"))?; + MathHeader::parse(data).ok() + }) + .as_ref() + } +} + +impl Hash for Font { + fn hash(&self, state: &mut H) { + self.0.data.hash(state); + } +} + +impl Debug for Font { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Font({})", self.info().family) + } +} + +impl Eq for Font {} + +impl PartialEq for Font { + fn eq(&self, other: &Self) -> bool { + self.0.data.eq(&other.0.data) + } +} + +/// Metrics of 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, +} diff --git a/src/font/variant.rs b/src/font/variant.rs new file mode 100644 index 000000000..9e16afc88 --- /dev/null +++ b/src/font/variant.rs @@ -0,0 +1,226 @@ +use std::fmt::{self, Debug, Formatter}; + +use serde::{Deserialize, Serialize}; + +/// 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()) + } +} + +#[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%") + } +} diff --git a/src/frame.rs b/src/frame.rs index 7a5fb9e4c..287bbf908 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -5,7 +5,7 @@ use std::num::NonZeroUsize; use std::sync::Arc; use crate::eval::{Dict, Value}; -use crate::font::FontId; +use crate::font::Font; use crate::geom::{ Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform, }; @@ -353,7 +353,7 @@ impl Debug for Group { #[derive(Clone, Eq, PartialEq)] pub struct Text { /// The font the glyphs are contained in. - pub font_id: FontId, + pub font: Font, /// The font size. pub size: Length, /// Glyph color. diff --git a/src/lib.rs b/src/lib.rs index 9da37df79..c26ef8092 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,6 @@ use std::sync::Arc; use crate::diag::TypResult; use crate::eval::Scope; -use crate::font::FontStore; use crate::frame::Frame; use crate::loading::Loader; use crate::model::StyleMap; @@ -77,8 +76,6 @@ pub struct Context { pub loader: Arc, /// Stores loaded source files. pub sources: SourceStore, - /// Stores parsed fonts. - pub fonts: FontStore, /// The context's configuration. config: Config, } @@ -89,7 +86,6 @@ impl Context { Self { loader: Arc::clone(&loader), sources: SourceStore::new(Arc::clone(&loader)), - fonts: FontStore::new(Arc::clone(&loader)), config, } } diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs index 784afa01f..f8cdc4cde 100644 --- a/src/library/graphics/image.rs +++ b/src/library/graphics/image.rs @@ -22,7 +22,7 @@ impl ImageNode { let image = vm .ctx .loader - .load(&full) + .file(&full) .and_then(|buffer| Image::new(buffer, ext)) .map_err(|err| failed_to_load("image", &full, err)) .at(span)?; diff --git a/src/library/math/rex.rs b/src/library/math/rex.rs index 165642d31..0116b4b26 100644 --- a/src/library/math/rex.rs +++ b/src/library/math/rex.rs @@ -4,7 +4,7 @@ use rex::layout::{LayoutSettings, Style}; use rex::parser::color::RGBA; use rex::render::{Backend, Cursor, Renderer}; -use crate::font::FontId; +use crate::font::Font; use crate::library::prelude::*; use crate::library::text::{variant, FontFamily, Lang, TextNode}; @@ -28,14 +28,15 @@ impl Layout for RexNode { ) -> TypResult> { // Load the font. let span = self.tex.span; - let font_id = ctx - .fonts + let font = ctx + .loader + .book() .select(self.family.as_str(), variant(styles)) + .and_then(|id| ctx.loader.font(id).ok()) .ok_or("failed to find math font") .at(span)?; // Prepare the font context. - let font = ctx.fonts.get(font_id); let ctx = font .math() .map(|math| FontContext::new(font.ttf(), math)) @@ -76,7 +77,7 @@ impl Layout for RexNode { frame }, baseline: top, - font_id, + font: font.clone(), fill: styles.get(TextNode::FILL), lang: styles.get(TextNode::LANG), colors: vec![], @@ -93,7 +94,7 @@ impl Layout for RexNode { struct FrameBackend { frame: Frame, baseline: Length, - font_id: FontId, + font: Font, fill: Paint, lang: Lang, colors: Vec, @@ -119,7 +120,7 @@ impl Backend for FrameBackend { self.frame.push( self.transform(pos), Element::Text(Text { - font_id: self.font_id, + font: self.font.clone(), size: Length::pt(scale), fill: self.fill(), lang: self.lang, diff --git a/src/library/prelude.rs b/src/library/prelude.rs index f55447c38..8bd3b8159 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -17,6 +17,7 @@ pub use crate::eval::{ }; pub use crate::frame::*; pub use crate::geom::*; +pub use crate::loading::Loader; pub use crate::model::{ Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Selector, Show, ShowNode, StyleChain, StyleMap, StyleVec, diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index 6d8b28543..8295f1a2e 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -2,7 +2,6 @@ use kurbo::{BezPath, Line, ParamCurve}; use ttf_parser::{GlyphId, OutlineBuilder}; use super::TextNode; -use crate::font::FontStore; use crate::library::prelude::*; /// Typeset underline, stricken-through or overlined text. @@ -88,14 +87,12 @@ pub const OVERLINE: DecoLine = 2; pub fn decorate( frame: &mut Frame, deco: &Decoration, - fonts: &FontStore, text: &Text, shift: Length, pos: Point, width: Length, ) { - let font = fonts.get(text.font_id); - let font_metrics = font.metrics(); + let font_metrics = text.font.metrics(); let metrics = match deco.line { STRIKETHROUGH => font_metrics.strikethrough, OVERLINE => font_metrics.overline, @@ -143,7 +140,7 @@ pub fn decorate( let mut builder = BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); - let bbox = font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); let path = builder.finish(); x += glyph.x_advance.at(text.size); @@ -151,8 +148,8 @@ pub fn decorate( // Only do the costly segments intersection test if the line // intersects the bounding box. if bbox.map_or(false, |bbox| { - let y_min = -font.to_em(bbox.y_max).at(text.size); - let y_max = -font.to_em(bbox.y_min).at(text.size); + let y_min = -text.font.to_em(bbox.y_max).at(text.size); + let y_max = -text.font.to_em(bbox.y_min).at(text.size); offset >= y_min && offset <= y_max }) { diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 9c4f33f11..357801861 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -24,9 +24,7 @@ use std::borrow::Cow; use ttf_parser::Tag; -use crate::font::{ - Font, FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric, -}; +use crate::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::library::prelude::*; use crate::util::EcoString; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 168aca262..8309bcc8b 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -5,7 +5,6 @@ use unicode_script::{Script, UnicodeScript}; use xi_unicode::LineBreakIterator; use super::{shape, Lang, Quoter, Quotes, RepeatNode, ShapedText, TextNode}; -use crate::font::FontStore; use crate::library::layout::Spacing; use crate::library::prelude::*; use crate::util::EcoString; @@ -78,7 +77,7 @@ impl Layout for ParNode { let p = prepare(ctx, self, &text, segments, regions, styles)?; // Break the paragraph into lines. - let lines = linebreak(&p, &mut ctx.fonts, regions.first.x); + let lines = linebreak(&p, ctx.loader.as_ref(), regions.first.x); // Stack the lines into one frame per region. stack(&p, ctx, &lines, regions) @@ -518,7 +517,13 @@ fn prepare<'a>( let end = cursor + segment.len(); match segment { Segment::Text(_) => { - shape_range(&mut items, &mut ctx.fonts, &bidi, cursor .. end, styles); + shape_range( + &mut items, + ctx.loader.as_ref(), + &bidi, + cursor .. end, + styles, + ); } Segment::Spacing(spacing) => match spacing { Spacing::Relative(v) => { @@ -562,14 +567,14 @@ fn prepare<'a>( /// items for them. fn shape_range<'a>( items: &mut Vec>, - fonts: &mut FontStore, + loader: &dyn Loader, bidi: &BidiInfo<'a>, range: Range, styles: StyleChain<'a>, ) { let mut process = |text, level: Level| { let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = shape(fonts, text, styles, dir); + let shaped = shape(loader, text, styles, dir); items.push(Item::Text(shaped)); }; @@ -628,12 +633,12 @@ fn shared_get<'a, K: Key<'a>>( /// Find suitable linebreaks. fn linebreak<'a>( p: &'a Preparation<'a>, - fonts: &mut FontStore, + loader: &dyn Loader, width: Length, ) -> Vec> { match p.styles.get(ParNode::LINEBREAKS) { - Linebreaks::Simple => linebreak_simple(p, fonts, width), - Linebreaks::Optimized => linebreak_optimized(p, fonts, width), + Linebreaks::Simple => linebreak_simple(p, loader, width), + Linebreaks::Optimized => linebreak_optimized(p, loader, width), } } @@ -642,7 +647,7 @@ fn linebreak<'a>( /// very unbalanced line, but is fast and simple. fn linebreak_simple<'a>( p: &'a Preparation<'a>, - fonts: &mut FontStore, + loader: &dyn Loader, width: Length, ) -> Vec> { let mut lines = vec![]; @@ -651,7 +656,7 @@ fn linebreak_simple<'a>( for (end, mandatory, hyphen) in breakpoints(p) { // Compute the line and its size. - let mut attempt = line(p, fonts, start .. end, mandatory, hyphen); + let mut attempt = line(p, loader, start .. end, mandatory, hyphen); // If the line doesn't fit anymore, we push the last fitting attempt // into the stack and rebuild the line from the attempt's end. The @@ -660,7 +665,7 @@ fn linebreak_simple<'a>( if let Some((last_attempt, last_end)) = last.take() { lines.push(last_attempt); start = last_end; - attempt = line(p, fonts, start .. end, mandatory, hyphen); + attempt = line(p, loader, start .. end, mandatory, hyphen); } } @@ -702,7 +707,7 @@ fn linebreak_simple<'a>( /// text. fn linebreak_optimized<'a>( p: &'a Preparation<'a>, - fonts: &mut FontStore, + loader: &dyn Loader, width: Length, ) -> Vec> { /// The cost of a line or paragraph layout. @@ -727,7 +732,7 @@ fn linebreak_optimized<'a>( let mut table = vec![Entry { pred: 0, total: 0.0, - line: line(p, fonts, 0 .. 0, false, false), + line: line(p, loader, 0 .. 0, false, false), }]; let em = p.styles.get(TextNode::SIZE); @@ -741,7 +746,7 @@ fn linebreak_optimized<'a>( for (i, pred) in table.iter_mut().enumerate().skip(active) { // Layout the line. let start = pred.line.end; - let attempt = line(p, fonts, start .. end, mandatory, hyphen); + let attempt = line(p, loader, start .. end, mandatory, hyphen); // Determine how much the line's spaces would need to be stretched // to make it the desired width. @@ -915,7 +920,7 @@ impl Breakpoints<'_> { /// Create a line which spans the given range. fn line<'a>( p: &'a Preparation, - fonts: &mut FontStore, + loader: &dyn Loader, mut range: Range, mandatory: bool, hyphen: bool, @@ -970,9 +975,9 @@ fn line<'a>( if hyphen || start + shaped.text.len() > range.end { if hyphen || start < range.end || before.is_empty() { let shifted = start - base .. range.end - base; - let mut reshaped = shaped.reshape(fonts, shifted); + let mut reshaped = shaped.reshape(loader, shifted); if hyphen || shy { - reshaped.push_hyphen(fonts); + reshaped.push_hyphen(loader); } width += reshaped.width; last = Some(Item::Text(reshaped)); @@ -993,7 +998,7 @@ fn line<'a>( if range.start + shaped.text.len() > end { if range.start < end { let shifted = range.start - base .. end - base; - let reshaped = shaped.reshape(fonts, shifted); + let reshaped = shaped.reshape(loader, shifted); width += reshaped.width; first = Some(Item::Text(reshaped)); } @@ -1144,7 +1149,7 @@ fn commit( offset += v.share(fr, remaining); } Item::Text(shaped) => { - let frame = shaped.build(&mut ctx.fonts, justification); + let frame = shaped.build(ctx.loader.as_ref(), justification); push(&mut offset, frame); } Item::Frame(frame) => { diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index 4f7647d42..3d905c997 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use rustybuzz::{Feature, UnicodeBuffer}; use super::*; -use crate::font::{FontId, FontStore, FontVariant}; +use crate::font::{Font, FontVariant}; use crate::library::prelude::*; use crate::util::SliceExt; @@ -31,10 +31,10 @@ pub struct ShapedText<'a> { } /// A single glyph resulting from shaping. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct ShapedGlyph { /// The font the glyph is contained in. - pub font_id: FontId, + pub font: Font, /// The glyph's index in the font. pub glyph_id: u16, /// The advance width of the glyph. @@ -80,8 +80,8 @@ impl<'a> ShapedText<'a> { /// /// The `justification` defines how much extra advance width each /// [justifiable glyph](ShapedGlyph::is_justifiable) will get. - pub fn build(&self, fonts: &mut FontStore, justification: Length) -> Frame { - let (top, bottom) = self.measure(fonts); + pub fn build(&self, loader: &dyn Loader, justification: Length) -> Frame { + let (top, bottom) = self.measure(loader); let size = Size::new(self.width, top + bottom); let mut offset = Length::zero(); @@ -94,8 +94,8 @@ impl<'a> ShapedText<'a> { let fill = self.styles.get(TextNode::FILL); let link = self.styles.get(TextNode::LINK); - for ((font_id, y_offset), group) in - self.glyphs.as_ref().group_by_key(|g| (g.font_id, g.y_offset)) + for ((font, y_offset), group) in + self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) { let pos = Point::new(offset, top + shift + y_offset.at(self.size)); @@ -116,7 +116,7 @@ impl<'a> ShapedText<'a> { .collect(); let text = Text { - font_id, + font, size: self.size, lang, fill, @@ -128,7 +128,7 @@ impl<'a> ShapedText<'a> { // Apply line decorations. for deco in &decos { - decorate(&mut frame, &deco, fonts, &text, shift, pos, width); + decorate(&mut frame, &deco, &text, shift, pos, width); } frame.insert(text_layer, pos, Element::Text(text)); @@ -144,7 +144,7 @@ impl<'a> ShapedText<'a> { } /// Measure the top and bottom extent of this text. - fn measure(&self, fonts: &mut FontStore) -> (Length, Length) { + fn measure(&self, loader: &dyn Loader) -> (Length, Length) { let mut top = Length::zero(); let mut bottom = Length::zero(); @@ -162,14 +162,18 @@ impl<'a> ShapedText<'a> { // When there are no glyphs, we just use the vertical metrics of the // first available font. for family in families(self.styles) { - if let Some(font_id) = fonts.select(family, self.variant) { - expand(fonts.get(font_id)); + if let Some(font) = loader + .book() + .select(family, self.variant) + .and_then(|id| loader.font(id).ok()) + { + expand(&font); break; } } } else { - for (font_id, _) in self.glyphs.group_by_key(|g| g.font_id) { - expand(fonts.get(font_id)); + for g in self.glyphs.iter() { + expand(&g.font); } } @@ -195,7 +199,7 @@ impl<'a> ShapedText<'a> { /// shaping process if possible. pub fn reshape( &'a self, - fonts: &mut FontStore, + loader: &dyn Loader, text_range: Range, ) -> ShapedText<'a> { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { @@ -209,22 +213,24 @@ impl<'a> ShapedText<'a> { glyphs: Cow::Borrowed(glyphs), } } else { - shape(fonts, &self.text[text_range], self.styles, self.dir) + shape(loader, &self.text[text_range], self.styles, self.dir) } } /// Push a hyphen to end of the text. - pub fn push_hyphen(&mut self, fonts: &mut FontStore) { + pub fn push_hyphen(&mut self, loader: &dyn Loader) { families(self.styles).find_map(|family| { - let font_id = fonts.select(family, self.variant)?; - let font = fonts.get(font_id); + let font = loader + .book() + .select(family, self.variant) + .and_then(|id| loader.font(id).ok())?; let ttf = font.ttf(); let glyph_id = ttf.glyph_index('-')?; let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?); let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default(); self.width += x_advance.at(self.size); self.glyphs.to_mut().push(ShapedGlyph { - font_id, + font, glyph_id: glyph_id.0, x_advance, x_offset: Em::zero(), @@ -300,9 +306,9 @@ impl Debug for ShapedText<'_> { /// Holds shaping results and metadata common to all shaped segments. struct ShapingContext<'a> { - fonts: &'a mut FontStore, + loader: &'a dyn Loader, glyphs: Vec, - used: Vec, + used: Vec, styles: StyleChain<'a>, size: Length, variant: FontVariant, @@ -313,7 +319,7 @@ struct ShapingContext<'a> { /// Shape text into [`ShapedText`]. pub fn shape<'a>( - fonts: &mut FontStore, + loader: &dyn Loader, text: &'a str, styles: StyleChain<'a>, dir: Dir, @@ -321,7 +327,7 @@ pub fn shape<'a>( let size = styles.get(TextNode::SIZE); let mut ctx = ShapingContext { - fonts, + loader, size, glyphs: vec![], used: vec![], @@ -362,32 +368,33 @@ fn shape_segment<'a>( } // Find the next available family. + let book = ctx.loader.book(); let mut selection = families.find_map(|family| { - ctx.fonts - .select(family, ctx.variant) - .filter(|id| !ctx.used.contains(id)) + book.select(family, ctx.variant) + .and_then(|id| ctx.loader.font(id).ok()) + .filter(|font| !ctx.used.contains(font)) }); // 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 + let first = ctx.used.first().map(Font::info); + selection = book .select_fallback(first, ctx.variant, text) - .filter(|id| !ctx.used.contains(id)); + .and_then(|id| ctx.loader.font(id).ok()) + .filter(|font| !ctx.used.contains(font)); } // Extract the font id or shape notdef glyphs if we couldn't find any font. - let font_id = if let Some(id) = selection { - id + let font = if let Some(font) = selection { + font } else { - if let Some(&font_id) = ctx.used.first() { - shape_tofus(ctx, base, text, font_id); + if let Some(font) = ctx.used.first().cloned() { + shape_tofus(ctx, base, text, font); } return; }; - ctx.used.push(font_id); + ctx.used.push(font.clone()); // Fill the buffer with our text. let mut buffer = UnicodeBuffer::new(); @@ -400,7 +407,6 @@ fn shape_segment<'a>( }); // Shape! - let mut font = ctx.fonts.get(font_id); let buffer = rustybuzz::shape(font.ttf(), &ctx.tags, buffer); let infos = buffer.glyph_infos(); let pos = buffer.glyph_positions(); @@ -416,7 +422,7 @@ fn shape_segment<'a>( // Add the glyph to the shaped output. // TODO: Don't ignore y_advance. ctx.glyphs.push(ShapedGlyph { - font_id, + font: font.clone(), glyph_id: info.glyph_id as u16, x_advance: font.to_em(pos[i].x_advance), x_offset: font.to_em(pos[i].x_offset), @@ -471,8 +477,6 @@ fn shape_segment<'a>( // Recursively shape the tofu sequence with the next family. shape_segment(ctx, base + range.start, &text[range], families.clone()); - - font = ctx.fonts.get(font_id); } i += 1; @@ -482,12 +486,11 @@ fn shape_segment<'a>( } /// Shape the text with tofus from the given font. -fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font_id: FontId) { - let font = ctx.fonts.get(font_id); +fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { let x_advance = font.advance(0).unwrap_or_default(); for (cluster, c) in text.char_indices() { ctx.glyphs.push(ShapedGlyph { - font_id, + font: font.clone(), glyph_id: 0, x_advance, x_offset: Em::zero(), @@ -511,8 +514,7 @@ fn track_and_space(ctx: &mut ShapingContext) { while let Some(glyph) = glyphs.next() { // Make non-breaking space same width as normal space. if glyph.c == '\u{00A0}' { - let font = ctx.fonts.get(glyph.font_id); - glyph.x_advance -= nbsp_delta(font).unwrap_or_default(); + glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default(); } if glyph.is_space() { diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs index fde969d3f..75b2a5790 100644 --- a/src/library/text/shift.rs +++ b/src/library/text/shift.rs @@ -1,5 +1,4 @@ use super::{variant, TextNode, TextSize}; -use crate::font::FontStore; use crate::library::prelude::*; use crate::util::EcoString; @@ -47,7 +46,7 @@ impl Show for ShiftNode { let mut transformed = None; if styles.get(Self::TYPOGRAPHIC) { if let Some(text) = search_text(&self.0, S) { - if is_shapable(&mut ctx.fonts, &text, styles) { + if is_shapable(ctx.loader.as_ref(), &text, styles) { transformed = Some(Content::Text(text)); } } @@ -92,11 +91,14 @@ fn search_text(content: &Content, mode: ScriptKind) -> Option { /// Checks whether the first retrievable family contains all code points of the /// given string. -fn is_shapable(fonts: &mut FontStore, text: &str, styles: StyleChain) -> bool { +fn is_shapable(loader: &dyn Loader, text: &str, styles: StyleChain) -> bool { + let book = loader.book(); for family in styles.get(TextNode::FAMILY).iter() { - if let Some(font_id) = fonts.select(family.as_str(), variant(styles)) { - let ttf = fonts.get(font_id).ttf(); - return text.chars().all(|c| ttf.glyph_index(c).is_some()); + if let Some(font) = book + .select(family.as_str(), variant(styles)) + .and_then(|id| loader.font(id).ok()) + { + return text.chars().all(|c| font.ttf().glyph_index(c).is_some()); } } diff --git a/src/library/utility/data.rs b/src/library/utility/data.rs index 0f9e6bf09..f9e970dc8 100644 --- a/src/library/utility/data.rs +++ b/src/library/utility/data.rs @@ -7,7 +7,7 @@ pub fn csv(vm: &mut Machine, args: &mut Args) -> TypResult { let path = vm.locate(&path).at(span)?; let try_load = || -> io::Result { - let data = vm.ctx.loader.load(&path)?; + let data = vm.ctx.loader.file(&path)?; let mut builder = csv::ReaderBuilder::new(); builder.has_headers(false); diff --git a/src/loading.rs b/src/loading.rs new file mode 100644 index 000000000..9a32d0257 --- /dev/null +++ b/src/loading.rs @@ -0,0 +1,239 @@ +//! Resource loading. + +use std::fmt::{self, Debug, Formatter}; +use std::io; +use std::ops::Deref; +use std::path::Path; +use std::sync::Arc; + +use crate::font::{Font, FontBook}; +use crate::util::Prehashed; + +/// A hash that identifies a file. +/// +/// Such a hash can be [resolved](Loader::resolve) from a path. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct FileHash(pub u64); + +/// Loads resources from a local or remote source. +pub trait Loader { + /// Metadata about all known fonts. + fn book(&self) -> &FontBook; + + /// Access the font with the given id. + fn font(&self, id: usize) -> io::Result; + + /// Resolve a hash that is the same for this and all other paths pointing to + /// the same file. + fn resolve(&self, path: &Path) -> io::Result; + + /// Load a file from a path. + fn file(&self, path: &Path) -> io::Result; +} + +/// A shared buffer that is cheap to clone. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Buffer(Prehashed>>); + +impl Buffer { + /// Return a view into the buffer. + pub fn as_slice(&self) -> &[u8] { + self + } + + /// Return a copy of the buffer as a vector. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} + +impl From<&[u8]> for Buffer { + fn from(slice: &[u8]) -> Self { + Self(Prehashed::new(Arc::new(slice.to_vec()))) + } +} + +impl From> for Buffer { + fn from(vec: Vec) -> Self { + Self(Prehashed::new(Arc::new(vec))) + } +} + +impl From>> for Buffer { + fn from(arc: Arc>) -> Self { + Self(Prehashed::new(arc)) + } +} + +impl Deref for Buffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Buffer { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl Debug for Buffer { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Buffer(..)") + } +} + +#[cfg(feature = "fs")] +pub use fs::*; + +#[cfg(feature = "fs")] +mod fs { + use std::fs::{self, File}; + use std::io; + use std::path::{Path, PathBuf}; + + use memmap2::Mmap; + use same_file::Handle; + use walkdir::WalkDir; + + use super::{Buffer, FileHash, Loader}; + use crate::font::{Font, FontBook, FontInfo}; + + /// Loads fonts and files from the local file system. + /// + /// _This is only available when the `system` feature is enabled._ + pub struct FsLoader { + book: FontBook, + paths: Vec<(PathBuf, u32)>, + } + + impl FsLoader { + /// Create a new system loader. + pub fn new() -> Self { + Self { book: FontBook::new(), paths: vec![] } + } + + /// Builder-style variant of [`search_path`](Self::search_path). + pub fn with_path(mut self, dir: impl AsRef) -> Self { + self.search_path(dir); + self + } + + /// Search for all fonts at a path. + /// + /// If the path is a directory, all contained fonts will be searched for + /// recursively. + pub fn search_path(&mut self, path: impl AsRef) { + let walk = WalkDir::new(path) + .follow_links(true) + .sort_by(|a, b| a.file_name().cmp(b.file_name())) + .into_iter() + .filter_map(|e| e.ok()); + + for entry in walk { + let path = entry.path(); + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + if matches!( + ext, + "ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC", + ) { + self.search_file(path); + } + } + } + } + + /// Index the fonts in the file at the given path. + /// + /// The file may form a font collection and contain multiple fonts, + /// which will then all be indexed. + fn search_file(&mut self, path: impl AsRef) { + let path = path.as_ref(); + let path = path.strip_prefix(".").unwrap_or(path); + if let Ok(file) = File::open(path) { + if let Ok(mmap) = unsafe { Mmap::map(&file) } { + for (i, info) in FontInfo::from_data(&mmap).enumerate() { + self.book.push(info); + self.paths.push((path.into(), i as u32)); + } + } + } + } + + /// Builder-style variant of [`search_system`](Self::search_system). + pub fn with_system(mut self) -> Self { + self.search_system(); + self + } + + /// Search for fonts in the operating system's font directories. + pub fn search_system(&mut self) { + self.search_system_impl(); + } + + #[cfg(all(unix, not(target_os = "macos")))] + fn search_system_impl(&mut self) { + self.search_path("/usr/share/fonts"); + self.search_path("/usr/local/share/fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_path(dir); + } + } + + #[cfg(target_os = "macos")] + fn search_system_impl(&mut self) { + self.search_path("/Library/Fonts"); + self.search_path("/Network/Library/Fonts"); + self.search_path("/System/Library/Fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_path(dir); + } + } + + #[cfg(windows)] + fn search_system_impl(&mut self) { + let windir = + std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); + + self.search_path(Path::new(&windir).join("Fonts")); + + if let Some(roaming) = dirs::config_dir() { + self.search_path(roaming.join("Microsoft\\Windows\\Fonts")); + } + + if let Some(local) = dirs::cache_dir() { + self.search_path(local.join("Microsoft\\Windows\\Fonts")); + } + } + } + + impl Loader for FsLoader { + fn book(&self) -> &FontBook { + &self.book + } + + fn font(&self, id: usize) -> io::Result { + let (path, index) = &self.paths[id]; + let data = self.file(path)?; + Font::new(data, *index).ok_or_else(|| io::ErrorKind::InvalidData.into()) + } + + fn resolve(&self, path: &Path) -> io::Result { + let meta = fs::metadata(path)?; + if meta.is_file() { + let handle = Handle::from_path(path)?; + Ok(FileHash(fxhash::hash64(&handle))) + } else { + Err(io::ErrorKind::NotFound.into()) + } + } + + fn file(&self, path: &Path) -> io::Result { + Ok(fs::read(path)?.into()) + } + } +} diff --git a/src/loading/fs.rs b/src/loading/fs.rs deleted file mode 100644 index 55aa967b5..000000000 --- a/src/loading/fs.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::fs::{self, File}; -use std::io; -use std::path::Path; - -use memmap2::Mmap; -use same_file::Handle; -use walkdir::WalkDir; - -use super::{Buffer, FileHash, Loader}; -use crate::font::FontInfo; - -/// Loads fonts and files from the local file system. -/// -/// _This is only available when the `fs` feature is enabled._ -pub struct FsLoader { - fonts: Vec, -} - -impl FsLoader { - /// Create a new loader without any fonts. - pub fn new() -> Self { - Self { fonts: vec![] } - } - - /// Builder-style variant of [`search_system`](Self::search_system). - pub fn with_system(mut self) -> Self { - self.search_system(); - self - } - - /// Builder-style variant of [`search_path`](Self::search_path). - pub fn with_path(mut self, dir: impl AsRef) -> Self { - self.search_path(dir); - self - } - - /// Search for fonts in the operating system's font directories. - pub fn search_system(&mut self) { - self.search_system_impl(); - } - - #[cfg(all(unix, not(target_os = "macos")))] - fn search_system_impl(&mut self) { - self.search_path("/usr/share/fonts"); - self.search_path("/usr/local/share/fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_path(dir); - } - } - - #[cfg(target_os = "macos")] - fn search_system_impl(&mut self) { - self.search_path("/Library/Fonts"); - self.search_path("/Network/Library/Fonts"); - self.search_path("/System/Library/Fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_path(dir); - } - } - - #[cfg(windows)] - fn search_system_impl(&mut self) { - let windir = - std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); - - self.search_path(Path::new(&windir).join("Fonts")); - - if let Some(roaming) = dirs::config_dir() { - self.search_path(roaming.join("Microsoft\\Windows\\Fonts")); - } - - if let Some(local) = dirs::cache_dir() { - self.search_path(local.join("Microsoft\\Windows\\Fonts")); - } - } - - /// Search for all fonts at a path. - /// - /// If the path is a directory, all contained fonts will be searched for - /// recursively. - pub fn search_path(&mut self, path: impl AsRef) { - let walk = WalkDir::new(path) - .follow_links(true) - .sort_by(|a, b| a.file_name().cmp(b.file_name())) - .into_iter() - .filter_map(|e| e.ok()); - - for entry in walk { - let path = entry.path(); - if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - if matches!( - ext, - "ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC", - ) { - self.search_file(path); - } - } - } - } - - /// Index the fonts in the file at the given path. - /// - /// The file may form a font collection and contain multiple fonts, - /// which will then all be indexed. - fn search_file(&mut self, path: impl AsRef) { - let path = path.as_ref(); - let path = path.strip_prefix(".").unwrap_or(path); - if let Ok(file) = File::open(path) { - if let Ok(mmap) = unsafe { Mmap::map(&file) } { - self.fonts.extend(FontInfo::from_data(path, &mmap)); - } - } - } -} - -impl Loader for FsLoader { - fn fonts(&self) -> &[FontInfo] { - &self.fonts - } - - fn resolve(&self, path: &Path) -> io::Result { - let meta = fs::metadata(path)?; - if meta.is_file() { - let handle = Handle::from_path(path)?; - Ok(FileHash(fxhash::hash64(&handle))) - } else { - Err(io::ErrorKind::NotFound.into()) - } - } - - fn load(&self, path: &Path) -> io::Result { - Ok(fs::read(path)?.into()) - } -} diff --git a/src/loading/mem.rs b/src/loading/mem.rs deleted file mode 100644 index 36e920d9b..000000000 --- a/src/loading/mem.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::borrow::Cow; -use std::collections::HashMap; -use std::io; -use std::path::{Path, PathBuf}; - -use super::{Buffer, FileHash, Loader}; -use crate::font::FontInfo; -use crate::util::PathExt; - -/// Loads fonts and files from an in-memory storage. -#[derive(Default)] -pub struct MemLoader { - fonts: Vec, - files: HashMap>, -} - -impl MemLoader { - /// Create a new from-memory loader. - pub fn new() -> Self { - Self { fonts: vec![], files: HashMap::new() } - } - - /// Builder-style variant of [`insert`](Self::insert). - pub fn with(mut self, path: P, data: D) -> Self - where - P: AsRef, - D: Into>, - { - self.insert(path, data); - self - } - - /// Insert a path-file mapping. If the data forms a font, then that font - /// will be available for layouting. - /// - /// The data can either be owned or referenced, but the latter only if its - /// lifetime is `'static`. - pub fn insert(&mut self, path: P, data: D) - where - P: AsRef, - D: Into>, - { - let path = path.as_ref().normalize(); - let data = data.into(); - self.fonts.extend(FontInfo::from_data(&path, &data)); - self.files.insert(path, data); - } -} - -impl Loader for MemLoader { - fn fonts(&self) -> &[FontInfo] { - &self.fonts - } - - fn resolve(&self, path: &Path) -> io::Result { - let norm = path.normalize(); - if self.files.contains_key(&norm) { - Ok(FileHash(fxhash::hash64(&norm))) - } else { - Err(io::ErrorKind::NotFound.into()) - } - } - - fn load(&self, path: &Path) -> io::Result { - self.files - .get(&path.normalize()) - .map(|cow| cow.clone().into_owned().into()) - .ok_or_else(|| io::ErrorKind::NotFound.into()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::font::FontVariant; - - #[test] - fn test_recognize_and_load_font() { - let data = include_bytes!("../../fonts/PTSans-Regular.ttf"); - let path = Path::new("PTSans.ttf"); - let loader = MemLoader::new().with(path, &data[..]); - - // Test that the font was found. - let info = &loader.fonts[0]; - assert_eq!(info.path, path); - assert_eq!(info.index, 0); - assert_eq!(info.family, "PT Sans"); - assert_eq!(info.variant, FontVariant::default()); - assert_eq!(loader.fonts.len(), 1); - - // Test that the file can be loaded. - assert_eq!( - loader.load(Path::new("directory/../PTSans.ttf")).unwrap().as_slice(), - data - ); - } -} diff --git a/src/loading/mod.rs b/src/loading/mod.rs deleted file mode 100644 index ecc1e8d5e..000000000 --- a/src/loading/mod.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Resource loading. - -#[cfg(feature = "fs")] -mod fs; -mod mem; - -#[cfg(feature = "fs")] -pub use fs::*; -pub use mem::*; - -use std::fmt::{self, Debug, Formatter}; -use std::io; -use std::ops::Deref; -use std::path::Path; -use std::sync::Arc; - -use crate::font::FontInfo; -use crate::util::Prehashed; - -/// A hash that identifies a file. -/// -/// Such a hash can be [resolved](Loader::resolve) from a path. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct FileHash(pub u64); - -/// Loads resources from a local or remote source. -pub trait Loader { - /// Descriptions of all fonts this loader serves. - fn fonts(&self) -> &[FontInfo]; - - /// Resolve a hash that is the same for this and all other paths pointing to - /// the same file. - fn resolve(&self, path: &Path) -> io::Result; - - /// Load a file from a path. - fn load(&self, path: &Path) -> io::Result; -} - -/// A loader which serves nothing. -pub struct BlankLoader; - -impl Loader for BlankLoader { - fn fonts(&self) -> &[FontInfo] { - &[] - } - - fn resolve(&self, _: &Path) -> io::Result { - Err(io::ErrorKind::NotFound.into()) - } - - fn load(&self, _: &Path) -> io::Result { - Err(io::ErrorKind::NotFound.into()) - } -} - -/// A shared buffer that is cheap to clone. -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Buffer(Prehashed>>); - -impl Buffer { - /// Return a view into the buffer. - pub fn as_slice(&self) -> &[u8] { - self - } - - /// Return a copy of the buffer as a vector. - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } -} - -impl From<&[u8]> for Buffer { - fn from(slice: &[u8]) -> Self { - Self(Prehashed::new(Arc::new(slice.to_vec()))) - } -} - -impl From> for Buffer { - fn from(vec: Vec) -> Self { - Self(Prehashed::new(Arc::new(vec))) - } -} - -impl From>> for Buffer { - fn from(arc: Arc>) -> Self { - Self(Prehashed::new(arc)) - } -} - -impl Deref for Buffer { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<[u8]> for Buffer { - fn as_ref(&self) -> &[u8] { - self - } -} - -impl Debug for Buffer { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("Buffer(..)") - } -} diff --git a/src/main.rs b/src/main.rs index 6c0ad6491..8d6a669af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,9 @@ use same_file::is_same_file; use termcolor::{ColorChoice, StandardStream, WriteColor}; use typst::diag::{Error, StrResult}; -use typst::font::{FontInfo, FontStore}; +use typst::font::FontVariant; use typst::library::text::THEME; -use typst::loading::FsLoader; +use typst::loading::{FsLoader, Loader}; use typst::parse::TokenMode; use typst::source::SourceStore; use typst::{Config, Context}; @@ -212,7 +212,7 @@ fn typeset(command: TypesetCommand) -> StrResult<()> { match typst::typeset(&mut ctx, id) { // Export the PDF. Ok(frames) => { - let buffer = typst::export::pdf(&ctx, &frames); + let buffer = typst::export::pdf(&frames); fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; } @@ -271,16 +271,12 @@ fn highlight(command: HighlightCommand) -> StrResult<()> { /// Execute a font listing command. fn fonts(command: FontsCommand) -> StrResult<()> { let loader = FsLoader::new().with_system(); - let fonts = FontStore::new(Arc::new(loader)); - - for (name, infos) in fonts.families() { + for (name, infos) in loader.book().families() { println!("{name}"); if command.variants { - for &FontInfo { variant, .. } in infos { - println!( - "- Style: {:?}, Weight: {:?}, Stretch: {:?}", - variant.style, variant.weight, variant.stretch, - ); + for info in infos { + let FontVariant { style, weight, stretch } = info.variant; + println!("- Style: {style:?}, Weight: {weight:?}, Stretch: {stretch:?}"); } } } diff --git a/src/source.rs b/src/source.rs index 226480197..24d830ad6 100644 --- a/src/source.rs +++ b/src/source.rs @@ -81,7 +81,7 @@ impl SourceStore { return Ok(id); } - let data = self.loader.load(path)?; + let data = self.loader.file(path)?; let src = String::from_utf8(data.to_vec()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8") })?; @@ -96,7 +96,7 @@ impl SourceStore { /// /// The `path` does not need to be [resolvable](Loader::resolve) through the /// `loader`. If it is though, imports that resolve to the same file hash - /// will use the inserted file instead of going through [`Loader::load`]. + /// will use the inserted file instead of going through [`Loader::file`]. /// /// If the path is resolvable and points to an existing source file, it is /// [replaced](Source::replace). diff --git a/tests/typeset.rs b/tests/typeset.rs index f66e22fc2..4b25e1767 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -234,7 +234,7 @@ fn test( if compare_ever { if let Some(pdf_path) = pdf_path { - let pdf_data = typst::export::pdf(ctx, &frames); + let pdf_data = typst::export::pdf(&frames); fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap(); fs::write(pdf_path, pdf_data).unwrap(); } @@ -245,7 +245,7 @@ fn test( } } - let canvas = render(ctx, &frames); + let canvas = render(&frames); fs::create_dir_all(&png_path.parent().unwrap()).unwrap(); canvas.save_png(png_path).unwrap(); @@ -532,7 +532,7 @@ fn test_spans_impl(node: &SyntaxNode, within: Range) -> bool { } /// Draw all frames into one image with padding in between. -fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap { +fn render(frames: &[Frame]) -> sk::Pixmap { let pixel_per_pt = 2.0; let pixmaps: Vec<_> = frames .iter() @@ -541,7 +541,7 @@ fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap { if frame.width() > limit || frame.height() > limit { panic!("overlarge frame: {:?}", frame.size()); } - typst::export::render(ctx, frame, pixel_per_pt) + typst::export::render(frame, pixel_per_pt) }) .collect(); @@ -555,7 +555,7 @@ fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap { let [x, mut y] = [pad; 2]; for (frame, mut pixmap) in frames.iter().zip(pixmaps) { let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); - render_links(&mut pixmap, ts, ctx, frame); + render_links(&mut pixmap, ts, frame); canvas.draw_pixmap( x as i32, @@ -573,18 +573,13 @@ fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap { } /// Draw extra boxes for links so we can see whether they are there. -fn render_links( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - ctx: &Context, - frame: &Frame, -) { +fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) { for (pos, element) in frame.elements() { let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32); match *element { Element::Group(ref group) => { let ts = ts.pre_concat(group.transform.into()); - render_links(canvas, ts, ctx, &group.frame); + render_links(canvas, ts, &group.frame); } Element::Link(_, size) => { let w = size.x.to_pt() as f32;