diff --git a/Cargo.toml b/Cargo.toml index ecb895c0f..883d3443b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ fontdock = { path = "../fontdock", default-features = false } image = { version = "0.23", default-features = false, features = ["jpeg", "png"] } miniz_oxide = "0.3" pdf-writer = { path = "../pdf-writer" } -ttf-parser = "0.8.2" +rustybuzz = "0.3" +ttf-parser = "0.9" unicode-xid = "0.2" anyhow = { version = "1", optional = true } serde = { version = "1", features = ["derive"], optional = true } diff --git a/fonts/NotoSansArabic-Regular.ttf b/fonts/NotoSansArabic-Regular.ttf new file mode 100644 index 000000000..94eead4c0 Binary files /dev/null and b/fonts/NotoSansArabic-Regular.ttf differ diff --git a/src/env.rs b/src/env.rs index 75e2853a5..10230bbfd 100644 --- a/src/env.rs +++ b/src/env.rs @@ -7,14 +7,15 @@ use std::fs; use std::io::Cursor; use std::path::{Path, PathBuf}; -use fontdock::{FaceFromVec, FaceId, FontSource}; +use fontdock::{FaceId, FontSource}; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; -use ttf_parser::Face; #[cfg(feature = "fs")] use fontdock::{FsIndex, FsSource}; +use crate::font::FaceBuf; + /// Encapsulates all environment dependencies (fonts, resources). #[derive(Debug)] pub struct Env { @@ -47,42 +48,6 @@ impl Env { /// A font loader that is backed by a dynamic source. pub type FontLoader = fontdock::FontLoader>>; -/// An owned font face. -pub struct FaceBuf { - data: Box<[u8]>, - face: Face<'static>, -} - -impl FaceBuf { - /// Get a reference to the underlying face. - pub fn get(&self) -> &Face<'_> { - // We can't implement Deref because that would leak the internal 'static - // lifetime. - &self.face - } - - /// The raw face data. - pub fn data(&self) -> &[u8] { - &self.data - } -} - -impl FaceFromVec for FaceBuf { - fn from_vec(vec: Vec, i: u32) -> Option { - let data = vec.into_boxed_slice(); - - // SAFETY: The slices's location is stable in memory since we don't - // touch it and it can't be touched from outside this type. - let slice: &'static [u8] = - unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; - - Some(Self { - data, - face: Face::from_slice(slice, i).ok()?, - }) - } -} - /// Simplify font loader construction from an [`FsIndex`]. #[cfg(feature = "fs")] pub trait FsIndexExt { diff --git a/src/exec/context.rs b/src/exec/context.rs index 6ba5b25f5..b6a67a2ec 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -1,7 +1,4 @@ use std::mem; -use std::rc::Rc; - -use fontdock::FontStyle; use super::{Exec, FontFamily, State}; use crate::diag::{Diag, DiagSet, Pass}; @@ -87,7 +84,7 @@ impl<'a> ExecContext<'a> { /// Push a word space into the active paragraph. pub fn push_space(&mut self) { - let em = self.state.font.font_size(); + let em = self.state.font.resolve_size(); self.push(SpacingNode { amount: self.state.par.word_spacing.resolve(em), softness: 1, @@ -103,19 +100,19 @@ impl<'a> ExecContext<'a> { while let Some(c) = scanner.eat_merging_crlf() { if is_newline(c) { - self.push(self.make_text_node(mem::take(&mut line))); + self.push(TextNode::new(mem::take(&mut line), &self.state)); self.push_linebreak(); } else { line.push(c); } } - self.push(self.make_text_node(line)); + self.push(TextNode::new(line, &self.state)); } /// Apply a forced line break. pub fn push_linebreak(&mut self) { - let em = self.state.font.font_size(); + let em = self.state.font.resolve_size(); self.push_into_stack(SpacingNode { amount: self.state.par.leading.resolve(em), softness: 2, @@ -124,7 +121,7 @@ impl<'a> ExecContext<'a> { /// Apply a forced paragraph break. pub fn push_parbreak(&mut self) { - let em = self.state.font.font_size(); + let em = self.state.font.resolve_size(); self.push_into_stack(SpacingNode { amount: self.state.par.spacing.resolve(em), softness: 1, @@ -154,36 +151,6 @@ impl<'a> ExecContext<'a> { result } - /// Construct a text node from the given string based on the active text - /// state. - pub fn make_text_node(&self, text: String) -> TextNode { - let mut variant = self.state.font.variant; - - if self.state.font.strong { - variant.weight = variant.weight.thicken(300); - } - - if self.state.font.emph { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - TextNode { - text, - dir: self.state.dirs.cross, - aligns: self.state.aligns, - families: Rc::clone(&self.state.font.families), - variant, - font_size: self.state.font.font_size(), - top_edge: self.state.font.top_edge, - bottom_edge: self.state.font.bottom_edge, - color: self.state.font.color, - } - } - /// Finish the active paragraph. fn finish_par(&mut self) { let mut par = mem::replace(&mut self.par, ParNode::new(&self.state)); @@ -292,7 +259,7 @@ impl StackNode { impl ParNode { fn new(state: &State) -> Self { - let em = state.font.font_size(); + let em = state.font.resolve_size(); Self { dirs: state.dirs, aligns: state.aligns, @@ -301,3 +268,14 @@ impl ParNode { } } } + +impl TextNode { + fn new(text: String, state: &State) -> Self { + Self { + text, + dir: state.dirs.cross, + aligns: state.aligns, + props: state.font.resolve_props(), + } + } +} diff --git a/src/exec/state.rs b/src/exec/state.rs index 0322c4376..7957f3127 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -4,8 +4,9 @@ use std::rc::Rc; use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight}; use crate::color::{Color, RgbaColor}; +use crate::font::VerticalFontMetric; use crate::geom::*; -use crate::layout::{Fill, VerticalFontMetric}; +use crate::layout::Fill; use crate::paper::{Paper, PaperClass, PAPER_A4}; /// The evaluation state. @@ -100,7 +101,7 @@ impl Default for ParState { #[derive(Debug, Clone, PartialEq)] pub struct FontState { /// A list of font families with generic class definitions. - pub families: Rc, + pub families: Rc, /// The selected font variant. pub variant: FontVariant, /// The font size. @@ -111,32 +112,58 @@ pub struct FontState { pub top_edge: VerticalFontMetric, /// The bottom end of the text bounding box. pub bottom_edge: VerticalFontMetric, + /// The glyph fill color / texture. + pub color: Fill, /// Whether the strong toggle is active or inactive. This determines /// whether the next `*` adds or removes font weight. pub strong: bool, /// Whether the emphasis toggle is active or inactive. This determines /// whether the next `_` makes italic or non-italic. pub emph: bool, - /// The glyph fill color / texture. - pub color: Fill, } impl FontState { - /// Access the `families` mutably. - pub fn families_mut(&mut self) -> &mut FamilyMap { - Rc::make_mut(&mut self.families) + /// The resolved font size. + pub fn resolve_size(&self) -> Length { + self.scale.resolve(self.size) } - /// The absolute font size. - pub fn font_size(&self) -> Length { - self.scale.resolve(self.size) + /// Resolve font properties. + pub fn resolve_props(&self) -> FontProps { + let mut variant = self.variant; + + if self.strong { + variant.weight = variant.weight.thicken(300); + } + + if self.emph { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + FontProps { + families: Rc::clone(&self.families), + variant, + size: self.resolve_size(), + top_edge: self.top_edge, + bottom_edge: self.bottom_edge, + color: self.color, + } + } + + /// Access the `families` mutably. + pub fn families_mut(&mut self) -> &mut FamilyList { + Rc::make_mut(&mut self.families) } } impl Default for FontState { fn default() -> Self { Self { - families: Rc::new(FamilyMap::default()), + families: Rc::new(FamilyList::default()), variant: FontVariant { style: FontStyle::Normal, weight: FontWeight::REGULAR, @@ -146,16 +173,33 @@ impl Default for FontState { top_edge: VerticalFontMetric::CapHeight, bottom_edge: VerticalFontMetric::Baseline, scale: Linear::ONE, + color: Fill::Color(Color::Rgba(RgbaColor::BLACK)), strong: false, emph: false, - color: Fill::Color(Color::Rgba(RgbaColor::BLACK)), } } } +/// Properties used for font selection and layout. +#[derive(Debug, Clone, PartialEq)] +pub struct FontProps { + /// The list of font families to use for shaping. + pub families: Rc, + /// Which variant of the font to use. + pub variant: FontVariant, + /// The font size. + pub size: Length, + /// What line to consider the top edge of text. + pub top_edge: VerticalFontMetric, + /// What line to consider the bottom edge of text. + pub bottom_edge: VerticalFontMetric, + /// The color of the text. + pub color: Fill, +} + /// Font family definitions. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub struct FamilyMap { +pub struct FamilyList { /// The user-defined list of font families. pub list: Vec, /// Definition of serif font families. @@ -168,9 +212,9 @@ pub struct FamilyMap { pub base: Vec, } -impl FamilyMap { +impl FamilyList { /// Flat iterator over this map's family names. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator + Clone { self.list .iter() .flat_map(move |family: &FontFamily| { @@ -186,7 +230,7 @@ impl FamilyMap { } } -impl Default for FamilyMap { +impl Default for FamilyList { fn default() -> Self { Self { list: vec![FontFamily::Serif], diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 6881188d9..1abe104de 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -187,14 +187,15 @@ impl<'a> PdfExporter<'a> { // Then, also check if we need to issue a font switching // action. - if shaped.face != face || shaped.font_size != size { + if shaped.face != face || shaped.size != size { face = shaped.face; - size = shaped.font_size; + size = shaped.size; let name = format!("F{}", self.fonts.map(shaped.face)); text.font(Name(name.as_bytes()), size.to_pt() as f32); } + // TODO: Respect individual glyph offsets. text.matrix(1.0, 0.0, 0.0, 1.0, x, y); text.show(&shaped.encode_glyphs_be()); } @@ -206,10 +207,10 @@ impl<'a> PdfExporter<'a> { fn write_fonts(&mut self) { for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) { - let owned_face = self.env.fonts.face(face_id); - let face = owned_face.get(); + let face = self.env.fonts.face(face_id); + let ttf = face.ttf(); - let name = face + let name = ttf .names() .find(|entry| { entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode() @@ -228,18 +229,18 @@ impl<'a> PdfExporter<'a> { let mut flags = FontFlags::empty(); flags.set(FontFlags::SERIF, name.contains("Serif")); - flags.set(FontFlags::FIXED_PITCH, face.is_monospaced()); - flags.set(FontFlags::ITALIC, face.is_italic()); + flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); + flags.set(FontFlags::ITALIC, ttf.is_italic()); flags.insert(FontFlags::SYMBOLIC); flags.insert(FontFlags::SMALL_CAP); // Convert from OpenType font units to PDF glyph units. - let em_per_unit = 1.0 / face.units_per_em().unwrap_or(1000) as f32; + let em_per_unit = 1.0 / ttf.units_per_em().unwrap_or(1000) as f32; let convert = |font_unit: f32| (1000.0 * em_per_unit * font_unit).round(); let convert_i16 = |font_unit: i16| convert(font_unit as f32); let convert_u16 = |font_unit: u16| convert(font_unit as f32); - let global_bbox = face.global_bounding_box(); + let global_bbox = ttf.global_bounding_box(); let bbox = Rect::new( convert_i16(global_bbox.x_min), convert_i16(global_bbox.y_min), @@ -247,11 +248,11 @@ impl<'a> PdfExporter<'a> { convert_i16(global_bbox.y_max), ); - let italic_angle = face.italic_angle().unwrap_or(0.0); - let ascender = convert_i16(face.typographic_ascender().unwrap_or(0)); - let descender = convert_i16(face.typographic_descender().unwrap_or(0)); - let cap_height = face.capital_height().map(convert_i16); - let stem_v = 10.0 + 0.244 * (f32::from(face.weight().to_number()) - 50.0); + let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let ascender = convert_i16(ttf.typographic_ascender().unwrap_or(0)); + let descender = convert_i16(ttf.typographic_descender().unwrap_or(0)); + let cap_height = ttf.capital_height().map(convert_i16); + let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); // Write the base font object referencing the CID font. self.writer @@ -269,9 +270,9 @@ impl<'a> PdfExporter<'a> { .font_descriptor(refs.font_descriptor) .widths() .individual(0, { - let num_glyphs = face.number_of_glyphs(); + let num_glyphs = ttf.number_of_glyphs(); (0 .. num_glyphs).map(|g| { - let advance = face.glyph_hor_advance(GlyphId(g)); + let advance = ttf.glyph_hor_advance(GlyphId(g)); convert_u16(advance.unwrap_or(0)) }) }); @@ -294,10 +295,10 @@ impl<'a> PdfExporter<'a> { self.writer .cmap(refs.cmap, &{ let mut cmap = UnicodeCmap::new(cmap_name, system_info); - for subtable in face.character_mapping_subtables() { + for subtable in ttf.character_mapping_subtables() { subtable.codepoints(|n| { if let Some(c) = std::char::from_u32(n) { - if let Some(g) = face.glyph_index(c) { + if let Some(g) = ttf.glyph_index(c) { cmap.pair(g.0, c); } } @@ -309,7 +310,7 @@ impl<'a> PdfExporter<'a> { .system_info(system_info); // Write the face's bytes. - self.writer.stream(refs.data, owned_face.data()); + self.writer.stream(refs.data, face.data()); } } diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 000000000..78fc0d441 --- /dev/null +++ b/src/font.rs @@ -0,0 +1,115 @@ +//! Font handling. + +use std::fmt::{self, Display, Formatter}; + +use fontdock::FaceFromVec; + +/// An owned font face. +pub struct FaceBuf { + data: Box<[u8]>, + ttf: ttf_parser::Face<'static>, + buzz: rustybuzz::Face<'static>, +} + +impl FaceBuf { + /// The raw face data. + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Get a reference to the underlying ttf-parser face. + pub fn ttf(&self) -> &ttf_parser::Face<'_> { + // We can't implement Deref because that would leak the internal 'static + // lifetime. + &self.ttf + } + + /// Get a reference to the underlying rustybuzz face. + pub fn buzz(&self) -> &rustybuzz::Face<'_> { + // We can't implement Deref because that would leak the internal 'static + // lifetime. + &self.buzz + } +} + +impl FaceFromVec for FaceBuf { + fn from_vec(vec: Vec, i: u32) -> Option { + let data = vec.into_boxed_slice(); + + // SAFETY: The slices's location is stable in memory since we don't + // touch it and it can't be touched from outside this type. + let slice: &'static [u8] = + unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; + + Some(Self { + data, + ttf: ttf_parser::Face::from_slice(slice, i).ok()?, + buzz: rustybuzz::Face::from_slice(slice, i)?, + }) + } +} + +/// Identifies a vertical metric of a font. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +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, +} + +impl VerticalFontMetric { + /// Look up the metric in the given font face. + pub fn lookup(self, face: &ttf_parser::Face) -> i16 { + match self { + VerticalFontMetric::Ascender => lookup_ascender(face), + VerticalFontMetric::CapHeight => face + .capital_height() + .filter(|&h| h > 0) + .unwrap_or_else(|| lookup_ascender(face)), + VerticalFontMetric::XHeight => face + .x_height() + .filter(|&h| h > 0) + .unwrap_or_else(|| lookup_ascender(face)), + VerticalFontMetric::Baseline => 0, + VerticalFontMetric::Descender => lookup_descender(face), + } + } +} + +/// The ascender of the face. +fn lookup_ascender(face: &ttf_parser::Face) -> i16 { + // We prefer the typographic ascender over the Windows ascender because + // it can be overly large if the font has large glyphs. + face.typographic_ascender().unwrap_or_else(|| face.ascender()) +} + +/// The descender of the face. +fn lookup_descender(face: &ttf_parser::Face) -> i16 { + // See `lookup_ascender` for reason. + face.typographic_descender().unwrap_or_else(|| face.descender()) +} + +impl Display for VerticalFontMetric { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Ascender => "ascender", + Self::CapHeight => "cap-height", + Self::XHeight => "x-height", + Self::Baseline => "baseline", + Self::Descender => "descender", + }) + } +} diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 6e8761514..d3276e996 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -1,7 +1,9 @@ -use super::Shaped; +use fontdock::FaceId; +use ttf_parser::GlyphId; + use crate::color::Color; use crate::env::ResourceId; -use crate::geom::{Path, Point, Size}; +use crate::geom::{Length, Path, Point, Size}; /// A finished layout with elements at fixed positions. #[derive(Debug, Clone, PartialEq)] @@ -36,13 +38,67 @@ impl Frame { #[derive(Debug, Clone, PartialEq)] pub enum Element { /// Shaped text. - Text(Shaped), + Text(ShapedText), /// A geometric shape. Geometry(Geometry), /// A raster image. Image(Image), } +/// A shaped run of text. +#[derive(Debug, Clone, PartialEq)] +pub struct ShapedText { + /// The font face the text was shaped with. + pub face: FaceId, + /// The font size. + pub size: Length, + /// The width. + pub width: Length, + /// The extent to the top. + pub top: Length, + /// The extent to the bottom. + pub bottom: Length, + /// The glyph fill color / texture. + pub color: Fill, + /// The shaped glyphs. + pub glyphs: Vec, + /// The horizontal offsets of the glyphs. This is indexed parallel to + /// `glyphs`. Vertical offsets are not yet supported. + pub offsets: Vec, +} + +impl ShapedText { + /// Create a new shape run with `width` zero and empty `glyphs` and `offsets`. + pub fn new( + face: FaceId, + size: Length, + top: Length, + bottom: Length, + color: Fill, + ) -> Self { + Self { + face, + size, + width: Length::ZERO, + top, + bottom, + glyphs: vec![], + offsets: vec![], + color, + } + } + + /// Encode the glyph ids into a big-endian byte buffer. + pub fn encode_glyphs_be(&self) -> Vec { + let mut bytes = Vec::with_capacity(2 * self.glyphs.len()); + for &GlyphId(g) in &self.glyphs { + bytes.push((g >> 8) as u8); + bytes.push((g & 0xff) as u8); + } + bytes + } +} + /// A shape with some kind of fill. #[derive(Debug, Clone, PartialEq)] pub struct Geometry { diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index fd10b41ed..f7eece929 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -1,205 +1,129 @@ -//! Super-basic text shaping. -//! -//! This is really only suited for simple Latin text. It picks the most suitable -//! font for each individual character. When the direction is right-to-left, the -//! word is spelled backwards. Vertical shaping is not supported. - -use std::fmt::{self, Debug, Display, Formatter}; - -use fontdock::{FaceId, FontVariant}; -use ttf_parser::{Face, GlyphId}; +use fontdock::FaceId; +use rustybuzz::UnicodeBuffer; +use ttf_parser::GlyphId; +use super::{Element, Frame, ShapedText}; use crate::env::FontLoader; -use crate::exec::FamilyMap; -use crate::geom::{Dir, Length, Point, Size}; -use crate::layout::{Element, Fill, Frame}; +use crate::exec::FontProps; +use crate::geom::{Length, Point, Size}; -/// A shaped run of text. -#[derive(Clone, PartialEq)] -pub struct Shaped { - /// The shaped text. - pub text: String, - /// The font face the text was shaped with. - pub face: FaceId, - /// The shaped glyphs. - pub glyphs: Vec, - /// The horizontal offsets of the glyphs. This is indexed parallel to - /// `glyphs`. Vertical offsets are not yet supported. - pub offsets: Vec, - /// The font size. - pub font_size: Length, - /// The glyph fill color / texture. - pub color: Fill, -} - -impl Shaped { - /// Create a new shape run with empty `text`, `glyphs` and `offsets`. - pub fn new(face: FaceId, font_size: Length, color: Fill) -> Self { - Self { - text: String::new(), - face, - glyphs: vec![], - offsets: vec![], - font_size, - color, - } - } - - /// Encode the glyph ids into a big-endian byte buffer. - pub fn encode_glyphs_be(&self) -> Vec { - let mut bytes = Vec::with_capacity(2 * self.glyphs.len()); - for &GlyphId(g) in &self.glyphs { - bytes.push((g >> 8) as u8); - bytes.push((g & 0xff) as u8); - } - bytes - } -} - -impl Debug for Shaped { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Debug::fmt(&self.text, f) - } -} - -/// Identifies a vertical metric of a font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -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, -} - -impl Display for VerticalFontMetric { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Ascender => "ascender", - Self::CapHeight => "cap-height", - Self::XHeight => "x-height", - Self::Baseline => "baseline", - Self::Descender => "descender", - }) - } -} - -/// Shape text into a frame containing [`Shaped`] runs. -pub fn shape( - text: &str, - dir: Dir, - families: &FamilyMap, - variant: FontVariant, - font_size: Length, - top_edge: VerticalFontMetric, - bottom_edge: VerticalFontMetric, - color: Fill, - loader: &mut FontLoader, -) -> Frame { +/// Shape text into a frame containing shaped [`ShapedText`] runs. +pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame { let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO)); - let mut shaped = Shaped::new(FaceId::MAX, font_size, color); - let mut width = Length::ZERO; - let mut top = Length::ZERO; - let mut bottom = Length::ZERO; - - // Create an iterator with conditional direction. - let mut forwards = text.chars(); - let mut backwards = text.chars().rev(); - let chars: &mut dyn Iterator = if dir.is_positive() { - &mut forwards - } else { - &mut backwards - }; - - for c in chars { - for family in families.iter() { - if let Some(id) = loader.query(family, variant) { - let face = loader.face(id).get(); - let (glyph, glyph_width) = match lookup_glyph(face, c) { - Some(v) => v, - None => continue, - }; - - let units_per_em = f64::from(face.units_per_em().unwrap_or(1000)); - let convert = |units| units / units_per_em * font_size; - - // Flush the buffer and reset the metrics if we use a new font face. - if shaped.face != id { - place(&mut frame, shaped, width, top, bottom); - - shaped = Shaped::new(id, font_size, color); - width = Length::ZERO; - top = convert(f64::from(lookup_metric(face, top_edge))); - bottom = convert(f64::from(lookup_metric(face, bottom_edge))); - } - - shaped.text.push(c); - shaped.glyphs.push(glyph); - shaped.offsets.push(width); - width += convert(f64::from(glyph_width)); - break; - } - } - } - - place(&mut frame, shaped, width, top, bottom); - + shape_segment(&mut frame, text, props.families.iter(), None, loader, props); frame } +/// Shape text into a frame with font fallback using the `families` iterator. +fn shape_segment<'a>( + frame: &mut Frame, + text: &str, + mut families: impl Iterator + Clone, + mut first: Option, + loader: &mut FontLoader, + props: &FontProps, +) { + // Select the font family. + let (id, fallback) = loop { + // Try to load the next available font family. + match families.next() { + Some(family) => match loader.query(family, props.variant) { + Some(id) => break (id, true), + None => {} + }, + // We're out of families, so we don't do any more fallback and just + // shape the tofus with the first face we originally used. + None => match first { + Some(id) => break (id, false), + None => return, + }, + } + }; + + // Register that this is the first available font. + let face = loader.face(id); + if first.is_none() { + first = Some(id); + } + + // Find out some metrics and prepare the shaped text container. + let ttf = face.ttf(); + let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000)); + let convert = |units| f64::from(units) / units_per_em * props.size; + let top = convert(i32::from(props.top_edge.lookup(ttf))); + let bottom = convert(i32::from(props.bottom_edge.lookup(ttf))); + let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color); + + // Fill the buffer with our text. + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(text); + buffer.guess_segment_properties(); + + // Find out the text direction. + // TODO: Replace this once we do BiDi. + let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft); + + // Shape! + let glyphs = rustybuzz::shape(face.buzz(), &[], buffer); + let info = glyphs.glyph_infos(); + let pos = glyphs.glyph_positions(); + let mut iter = info.iter().zip(pos).peekable(); + + while let Some((info, pos)) = iter.next() { + // Do font fallback if the glyph is a tofu. + if info.codepoint == 0 && fallback { + // Flush what we have so far. + if !shaped.glyphs.is_empty() { + place(frame, shaped); + shaped = ShapedText::new(id, props.size, top, bottom, props.color); + } + + // Determine the start and end cluster index of the tofu sequence. + let mut start = info.cluster as usize; + let mut end = info.cluster as usize; + while let Some((info, _)) = iter.peek() { + if info.codepoint != 0 { + break; + } + end = info.cluster as usize; + iter.next(); + } + + // Because Harfbuzz outputs glyphs in visual order, the start + // cluster actually corresponds to the last codepoint in + // right-to-left text. + if rtl { + assert!(end <= start); + std::mem::swap(&mut start, &mut end); + } + + // The end cluster index points right before the last character that + // mapped to the tofu sequence. So we have to offset the end by one + // char. + let offset = text[end ..].chars().next().unwrap().len_utf8(); + let range = start .. end + offset; + + // Recursively shape the tofu sequence with the next family. + shape_segment(frame, &text[range], families.clone(), first, loader, props); + } else { + // Add the glyph to the shaped output. + // TODO: Don't ignore y_advance and y_offset. + let glyph = GlyphId(info.codepoint as u16); + shaped.glyphs.push(glyph); + shaped.offsets.push(shaped.width + convert(pos.x_offset)); + shaped.width += convert(pos.x_advance); + } + } + + if !shaped.glyphs.is_empty() { + place(frame, shaped) + } +} + /// Place shaped text into a frame. -fn place(frame: &mut Frame, shaped: Shaped, width: Length, top: Length, bottom: Length) { - if !shaped.text.is_empty() { - frame.push(Point::new(frame.size.width, top), Element::Text(shaped)); - frame.size.width += width; - frame.size.height = frame.size.height.max(top - bottom); - } -} - -/// Look up the glyph for `c` and returns its index alongside its advance width. -fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> { - let glyph = face.glyph_index(c)?; - let width = face.glyph_hor_advance(glyph)?; - Some((glyph, width)) -} - -/// Look up a vertical metric. -fn lookup_metric(face: &Face, metric: VerticalFontMetric) -> i16 { - match metric { - VerticalFontMetric::Ascender => lookup_ascender(face), - VerticalFontMetric::CapHeight => face - .capital_height() - .filter(|&h| h > 0) - .unwrap_or_else(|| lookup_ascender(face)), - VerticalFontMetric::XHeight => face - .x_height() - .filter(|&h| h > 0) - .unwrap_or_else(|| lookup_ascender(face)), - VerticalFontMetric::Baseline => 0, - VerticalFontMetric::Descender => lookup_descender(face), - } -} - -/// The ascender of the face. -fn lookup_ascender(face: &Face) -> i16 { - // We prefer the typographic ascender over the Windows ascender because - // it can be overly large if the font has large glyphs. - face.typographic_ascender().unwrap_or_else(|| face.ascender()) -} - -/// The descender of the face. -fn lookup_descender(face: &Face) -> i16 { - // See `lookup_ascender` for reason. - face.typographic_descender().unwrap_or_else(|| face.descender()) +fn place(frame: &mut Frame, shaped: ShapedText) { + let offset = frame.size.width; + frame.size.width += shaped.width; + frame.size.height = frame.size.height.max(shaped.top - shaped.bottom); + frame.push(Point::new(offset, shaped.top), Element::Text(shaped)); } diff --git a/src/layout/text.rs b/src/layout/text.rs index 2239afac1..398669076 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -1,50 +1,25 @@ use std::fmt::{self, Debug, Formatter}; -use std::rc::Rc; - -use fontdock::FontVariant; use super::*; -use crate::exec::FamilyMap; +use crate::exec::FontProps; /// A consecutive, styled run of text. #[derive(Clone, PartialEq)] pub struct TextNode { - /// The text. - pub text: String, /// The text direction. pub dir: Dir, /// How to align this text node in its parent. pub aligns: LayoutAligns, - /// The list of font families for shaping. - pub families: Rc, - /// The font variant, - pub variant: FontVariant, - /// The font size. - pub font_size: Length, - /// The top end of the text bounding box. - pub top_edge: VerticalFontMetric, - /// The bottom end of the text bounding box. - pub bottom_edge: VerticalFontMetric, - /// The glyph fill. - pub color: Fill, + /// The text. + pub text: String, + /// Properties used for font selection and layout. + pub props: FontProps, } impl Layout for TextNode { fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Fragment { - Fragment::Frame( - shape( - &self.text, - self.dir, - &self.families, - self.variant, - self.font_size, - self.top_edge, - self.bottom_edge, - self.color, - &mut ctx.env.fonts, - ), - self.aligns, - ) + let frame = shape(&self.text, &mut ctx.env.fonts, &self.props); + Fragment::Frame(frame, self.aligns) } } diff --git a/src/lib.rs b/src/lib.rs index cc370a0a8..20c69fe92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,7 @@ pub mod color; pub mod env; pub mod exec; pub mod export; +pub mod font; pub mod geom; pub mod layout; pub mod library; diff --git a/src/library/mod.rs b/src/library/mod.rs index 58e62d566..1f412cd0f 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -32,8 +32,8 @@ use fontdock::{FontStyle, FontWeight}; use crate::eval::{AnyValue, FuncValue, Scope}; use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value}; use crate::exec::{Exec, ExecContext, FontFamily}; +use crate::font::VerticalFontMetric; use crate::geom::*; -use crate::layout::VerticalFontMetric; use crate::syntax::{Node, Spanned}; /// Construct a scope containing all standard library definitions. diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 506f65856..d4648566d 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -27,7 +27,7 @@ fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> V let spacing: Option = args.require(ctx, "spacing"); Value::template("spacing", move |ctx| { if let Some(linear) = spacing { - let amount = linear.resolve(ctx.state.font.font_size()); + let amount = linear.resolve(ctx.state.font.resolve_size()); let spacing = SpacingNode { amount, softness: 0 }; if axis == ctx.state.dirs.main.axis() { ctx.push_into_stack(spacing); diff --git a/tests/ref/control/for.png b/tests/ref/control/for.png index b37576928..1c7dfd42e 100644 Binary files a/tests/ref/control/for.png and b/tests/ref/control/for.png differ diff --git a/tests/ref/control/if.png b/tests/ref/control/if.png index e38cf7c45..b30e63f88 100644 Binary files a/tests/ref/control/if.png and b/tests/ref/control/if.png differ diff --git a/tests/ref/control/let.png b/tests/ref/control/let.png index 20aad953a..8a5c5fc68 100644 Binary files a/tests/ref/control/let.png and b/tests/ref/control/let.png differ diff --git a/tests/ref/expr/call.png b/tests/ref/expr/call.png index 26f1cb03b..f05fb8351 100644 Binary files a/tests/ref/expr/call.png and b/tests/ref/expr/call.png differ diff --git a/tests/ref/expr/ops.png b/tests/ref/expr/ops.png index 2f3a28ea1..e6f2edabf 100644 Binary files a/tests/ref/expr/ops.png and b/tests/ref/expr/ops.png differ diff --git a/tests/ref/full/coma.png b/tests/ref/full/coma.png index 5fd6d801c..d869d57ee 100644 Binary files a/tests/ref/full/coma.png and b/tests/ref/full/coma.png differ diff --git a/tests/ref/library/circle.png b/tests/ref/library/circle.png index 9e89d98b7..f11286422 100644 Binary files a/tests/ref/library/circle.png and b/tests/ref/library/circle.png differ diff --git a/tests/ref/library/ellipse.png b/tests/ref/library/ellipse.png index de178d60e..c78c92727 100644 Binary files a/tests/ref/library/ellipse.png and b/tests/ref/library/ellipse.png differ diff --git a/tests/ref/library/font.png b/tests/ref/library/font.png index 57a45006c..07c657261 100644 Binary files a/tests/ref/library/font.png and b/tests/ref/library/font.png differ diff --git a/tests/ref/library/pad.png b/tests/ref/library/pad.png index 3536db5d1..3bc0ddd06 100644 Binary files a/tests/ref/library/pad.png and b/tests/ref/library/pad.png differ diff --git a/tests/ref/library/page.png b/tests/ref/library/page.png index 7d1ff96f7..8e5a83ffb 100644 Binary files a/tests/ref/library/page.png and b/tests/ref/library/page.png differ diff --git a/tests/ref/library/pagebreak.png b/tests/ref/library/pagebreak.png index d513c963a..ab990c691 100644 Binary files a/tests/ref/library/pagebreak.png and b/tests/ref/library/pagebreak.png differ diff --git a/tests/ref/library/paragraph.png b/tests/ref/library/paragraph.png index bf38bdf8c..378980170 100644 Binary files a/tests/ref/library/paragraph.png and b/tests/ref/library/paragraph.png differ diff --git a/tests/ref/library/rect.png b/tests/ref/library/rect.png index 81ee91d70..56f1003fc 100644 Binary files a/tests/ref/library/rect.png and b/tests/ref/library/rect.png differ diff --git a/tests/ref/library/spacing.png b/tests/ref/library/spacing.png index 6a234a8a1..c266b9fab 100644 Binary files a/tests/ref/library/spacing.png and b/tests/ref/library/spacing.png differ diff --git a/tests/ref/library/square.png b/tests/ref/library/square.png index 401b1ab2a..26469d20a 100644 Binary files a/tests/ref/library/square.png and b/tests/ref/library/square.png differ diff --git a/tests/ref/markup/emph.png b/tests/ref/markup/emph.png index f43eeecb7..9b6bda5cd 100644 Binary files a/tests/ref/markup/emph.png and b/tests/ref/markup/emph.png differ diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png index 561b5f1e8..9c6f1f599 100644 Binary files a/tests/ref/markup/escape.png and b/tests/ref/markup/escape.png differ diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png index f95b8c2cd..a32229e7a 100644 Binary files a/tests/ref/markup/heading.png and b/tests/ref/markup/heading.png differ diff --git a/tests/ref/markup/linebreak.png b/tests/ref/markup/linebreak.png index 4dfca22fb..512fa0f5e 100644 Binary files a/tests/ref/markup/linebreak.png and b/tests/ref/markup/linebreak.png differ diff --git a/tests/ref/markup/nbsp.png b/tests/ref/markup/nbsp.png index 89f75f145..cc920776a 100644 Binary files a/tests/ref/markup/nbsp.png and b/tests/ref/markup/nbsp.png differ diff --git a/tests/ref/markup/parbreak.png b/tests/ref/markup/parbreak.png index 008afca25..f100b9d73 100644 Binary files a/tests/ref/markup/parbreak.png and b/tests/ref/markup/parbreak.png differ diff --git a/tests/ref/markup/raw.png b/tests/ref/markup/raw.png index 1aebc02db..a20ca9994 100644 Binary files a/tests/ref/markup/raw.png and b/tests/ref/markup/raw.png differ diff --git a/tests/ref/markup/strong.png b/tests/ref/markup/strong.png index 20e29b1f7..4bbf6ac0e 100644 Binary files a/tests/ref/markup/strong.png and b/tests/ref/markup/strong.png differ diff --git a/tests/ref/repr.png b/tests/ref/repr.png index 390c2cab2..71f415ef2 100644 Binary files a/tests/ref/repr.png and b/tests/ref/repr.png differ diff --git a/tests/ref/text.png b/tests/ref/text.png deleted file mode 100644 index 1dd70c1cd..000000000 Binary files a/tests/ref/text.png and /dev/null differ diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png new file mode 100644 index 000000000..a06c87638 Binary files /dev/null and b/tests/ref/text/basic.png differ diff --git a/tests/ref/text/complex.png b/tests/ref/text/complex.png new file mode 100644 index 000000000..9af49f160 Binary files /dev/null and b/tests/ref/text/complex.png differ diff --git a/tests/typ/text.typ b/tests/typ/text/basic.typ similarity index 100% rename from tests/typ/text.typ rename to tests/typ/text/basic.typ diff --git a/tests/typ/text/complex.typ b/tests/typ/text/complex.typ new file mode 100644 index 000000000..567a208d2 --- /dev/null +++ b/tests/typ/text/complex.typ @@ -0,0 +1,38 @@ +// Test complex text shaping. + +--- +// Test ligatures. + +// This should create an "fi" ligature. +Le fira + +// This should just shape nicely. +#font("Noto Sans Arabic") +منش إلا بسم الله + +// This should form a three-member family. +#font("Twitter Color Emoji") +👩‍👩‍👦 🤚🏿 + +// These two shouldn't be affected by a zero-width joiner. +🏞‍🌋 + +--- +// Test font fallback. + +#font("EB Garamond", "Noto Sans Arabic", "Twitter Color Emoji") + +// Font fallback for emoji. +A😀B + +// Font fallback for entire text. +منش إلا بسم الله + +// Font fallback in right-to-left text. +ب🐈😀سم + +// Multi-layer font fallback. +Aب😀🏞سمB + +// Tofus are rendered with the first font. +A🐈中文B diff --git a/tests/typeset.rs b/tests/typeset.rs index c5ec01d26..b38311aa5 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -21,7 +21,7 @@ use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::exec::State; use typst::export::pdf; use typst::geom::{self, Length, Point, Sides, Size}; -use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Shaped}; +use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, ShapedText}; use typst::library; use typst::parse::{LineMap, Scanner}; use typst::pretty::pretty; @@ -391,15 +391,18 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { for &(pos, ref element) in &frame.elements { let pos = origin + pos; + let x = pos.x.to_pt() as f32; + let y = pos.y.to_pt() as f32; + let ts = ts.pre_translate(x, y); match element { Element::Text(shaped) => { - draw_text(&mut canvas, env, ts, pos, shaped); + draw_text(&mut canvas, env, ts, shaped); } Element::Image(image) => { - draw_image(&mut canvas, env, ts, pos, image); + draw_image(&mut canvas, env, ts, image); } Element::Geometry(geom) => { - draw_geometry(&mut canvas, ts, pos, geom); + draw_geometry(&mut canvas, ts, geom); } } } @@ -410,18 +413,18 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { canvas } -fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped: &Shaped) { - let face = env.fonts.face(shaped.face).get(); +fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) { + let ttf = env.fonts.face(shaped.face).ttf(); for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) { - let units_per_em = face.units_per_em().unwrap_or(1000); + let units_per_em = ttf.units_per_em().unwrap_or(1000); - let x = (pos.x + offset).to_pt() as f32; - let y = pos.y.to_pt() as f32; - let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32; + let x = offset.to_pt() as f32; + let s = (shaped.size / units_per_em as f64).to_pt() as f32; + let ts = ts.pre_translate(x, 0.0); // Try drawing SVG if present. - if let Some(tree) = face + if let Some(tree) = ttf .glyph_svg_image(glyph) .and_then(|data| std::str::from_utf8(data).ok()) .map(|svg| { @@ -433,11 +436,9 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped: for child in tree.root().children() { if let usvg::NodeKind::Path(node) = &*child.borrow() { let path = convert_usvg_path(&node.data); - let transform = convert_usvg_transform(node.transform); - let ts = transform - .post_concat(Transform::from_row(scale, 0.0, 0.0, scale, x, y)) + let ts = convert_usvg_transform(node.transform) + .post_scale(s, s) .post_concat(ts); - if let Some(fill) = &node.fill { let (paint, fill_rule) = convert_usvg_fill(fill); canvas.fill_path(&path, &paint, fill_rule, ts, None); @@ -450,9 +451,9 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped: // Otherwise, draw normal outline. let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new()); - if face.outline_glyph(glyph, &mut builder).is_some() { + if ttf.outline_glyph(glyph, &mut builder).is_some() { let path = builder.0.finish().unwrap(); - let ts = Transform::from_row(scale, 0.0, 0.0, -scale, x, y).post_concat(ts); + let ts = ts.pre_scale(s, -s); let mut paint = convert_typst_fill(shaped.color); paint.anti_alias = true; canvas.fill_path(&path, &paint, FillRule::default(), ts, None); @@ -460,11 +461,7 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped: } } -fn draw_geometry(canvas: &mut Pixmap, ts: Transform, pos: Point, element: &Geometry) { - let x = pos.x.to_pt() as f32; - let y = pos.y.to_pt() as f32; - let ts = Transform::from_translate(x, y).post_concat(ts); - +fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) { let paint = convert_typst_fill(element.fill); let rule = FillRule::default(); @@ -486,13 +483,7 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, pos: Point, element: &Geome }; } -fn draw_image( - canvas: &mut Pixmap, - env: &Env, - ts: Transform, - pos: Point, - element: &Image, -) { +fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) { let img = &env.resources.loaded::(element.res); let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap(); @@ -501,8 +492,6 @@ fn draw_image( *dest = ColorU8::from_rgba(r, g, b, a).premultiply(); } - let x = pos.x.to_pt() as f32; - let y = pos.y.to_pt() as f32; let view_width = element.size.width.to_pt() as f32; let view_height = element.size.height.to_pt() as f32; let scale_x = view_width as f32 / pixmap.width() as f32; @@ -514,10 +503,10 @@ fn draw_image( SpreadMode::Pad, FilterQuality::Bilinear, 1.0, - Transform::from_row(scale_x, 0.0, 0.0, scale_y, x, y), + Transform::from_row(scale_x, 0.0, 0.0, scale_y, 0.0, 0.0), ); - let rect = Rect::from_xywh(x, y, view_width, view_height).unwrap(); + let rect = Rect::from_xywh(0.0, 0.0, view_width, view_height).unwrap(); canvas.fill_rect(rect, &paint, ts, None); }