From 73615f7e3ce23f2ea656d04ea9f96184f5ebdc0a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 24 Mar 2021 17:12:34 +0100 Subject: =?UTF-8?q?Text=20shaping=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shapes text with rustybuzz - Font fallback with family list - Tofus are shown in the first font Co-Authored-By: Martin --- src/layout/shaping.rs | 292 +++++++++++++++++++------------------------------- 1 file changed, 108 insertions(+), 184 deletions(-) (limited to 'src/layout/shaping.rs') 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, - /// 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, -} +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 + Clone, + mut first: Option, 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 = 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)); } -- cgit v1.2.3