diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-03-24 17:12:34 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-03-24 17:12:34 +0100 |
| commit | 73615f7e3ce23f2ea656d04ea9f96184f5ebdc0a (patch) | |
| tree | 7691b792e1e4b33469a72c40fc76854f1de0814e /src/layout | |
| parent | 6720520ec06dd0718f81049b2b11e81664f7ef62 (diff) | |
Text shaping 🚀
- Shapes text with rustybuzz
- Font fallback with family list
- Tofus are shown in the first font
Co-Authored-By: Martin <mhaug@live.de>
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/frame.rs | 62 | ||||
| -rw-r--r-- | src/layout/shaping.rs | 292 | ||||
| -rw-r--r-- | src/layout/text.rs | 39 |
3 files changed, 174 insertions, 219 deletions
diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 6e876151..d3276e99 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<GlyphId>, + /// The horizontal offsets of the glyphs. This is indexed parallel to + /// `glyphs`. Vertical offsets are not yet supported. + pub offsets: Vec<Length>, +} + +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<u8> { + 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 fd10b41e..f7eece92 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}; - -/// 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<GlyphId>, - /// The horizontal offsets of the glyphs. This is indexed parallel to - /// `glyphs`. Vertical offsets are not yet supported. - pub offsets: Vec<Length>, - /// 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<u8> { - 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, -} +use crate::exec::FontProps; +use crate::geom::{Length, Point, Size}; -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 [`ShapedText`] runs. +pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame { + let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO)); + shape_segment(&mut frame, text, props.families.iter(), None, loader, props); + frame } -/// Shape text into a frame containing [`Shaped`] runs. -pub fn shape( +/// Shape text into a frame with font fallback using the `families` iterator. +fn shape_segment<'a>( + frame: &mut Frame, text: &str, - dir: Dir, - families: &FamilyMap, - variant: FontVariant, - font_size: Length, - top_edge: VerticalFontMetric, - bottom_edge: VerticalFontMetric, - color: Fill, + mut families: impl Iterator<Item = &'a str> + Clone, + mut first: Option<FaceId>, loader: &mut FontLoader, -) -> 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<Item = char> = if dir.is_positive() { - &mut forwards - } else { - &mut backwards + 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, + }, + } }; - 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; + // Register that this is the first available font. + let face = loader.face(id); + if first.is_none() { + first = Some(id); + } - // 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); + // 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); + } - 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))); + // 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; } - - shaped.text.push(c); - shaped.glyphs.push(glyph); - shaped.offsets.push(width); - width += convert(f64::from(glyph_width)); - break; + end = info.cluster as usize; + iter.next(); } - } - } - place(&mut frame, shaped, width, top, bottom); - - frame -} + // 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); + } -/// 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); + // 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); + } } -} -/// 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), + if !shaped.glyphs.is_empty() { + place(frame, shaped) } } -/// 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()) +/// Place shaped text into a frame. +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 2239afac..39866907 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<FamilyMap>, - /// 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) } } |
