diff --git a/Cargo.toml b/Cargo.toml index 4eef9f8cb..ab8c4bcbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ 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" } -rustybuzz = "0.3" +rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" } ttf-parser = "0.9" unicode-bidi = "0.3" unicode-xid = "0.2" diff --git a/src/font.rs b/src/font.rs index bcc646272..a816ed066 100644 --- a/src/font.rs +++ b/src/font.rs @@ -4,12 +4,19 @@ use std::fmt::{self, Display, Formatter}; use fontdock::FaceFromVec; +use crate::geom::Length; + /// An owned font face. pub struct FaceBuf { data: Box<[u8]>, index: u32, ttf: ttf_parser::Face<'static>, buzz: rustybuzz::Face<'static>, + units_per_em: f64, + ascender: f64, + cap_height: f64, + x_height: f64, + descender: f64, } impl FaceBuf { @@ -36,6 +43,22 @@ impl FaceBuf { // lifetime. &self.buzz } + + /// Look up a vertical metric at a given font size. + pub fn vertical_metric(&self, size: Length, metric: VerticalFontMetric) -> Length { + self.convert(size, match metric { + VerticalFontMetric::Ascender => self.ascender, + VerticalFontMetric::CapHeight => self.cap_height, + VerticalFontMetric::XHeight => self.x_height, + VerticalFontMetric::Baseline => 0.0, + VerticalFontMetric::Descender => self.descender, + }) + } + + /// Convert from font units to a length at a given font size. + pub fn convert(&self, size: Length, units: impl Into) -> Length { + units.into() / self.units_per_em * size + } } impl FaceFromVec for FaceBuf { @@ -47,11 +70,26 @@ impl FaceFromVec for FaceBuf { let slice: &'static [u8] = unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; + let ttf = ttf_parser::Face::from_slice(slice, index).ok()?; + let buzz = rustybuzz::Face::from_slice(slice, index)?; + + // Look up some metrics we may need often. + let units_per_em = ttf.units_per_em().unwrap_or(1000); + let ascender = ttf.typographic_ascender().unwrap_or(ttf.ascender()); + let cap_height = ttf.capital_height().filter(|&h| h > 0).unwrap_or(ascender); + let x_height = ttf.x_height().filter(|&h| h > 0).unwrap_or(ascender); + let descender = ttf.typographic_descender().unwrap_or(ttf.descender()); + Some(Self { data, index, - ttf: ttf_parser::Face::from_slice(slice, index).ok()?, - buzz: rustybuzz::Face::from_slice(slice, index)?, + ttf, + buzz, + units_per_em: f64::from(units_per_em), + ascender: f64::from(ascender), + cap_height: f64::from(cap_height), + x_height: f64::from(x_height), + descender: f64::from(descender), }) } } @@ -77,38 +115,6 @@ pub enum VerticalFontMetric { 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 { diff --git a/src/layout/frame.rs b/src/layout/frame.rs index b6100b83e..21fdbf28c 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -40,62 +40,45 @@ impl Frame { #[derive(Debug, Clone, PartialEq)] pub enum Element { /// Shaped text. - Text(ShapedText), + Text(Text), /// A geometric shape. Geometry(Geometry), /// A raster image. Image(Image), } -/// A shaped run of text. +/// A run of shaped text. #[derive(Debug, Clone, PartialEq)] -pub struct ShapedText { - /// The font face the text was shaped with. - pub face: FaceId, +pub struct Text { + /// The font face the glyphs are contained in. + pub face_id: 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, + /// The glyphs. + pub glyphs: 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, - } - } +/// A glyph in a run of shaped text. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Glyph { + /// The glyph's ID in the face. + pub id: GlyphId, + /// The advance width of the glyph. + pub x_advance: Length, + /// The horizontal offset of the glyph. + pub x_offset: Length, +} +impl Text { /// 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); + for glyph in &self.glyphs { + let id = glyph.id.0; + bytes.push((id >> 8) as u8); + bytes.push((id & 0xff) as u8); } bytes } diff --git a/src/layout/par.rs b/src/layout/par.rs index 5232c9467..cad53cd59 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -1,4 +1,3 @@ -use std::cmp::Ordering; use std::fmt::{self, Debug, Formatter}; use std::mem; @@ -7,6 +6,7 @@ use xi_unicode::LineBreakIterator; use super::*; use crate::exec::FontProps; +use crate::util::RangeExt; type Range = std::ops::Range; @@ -74,7 +74,6 @@ impl ParNode { /// A paragraph representation in which children are already layouted and text /// is separated into shapable runs. -#[derive(Debug)] struct ParLayout<'a> { /// The top-level direction. dir: Dir, @@ -87,12 +86,11 @@ struct ParLayout<'a> { } /// A prepared item in a paragraph layout. -#[derive(Debug)] enum ParItem<'a> { /// Spacing between other items. Spacing(Length), /// A shaped text run with consistent direction. - Text(ShapeResult<'a>, Align), + Text(ShapedText<'a>, Align), /// A layouted child node. Frame(Frame, Align), } @@ -151,24 +149,22 @@ impl<'a> ParLayout<'a> { // TODO: Provide line break opportunities on alignment changes. for (end, mandatory) in LineBreakIterator::new(self.bidi.text) { let mut line = LineLayout::new(&self, start .. end, ctx); - let mut size = line.measure().0; - if !stack.areas.current.fits(size) { + if !stack.areas.current.fits(line.size) { if let Some((last_line, last_end)) = last.take() { stack.push(last_line); start = last_end; line = LineLayout::new(&self, start .. end, ctx); - size = line.measure().0; } } - if !stack.areas.current.height.fits(size.height) + if !stack.areas.current.height.fits(line.size.height) && !stack.areas.in_full_last() { - stack.finish_area(); + stack.finish_area(ctx); } - if mandatory || !stack.areas.current.width.fits(size.width) { + if mandatory || !stack.areas.current.width.fits(line.size.width) { stack.push(line); start = end; last = None; @@ -185,7 +181,7 @@ impl<'a> ParLayout<'a> { stack.push(line); } - stack.finish() + stack.finish(ctx) } /// Find the index of the item whose range contains the `text_offset`. @@ -200,7 +196,7 @@ impl ParItem<'_> { pub fn measure(&self) -> (Size, Length) { match self { Self::Spacing(amount) => (Size::new(*amount, Length::ZERO), Length::ZERO), - Self::Text(shaped, _) => shaped.measure(), + Self::Text(shaped, _) => (shaped.size, shaped.baseline), Self::Frame(frame, _) => (frame.size, frame.baseline), } } @@ -239,6 +235,8 @@ struct LineLayout<'a> { items: &'a [ParItem<'a>], last: Option>, ranges: &'a [Range], + size: Size, + baseline: Length, } impl<'a> LineLayout<'a> { @@ -265,7 +263,7 @@ impl<'a> LineLayout<'a> { let end = line.end - range.start; // Trim whitespace at the end of the line. - let end = start + shaped.text()[start .. end].trim_end().len(); + let end = start + shaped.text[start .. end].trim_end().len(); line.end = range.start + end; if start != end || rest.is_empty() { @@ -291,28 +289,32 @@ impl<'a> LineLayout<'a> { items = rest; } - Self { par, line, first, items, last, ranges } - } - - /// Measure the size of the line without actually building its frame. - fn measure(&self) -> (Size, Length) { let mut width = Length::ZERO; let mut top = Length::ZERO; let mut bottom = Length::ZERO; - for item in self.iter() { + for item in first.iter().chain(items).chain(&last) { let (size, baseline) = item.measure(); width += size.width; top = top.max(baseline); bottom = bottom.max(size.height - baseline); } - (Size::new(width, top + bottom), top) + Self { + par, + line, + first, + items, + last, + ranges, + size: Size::new(width, top + bottom), + baseline: top, + } } /// Build the line's frame. - fn build(&self, width: Length) -> Frame { - let (size, baseline) = self.measure(); + fn build(&self, ctx: &mut LayoutContext, width: Length) -> Frame { + let (size, baseline) = (self.size, self.baseline); let full_size = Size::new(size.width.max(width), size.height); let mut output = Frame::new(full_size, baseline); @@ -325,7 +327,9 @@ impl<'a> LineLayout<'a> { offset += amount; return; } - ParItem::Text(ref shaped, align) => (shaped.build(), align), + ParItem::Text(ref shaped, align) => { + (shaped.build(&mut ctx.env.fonts), align) + } ParItem::Frame(ref frame, align) => (frame.clone(), align), }; @@ -400,18 +404,7 @@ impl<'a> LineLayout<'a> { /// Find the range that contains the position. fn find_range(ranges: &[Range], pos: usize) -> Option { - ranges.binary_search_by(|r| cmp(r, pos)).ok() -} - -/// Comparison function for a range and a position used in binary search. -fn cmp(range: &Range, pos: usize) -> Ordering { - if pos < range.start { - Ordering::Greater - } else if pos < range.end { - Ordering::Equal - } else { - Ordering::Less - } + ranges.binary_search_by(|r| r.locate(pos)).ok() } /// Stacks lines into paragraph frames. @@ -435,19 +428,17 @@ impl<'a> LineStack<'a> { } fn push(&mut self, line: LineLayout<'a>) { - let size = line.measure().0; - - self.size.width = self.size.width.max(size.width); - self.size.height += size.height; + self.size.width = self.size.width.max(line.size.width); + self.size.height += line.size.height; if !self.lines.is_empty() { self.size.height += self.line_spacing; } - self.areas.current.height -= size.height + self.line_spacing; + self.areas.current.height -= line.size.height + self.line_spacing; self.lines.push(line); } - fn finish_area(&mut self) { + fn finish_area(&mut self, ctx: &mut LayoutContext) { let expand = self.areas.expand.horizontal; let full = self.areas.full.width; self.size.width = expand.resolve(self.size.width, full); @@ -457,7 +448,7 @@ impl<'a> LineStack<'a> { let mut first = true; for line in mem::take(&mut self.lines) { - let frame = line.build(self.size.width); + let frame = line.build(ctx, self.size.width); let height = frame.size.height; if first { @@ -474,8 +465,8 @@ impl<'a> LineStack<'a> { self.size = Size::ZERO; } - fn finish(mut self) -> Vec { - self.finish_area(); + fn finish(mut self, ctx: &mut LayoutContext) -> Vec { + self.finish_area(ctx); self.finished } } diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index 7ead0dff1..b062f602d 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::ops::Range; @@ -5,86 +6,179 @@ use fontdock::FaceId; use rustybuzz::UnicodeBuffer; use ttf_parser::GlyphId; -use super::{Element, Frame, ShapedText}; +use super::{Element, Frame, Glyph, Text}; use crate::env::FontLoader; use crate::exec::FontProps; +use crate::font::FaceBuf; use crate::geom::{Dir, Length, Point, Size}; +use crate::util::SliceExt; -/// Shape text into a frame containing [`ShapedText`] runs. -pub fn shape<'a>( - text: &'a str, - dir: Dir, - loader: &mut FontLoader, - props: &'a FontProps, -) -> ShapeResult<'a> { - let iter = props.families.iter(); - let mut results = vec![]; - shape_segment(&mut results, text, dir, loader, props, iter, None); +/// The result of shaping text. +/// +/// This type contains owned or borrowed shaped text runs, which can be +/// measured, used to reshape substrings more quickly and converted into a +/// frame. +pub struct ShapedText<'a> { + /// The text that was shaped. + pub text: &'a str, + /// The text direction. + pub dir: Dir, + /// The properties used for font selection. + pub props: &'a FontProps, + /// The font size. + pub size: Size, + /// The baseline from the top of the frame. + pub baseline: Length, + /// The shaped glyphs. + pub glyphs: Cow<'a, [ShapedGlyph]>, +} - let mut top = Length::ZERO; - let mut bottom = Length::ZERO; - for result in &results { - top = top.max(result.top); - bottom = bottom.max(result.bottom); - } +/// A single glyph resulting from shaping. +#[derive(Debug, Copy, Clone)] +pub struct ShapedGlyph { + /// The font face the glyph is contained in. + pub face_id: FaceId, + /// The glyph's ID in the face. + pub id: GlyphId, + /// The advance width of the glyph. + pub x_advance: i32, + /// The horizontal offset of the glyph. + pub x_offset: i32, + /// The start index of the glyph in the source text. + pub text_index: usize, + /// Whether splitting the shaping result before this glyph would yield the + /// same results as shaping the parts to both sides of `text_index` + /// separately. + pub safe_to_break: bool, +} - let mut frame = Frame::new(Size::new(Length::ZERO, top + bottom), top); +impl<'a> ShapedText<'a> { + /// Build the shaped text's frame. + pub fn build(&self, loader: &mut FontLoader) -> Frame { + let mut frame = Frame::new(self.size, self.baseline); + let mut x = Length::ZERO; - for shaped in results { - let offset = frame.size.width; - frame.size.width += shaped.width; + for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { + let face = loader.face(face_id); - if !shaped.glyphs.is_empty() { - frame.push(Point::new(offset, top), Element::Text(shaped)); + let pos = Point::new(x, self.baseline); + let mut text = Text { + face_id, + size: self.props.size, + color: self.props.color, + glyphs: vec![], + }; + + for glyph in group { + let x_advance = face.convert(self.props.size, glyph.x_advance); + let x_offset = face.convert(self.props.size, glyph.x_offset); + text.glyphs.push(Glyph { id: glyph.id, x_advance, x_offset }); + x += x_advance; + } + + frame.push(pos, Element::Text(text)); } + + frame } - ShapeResult { frame, text, dir, props } -} - -#[derive(Clone)] -pub struct ShapeResult<'a> { - frame: Frame, - text: &'a str, - dir: Dir, - props: &'a FontProps, -} - -impl<'a> ShapeResult<'a> { + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. pub fn reshape( - &self, - range: Range, + &'a self, + text_range: Range, loader: &mut FontLoader, - ) -> ShapeResult<'_> { - if range.start == 0 && range.end == self.text.len() { - self.clone() + ) -> ShapedText<'a> { + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + let (size, baseline) = measure(glyphs, loader, self.props); + Self { + text: &self.text[text_range], + dir: self.dir, + props: self.props, + size, + baseline, + glyphs: Cow::Borrowed(glyphs), + } } else { - shape(&self.text[range], self.dir, loader, self.props) + shape(&self.text[text_range], self.dir, loader, self.props) } } - pub fn text(&self) -> &'a str { - self.text + /// Find the subslice of glyphs that represent the given text range if both + /// sides are safe to break. + fn slice_safe_to_break(&self, text_range: Range) -> Option<&[ShapedGlyph]> { + let mut start = self.find_safe_to_break(text_range.start)?; + let mut end = self.find_safe_to_break(text_range.end)?; + + if !self.dir.is_positive() { + std::mem::swap(&mut start, &mut end); + } + + // TODO: Expand to left and right if necessary because + // find_safe_to_break may find any glyph with the text_index. + + Some(&self.glyphs[start .. end]) } - pub fn measure(&self) -> (Size, Length) { - (self.frame.size, self.frame.baseline) - } + /// Find the glyph slice offset at the text index if it's safe to break. + fn find_safe_to_break(&self, text_index: usize) -> Option { + let ltr = self.dir.is_positive(); - pub fn build(&self) -> Frame { - self.frame.clone() + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == 0 { + return Some(if ltr { 0 } else { len }); + } else if text_index == self.text.len() { + return Some(if ltr { len } else { 0 }); + } + + // TODO: Do binary search. Take care that RTL needs reversed ordering. + let idx = self + .glyphs + .iter() + .position(|g| g.text_index == text_index) + .filter(|&i| self.glyphs[i].safe_to_break)?; + + // RTL needs offset one because the the start of the range should + // be exclusive and the end inclusive. + Some(if ltr { idx } else { idx + 1 }) } } -impl Debug for ShapeResult<'_> { +impl Debug for ShapedText<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Shaped({:?})", self.text) } } -/// Shape text into a frame with font fallback using the `families` iterator. +/// Shape text into [`ShapedText`]. +pub fn shape<'a>( + text: &'a str, + dir: Dir, + loader: &mut FontLoader, + props: &'a FontProps, +) -> ShapedText<'a> { + let mut glyphs = vec![]; + let families = props.families.iter(); + if !text.is_empty() { + shape_segment(&mut glyphs, 0, text, dir, loader, props, families, None); + } + + let (size, baseline) = measure(&glyphs, loader, props); + ShapedText { + text, + dir, + props, + size, + baseline, + glyphs: Cow::Owned(glyphs), + } +} + +/// Shape text with font fallback using the `families` iterator. fn shape_segment<'a>( - results: &mut Vec, + glyphs: &mut Vec, + base: usize, text: &str, dir: Dir, loader: &mut FontLoader, @@ -93,7 +187,7 @@ fn shape_segment<'a>( mut first: Option, ) { // Select the font family. - let (id, fallback) = loop { + let (face_id, fallback) = loop { // Try to load the next available font family. match families.next() { Some(family) => match loader.query(family, props.variant) { @@ -111,22 +205,7 @@ fn shape_segment<'a>( // Register that this is the first available font. if first.is_none() { - first = Some(id); - } - - // Find out some metrics and prepare the shaped text container. - let face = loader.face(id); - 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); - - // For empty text, we want a zero-width box with the correct height. - if text.is_empty() { - results.push(shaped); - return; + first = Some(face_id); } // Fill the buffer with our text. @@ -139,59 +218,117 @@ fn shape_segment<'a>( }); // 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(); + let buffer = rustybuzz::shape(loader.face(face_id).buzz(), &[], buffer); + let infos = buffer.glyph_infos(); + let pos = buffer.glyph_positions(); - 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() { - results.push(shaped); - shaped = ShapedText::new(id, props.size, top, bottom, props.color); - } + // Collect the shaped glyphs, reshaping with the next font if necessary. + let mut i = 0; + while i < infos.len() { + let info = &infos[i]; + let cluster = info.cluster as usize; - // 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 !dir.is_positive() { - 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; - let part = &text[range]; - - // Recursively shape the tofu sequence with the next family. - shape_segment(results, part, dir, loader, props, families.clone(), first); - } else { + if info.codepoint != 0 || !fallback { // 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); + glyphs.push(ShapedGlyph { + face_id, + id: GlyphId(info.codepoint as u16), + x_advance: pos[i].x_advance, + x_offset: pos[i].x_offset, + text_index: base + cluster, + safe_to_break: !info.unsafe_to_break(), + }); + } else { + // Do font fallback if the glyph is a tofu. + // + // First, search for the end of the tofu sequence. + let k = i; + while infos.get(i + 1).map_or(false, |info| info.codepoint == 0) { + i += 1; + } + + // Determine the source text range for the tofu sequence. + let range = { + // Examples + // + // Here, _ is a tofu. + // Note that the glyph cluster length is greater than 1 char! + // + // Left-to-right clusters: + // h a l i h a l l o + // A _ _ C E + // 0 2 4 6 8 + // + // Right-to-left clusters: + // O L L A H I L A H + // E C _ _ A + // 8 6 4 2 0 + + let ltr = dir.is_positive(); + let first = if ltr { k } else { i }; + let start = infos[first].cluster as usize; + + let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) }; + let end = last + .and_then(|last| infos.get(last)) + .map(|info| info.cluster as usize) + .unwrap_or(text.len()); + + start .. end + }; + + // Recursively shape the tofu sequence with the next family. + shape_segment( + glyphs, + base + range.start, + &text[range], + dir, + loader, + props, + families.clone(), + first, + ); + } + + i += 1; + } +} + +/// Measure the size and baseline of a run of shaped glyphs with the given +/// properties. +fn measure( + glyphs: &[ShapedGlyph], + loader: &mut FontLoader, + props: &FontProps, +) -> (Size, Length) { + let mut top = Length::ZERO; + let mut bottom = Length::ZERO; + let mut width = Length::ZERO; + let mut vertical = |face: &FaceBuf| { + top = top.max(face.vertical_metric(props.size, props.top_edge)); + bottom = bottom.max(-face.vertical_metric(props.size, props.bottom_edge)); + }; + + if glyphs.is_empty() { + // When there are no glyphs, we just use the vertical metrics of the + // first available font. + for family in props.families.iter() { + if let Some(face_id) = loader.query(family, props.variant) { + vertical(loader.face(face_id)); + break; + } + } + } else { + for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { + let face = loader.face(face_id); + vertical(face); + + for glyph in group { + width += face.convert(props.size, glyph.x_advance); + } } } - if !shaped.glyphs.is_empty() { - results.push(shaped); - } + (Size::new(width, top + bottom), top) } diff --git a/src/lib.rs b/src/lib.rs index 9e09a9b45..2802c3866 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ pub mod parse; pub mod pdf; pub mod pretty; pub mod syntax; +pub mod util; use crate::diag::Pass; use crate::env::Env; diff --git a/src/pdf/mod.rs b/src/pdf/mod.rs index a97dfa2c1..656635fce 100644 --- a/src/pdf/mod.rs +++ b/src/pdf/mod.rs @@ -50,7 +50,7 @@ impl<'a> PdfExporter<'a> { for frame in frames { for (_, element) in &frame.elements { match element { - Element::Text(shaped) => fonts.insert(shaped.face), + Element::Text(shaped) => fonts.insert(shaped.face_id), Element::Image(image) => { let img = env.resources.loaded::(image.res); if img.buf.color().has_alpha() { @@ -187,11 +187,11 @@ impl<'a> PdfExporter<'a> { // Then, also check if we need to issue a font switching // action. - if shaped.face != face || shaped.size != size { - face = shaped.face; + if shaped.face_id != face || shaped.size != size { + face = shaped.face_id; size = shaped.size; - let name = format!("F{}", self.fonts.map(shaped.face)); + let name = format!("F{}", self.fonts.map(shaped.face_id)); text.font(Name(name.as_bytes()), size.to_pt() as f32); } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 000000000..6fda2fb55 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,80 @@ +/// Utilities. +use std::cmp::Ordering; +use std::ops::Range; + +/// Additional methods for slices. +pub trait SliceExt { + /// Split a slice into consecutive groups with the same key. + /// + /// Returns an iterator of pairs of a key and the group with that key. + fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> + where + F: FnMut(&T) -> K, + K: PartialEq; +} + +impl SliceExt for [T] { + fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> + where + F: FnMut(&T) -> K, + K: PartialEq, + { + GroupByKey { slice: self, f } + } +} + +/// This struct is produced by [`SliceExt::group_by_key`]. +pub struct GroupByKey<'a, T, F> { + slice: &'a [T], + f: F, +} + +impl<'a, T, K, F> Iterator for GroupByKey<'a, T, F> +where + F: FnMut(&T) -> K, + K: PartialEq, +{ + type Item = (K, &'a [T]); + + fn next(&mut self) -> Option { + let first = self.slice.first()?; + let key = (self.f)(first); + + let mut i = 1; + while self.slice.get(i).map_or(false, |t| (self.f)(t) == key) { + i += 1; + } + + let (head, tail) = self.slice.split_at(i); + self.slice = tail; + Some((key, head)) + } +} + +/// Additional methods for [`Range`]. +pub trait RangeExt { + /// Locate a position relative to a range. + /// + /// This can be used for binary searching the range that contains the + /// position as follows: + /// ``` + /// # use typst::util::RangeExt; + /// assert_eq!( + /// [1..2, 2..7, 7..10].binary_search_by(|r| r.locate(5)), + /// Ok(1), + /// ); + /// ``` + fn locate(&self, pos: usize) -> Ordering; +} + +impl RangeExt for Range { + fn locate(&self, pos: usize) -> Ordering { + if pos < self.start { + Ordering::Greater + } else if pos < self.end { + Ordering::Equal + } else { + Ordering::Less + } + } +} diff --git a/tests/ref/full/coma.png b/tests/ref/full/coma.png index 0949e271e..2d067364a 100644 Binary files a/tests/ref/full/coma.png and b/tests/ref/full/coma.png differ diff --git a/tests/ref/library/paragraph.png b/tests/ref/library/paragraph.png index 791d6c0cc..41742f20d 100644 Binary files a/tests/ref/library/paragraph.png and b/tests/ref/library/paragraph.png differ diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png index aa5d9664c..ef265cbf0 100644 Binary files a/tests/ref/text/basic.png and b/tests/ref/text/basic.png differ diff --git a/tests/ref/text/bidi.png b/tests/ref/text/bidi.png index 829fba0c0..0b7a8c2bd 100644 Binary files a/tests/ref/text/bidi.png and b/tests/ref/text/bidi.png differ diff --git a/tests/typeset.rs b/tests/typeset.rs index 5c35d6b4c..771c86daf 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -20,7 +20,7 @@ use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader}; use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::exec::State; use typst::geom::{self, Length, Point, Sides, Size}; -use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, ShapedText}; +use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Text}; use typst::library; use typst::parse::{LineMap, Scanner}; use typst::pdf; @@ -413,19 +413,20 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { canvas } -fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) { - let ttf = env.fonts.face(shaped.face).ttf(); +fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) { + let ttf = env.fonts.face(shaped.face_id).ttf(); + let mut x = 0.0; - for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) { + for glyph in &shaped.glyphs { let units_per_em = ttf.units_per_em().unwrap_or(1000); - 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); + let dx = glyph.x_offset.to_pt() as f32; + let ts = ts.pre_translate(x + dx, 0.0); // Try drawing SVG if present. if let Some(tree) = ttf - .glyph_svg_image(glyph) + .glyph_svg_image(glyph.id) .and_then(|data| std::str::from_utf8(data).ok()) .map(|svg| { let viewbox = format!("viewBox=\"0 0 {0} {0}\" xmlns", units_per_em); @@ -445,19 +446,19 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) } } } - - continue; + } else { + // Otherwise, draw normal outline. + let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new()); + if ttf.outline_glyph(glyph.id, &mut builder).is_some() { + let path = builder.0.finish().unwrap(); + 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); + } } - // Otherwise, draw normal outline. - let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new()); - if ttf.outline_glyph(glyph, &mut builder).is_some() { - let path = builder.0.finish().unwrap(); - 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); - } + x += glyph.x_advance.to_pt() as f32; } }