//! Line breaking and glyph placement for point, area and on-path type. use std::ops::Range; use kurbo::{Affine, BezPath, ParamCurve, PathEl, Point, Rect, Shape, Vec2}; use unicode_bidi::{BidiInfo, Level}; use vectorcraft_doc::text::Kashida; use vectorcraft_doc::{ Burasagari, CharStyle, HyphenationOptions, InlineArt, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign, RubyPosition, SingleWordJustify, TextKind, TextObject, }; use vectorcraft_geom::{ArcPath, PathData}; use crate::GlyphBend; use crate::composer::{Breakpoint, Params, compose}; use crate::fontdb::FontDb; use crate::hyphen::points_for; use crate::justify::{Flex, LineFit, apply_desired, fit_line, is_word_space}; use crate::kashida; use crate::shape::{ Punct, SGlyph, Tcy, cap_x_heights, hyphen_glyph, is_cjk, mark_word_ends, no_line_end, no_line_start, punct, shape_range, soft_line_start, style_metrics, }; use crate::{Composer, FirstBaseline, InlineGlyph, LayoutOptions, LineInfo, ListMarker, OtFeatures, PositionedGlyph, TextLayout, VerticalAlign}; mod bidi; mod breaking; mod driver; mod flow; mod on_path; mod region; mod ruby; mod vertical; pub(crate) use bidi::{is_rtl, para_bidi}; use breaking::tab_advance; #[cfg(test)] use breaking::{break_line, candidates}; pub use driver::{decorations, fit_first_line, layout, layout_with, scaled}; use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, upright_extent, whole_character_widths}; const EPS: f64 = 1e-6; /// Is glyph `g` a later glyph of the cluster of glyph `prev` before it (a combining mark the shaper /// set as a glyph of its own)? A list marker's glyphs stand for no character and are not. fn continues_cluster(prev: Option<&SGlyph>, g: Option<&SGlyph>) -> bool { prev.zip(g).is_some_and(|(a, b)| a.len > 0 && b.len > 0 && a.byte == b.byte) } /// The caret stops inside glyph `g` when it alone stands for several characters (grapheme clusters) /// of `text`, a ligature ([`PositionedGlyph::carets`]); `rtl` when its logical leading edge is its /// right edge; `upright` when it stands upright in vertical type, its carets then down its cell /// ([`upright_extent`]). Empty for a glyph of one character. fn ligature_carets(text: &str, g: &SGlyph, rtl: bool, upright: bool) -> Vec<(usize, f64)> { use unicode_segmentation::UnicodeSegmentation; let Some(s) = text.get(g.byte..g.byte + g.len).filter(|_| g.len > 1) else { return Vec::new() }; let starts: Vec = s.grapheme_indices(true).skip(1).map(|(i, _)| g.byte + i).collect(); if starts.is_empty() { return Vec::new(); } let parts = starts.len() + 1; let from_font = if upright { let (cell, top) = upright_extent(g); g.face .vertical_ligature_carets(g.gid, starts.len(), top, cell) .filter(|c| c.len() == starts.len() && cell > 0.0) .map(|c| c.into_iter().map(|d| d / cell).collect::>()) } else { let advance = g.face.advance(g.gid); g.face .ligature_carets(g.gid, starts.len()) .filter(|c| c.len() == starts.len() && advance > 0.0) .map(|c| c.into_iter().map(|x| if rtl { 1.0 - x / advance } else { x / advance }).collect::>()) }; let mut at = from_font.unwrap_or_else(|| (1..parts).map(|j| j as f64 / parts as f64).collect()); at.sort_by(f64::total_cmp); starts.into_iter().zip(at).collect() } /// Does glyph `g`, standing upright in vertical type, take a cell of its own down the column? Every /// glyph with an advance does, including one whose tracking or kerning makes its advance zero or /// negative; a glyph without a font advance (a mark) does not. fn takes_cell(g: &SGlyph) -> bool { g.adv > 0.0 || g.face.advance(g.gid) > 0.0 } /// Clear the caret stops of each glyph that shares its cluster with another glyph: only a glyph /// that alone stands for its characters is a ligature. fn keep_lone_ligature_carets(glyphs: &mut [PositionedGlyph]) { if glyphs.iter().all(|g| g.carets.is_empty()) { return; } let mut count: std::collections::HashMap<(usize, usize), usize> = std::collections::HashMap::new(); for g in glyphs.iter().filter(|g| g.len > 0) { *count.entry((g.byte, g.len)).or_default() += 1; } for g in glyphs.iter_mut().filter(|g| !g.carets.is_empty()) { if count.get(&(g.byte, g.len)).is_some_and(|n| *n > 1) { g.carets.clear(); } } } struct Ctx<'a> { on_path: bool, db: &'a FontDb, text: &'a str, runs: Vec<(Range, &'a CharStyle)>, /// The inline graphic of each run that is one (parallel to `runs`). inlines: Vec>, default: CharStyle, opts: &'a LayoutOptions, /// Vertical type: lines are laid out as horizontal lines in line space, upright glyphs turned /// a quarter turn back (see [`stands_upright`]); [`TextLayout::line_xf`] then stands the lines /// up as columns. vertical: bool, /// Character panel menu › System Layout. On: positive advances are whole points. system_layout: bool, /// Character panel menu › Fractional Widths. Off, positive advances are whole points. fractional_widths: bool, /// Vertical type: the upright glyph [`Self::emit`] placed last, as (its byte, the index the /// next glyph of its cluster would take, the turn it got, the line-space x that next glyph is /// set after), so that a combining mark shaped into the same cluster turns with it. upright_cluster: Option<(usize, usize, Affine, f64)>, out: TextLayout, } impl Ctx<'_> { fn style_at(&self, b: usize) -> &CharStyle { // The runs follow one another: the first that ends after `b` is the only one that can hold it. let find = |b: usize| { let i = self.runs.partition_point(|(r, _)| r.end <= b); self.runs.get(i).filter(|(r, _)| r.start <= b).map(|(_, s)| *s) }; find(b).or_else(|| b.checked_sub(1).and_then(find)).or_else(|| self.runs.first().map(|(_, s)| *s)).unwrap_or(&self.default) } /// The stretches of paragraph `r` whose characters have a Character Direction, as byte ranges /// from `r.start` with their direction, in order; neighboring runs of one direction make one /// stretch. fn char_directions(&self, r: &Range) -> Vec<(Range, ParaDirection)> { let mut out: Vec<(Range, ParaDirection)> = Vec::new(); // Only the paragraph's own runs (they are in order), not every run of the text. let first = self.runs.partition_point(|(rr, _)| rr.end <= r.start); for (rr, st) in self.runs.iter().skip(first).take_while(|(rr, _)| rr.start < r.end) { let (a, b) = (rr.start.max(r.start), rr.end.min(r.end)); let Some(d) = st.char_direction.filter(|_| a < b) else { continue }; let (a, b) = (a - r.start, b - r.start); match out.last_mut() { Some((prev, pd)) if *pd == d && prev.end == a => prev.end = b, _ => out.push((a..b, d)), } } out } /// Shape paragraph `r` (its bidi resolution `bidi`). fn shape_para(&self, r: Range, bidi: Option<&BidiInfo<'_>>) -> Vec { let mut v = Vec::with_capacity(r.len()); let levels = bidi.map_or(&[][..], |b| &b.levels); shape_range(self.db, self.text, r, &self.runs, &self.inlines, &self.opts.features, levels, &mut v); self.finish_shaping(&mut v, |g| self.style_at(g.byte)); v } /// Set shaped glyphs `v` as the type sets them: in vertical type, tate-chu-yoko (by the size /// and Tate-chu-yoko of each glyph's `style`) and the cells of upright glyphs; with System /// Layout on or Fractional Widths off, whole-point advances. fn finish_shaping<'s>(&self, v: &mut [SGlyph], style: impl Fn(&SGlyph) -> &'s CharStyle) { if self.vertical { tate_chu_yoko(v, |g| { let st = style(g); TcyStyle { size: st.size, marked: st.tate_chu_yoko, up: st.tcy_up_down, right: st.tcy_left_right } }); // An upright glyph advances down the column by its vertical advance (the font's vertical // metrics; without them at least one em, as CJK fonts' is), centred across it: a narrow // mark like § must not overlap its neighbours. A combining mark shaped into the cluster // of the glyph before it takes no cell of its own: it turns with that glyph (`emit`). for i in 0..v.len() { let continues = continues_cluster(i.checked_sub(1).and_then(|p| v.get(p)), v.get(i)); let Some(g) = v.get_mut(i).filter(|g| g.adv > 0.0 && !continues && stands_upright(g)) else { continue }; let extra = upright_cell(g) - g.face.advance(g.gid) * g.sx; g.dx += extra * 0.5; g.adv += extra; // Proportional Metrics: `vpal` (and the vertical kerning with it) moves it and // changes its length down the column, in fonts with vertical metrics (else the cell // stays as it is, #966). if let Some(a) = g.vpal.filter(|_| g.tcy.is_none() && g.face.vertical_glyph(g.gid).is_some()) { g.dx += a.dx; g.dy += a.dy; g.adv += a.advance; g.proportional = a.proportional; } } } // System Layout or Fractional Widths off: whole-point advances, after tate-chu-yoko and the // upright cell have set the widths the line breaker will use. A zero advance stays zero. if self.system_layout || !self.fractional_widths { whole_character_widths(v); } } /// Bullets and numbered lists: put marker `text` (the bullet, or the number and its period; /// [`list_markers`]) of list paragraph `pr` (attributes `para`, right to left when `rtl`) /// before its glyphs `sg` and return how many glyphs went in (none for a paragraph without a /// marker). The marker's characters are set in /// [`vectorcraft_doc::text::ParaList::marker_style`], and a tab after them takes the text to /// the next tab stop past them on the first line, the left indent counting as a stop when no /// explicit stop comes before it. In a horizontal right-to-left paragraph, whose indents are /// mirrored ([`line_indents`]), these are measured from the right, and a number reads left to /// right with its period at its left. The tab keeps that width however the line is aligned or /// justified. Each of these glyphs stands for no character of the text: length 0, at the /// paragraph's first byte, in its first character's run, at the paragraph's embedding level. fn prepend_marker(&self, pr: &Range, para: &ParaStyle, text: Option<&str>, rtl: bool, sg: &mut Vec) -> usize { let Some(text) = text.filter(|_| para.list.is_some() && !pr.is_empty()) else { return 0 }; let st = vectorcraft_doc::text::ParaList::marker_style(self.style_at(pr.start)); let run = self.runs.partition_point(|(r, _)| r.end <= pr.start); if !self.runs.get(run).is_some_and(|(r, _)| r.start <= pr.start && pr.start < r.end) { return 0; } // The paragraph's direction: the marker starts a right-to-left paragraph at its right. let level = if rtl { Level::rtl() } else { Level::ltr() }; let mut marker = Vec::with_capacity(text.len() + 1); shape_range(self.db, text, 0..text.len(), &[(0..text.len(), &st)], &[None], &self.opts.features, &vec![level; text.len()], &mut marker); if rtl { // [`visual_order`] draws a right-to-left marker's glyphs in reverse: the number's // glyphs go in reversed, so that it reads left to right, and the period after them. let number = marker.iter().take_while(|g| text.get(g.byte..).is_some_and(|s| !s.starts_with('.'))).count(); marker[..number].reverse(); } for g in &mut marker { g.byte = pr.start; g.len = 0; g.run = run; } self.finish_shaping(&mut marker, |_| &st); let Some(last) = marker.last() else { return 0 }; let mut tab = last.clone(); tab.ch = '\t'; tab.gid = 0; tab.dx = 0.0; tab.dy = 0.0; tab.kern_shift = 0.0; tab.tcy = None; tab.vpal = None; let pen = para.left_indent + para.first_line_indent + marker.iter().map(|g| g.adv).sum::(); let to_indent = para.left_indent - pen; let stop_before_indent = para.tabs.iter().any(|t| t.position > pen + EPS && t.position < para.left_indent - EPS); tab.adv = if to_indent > EPS && !stop_before_indent { to_indent } else { tab_advance(¶.tabs, 0.0, pen, sg).0 }; marker.push(tab); let n = marker.len(); sg.splice(0..0, marker); n } fn emit(&mut self, g: &SGlyph, pre: Affine, origin: Point, angle: f64, advance: f64, line: usize) { // Only the glyph right after an upright glyph can continue its cluster. let cluster = self.upright_cluster.take(); // Control characters (tabs) and soft hyphens draw nothing (fonts map them to .notdef). let blank = g.is_soft_hyphen() || g.ch.is_control() || g.ch == '\u{2028}'; // A colour glyph draws its art (COLR layers, an SVG document) or its image (a bitmap, or // COLR paints documents have no form for), not its outline; COLR, then SVG, then bitmaps. // Art in the text's colours takes the run's fill (black when it isn't a colour) and // stroke. let plain = g.inline.is_some() || blank; let colour_of = |p: &vectorcraft_doc::color::Paint| match p { vectorcraft_doc::color::Paint::Solid { color, .. } => Some(*color), _ => None, }; let st = self.style_at(g.byte); let foreground = colour_of(&st.fill).unwrap_or(vectorcraft_doc::color::Color::BLACK); let stroke = colour_of(&st.stroke).filter(|_| st.has_stroke()); let (colour, image) = match (!plain).then(|| self.db.glyph_colour(&g.face, g.gid, foreground, stroke)).flatten() { Some(crate::colr::ColourContent::Art(art)) => (Some(art), None), Some(crate::colr::ColourContent::Image(image)) => (None, Some(image)), None => (None, (!plain).then(|| self.db.glyph_image(&g.face, g.gid)).flatten()), }; let src = if g.inline.is_some() || image.is_some() || colour.is_some() { std::sync::Arc::new(BezPath::new()) } else { self.db.outline(&g.face, g.gid) }; // A glyph whose leading space was taken off (mojikumi) is drawn that much earlier: an // upright one in vertical type by moving it up the column once it stands upright. let upright = self.vertical && !self.on_path && g.tcy.is_none() && stands_upright(g); let lead = if upright { 0.0 } else { g.lead }; let local = match &g.inline { // Inline graphics: art space to glyph space (no font outline), moved along the line by // right-to-left manual kerning as a glyph is. Some(ib) => Affine::translate((g.kern_shift - lead, -g.bshift)) * ib.xf, // Baseline shift moves the glyph across its line, and Character Rotation then turns it // about its shifted pen position, so the shift is across the line at any rotation. // Manual kerning and composition trim move that pen position along the line. None => { Affine::translate((g.kern_shift - lead, -g.bshift)) * Affine::rotate(-g.rotation.to_radians()) * Affine::translate((g.dx, g.dy)) * Affine::scale_non_uniform(g.sx, g.sy) } }; // The turning point in font units, which later moves of the glyph's transform keep. let pivot = match local.determinant() { det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(g.kern_shift - lead, -g.bshift), _ => Point::ZERO, }; let unturned = if g.inline.is_none() && g.rotation != 0.0 && g.sx != 0.0 && g.sy != 0.0 { let placed = Affine::translate((g.dx, g.dy)) * Affine::scale_non_uniform(g.sx, g.sy); placed.inverse() * Affine::rotate(g.rotation.to_radians()) * placed } else { Affine::IDENTITY }; let mut m = pre * local; if self.vertical && self.on_path { // Vertical path type keeps the baseline path and turns each glyph across it. m = Affine::rotate_about(std::f64::consts::FRAC_PI_2, origin) * m; } else if let (true, Some(t)) = (self.vertical, g.tcy) { // Tate-chu-yoko: the block set across the column, centred on its em of it, then moved up // by its Up/Down and right by its Left/Right (line x runs down the column and line y // from right to left across it). let em = self.style_at(g.byte).size; let start = origin.x - t.pen; let centre = Point::new(start + em * 0.5, origin.y - g.face.ideographic_centre() * em); let across = Affine::translate((centre.x - t.width * 0.5 + t.ink - origin.x, 0.0)); let squeeze = Affine::translate((centre.x, 0.0)) * Affine::scale_non_uniform(t.squeeze, 1.0) * Affine::translate((-centre.x, 0.0)); let offset = Affine::translate((-t.up, -t.right)); m = offset * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, centre) * squeeze * across * m; } else if self.vertical && stands_upright(g) { let next = self.out.glyphs.len(); let after_base = self.out.glyphs.last().is_some_and(|p| p.byte == g.byte && p.line == line && p.len > 0); // Baseline shift moves it across the column at any Character Rotation: the shift is // taken off before the turn and put back after it. let shift = pre * Point::new(0.0, -g.bshift) - pre * Point::ZERO; let (shift, unshift) = (Affine::translate(shift), Affine::translate(-shift)); if let Some((_, _, turn, after)) = cluster.filter(|c| c.0 == g.byte && c.1 == next && after_base && g.len > 0) { // A later glyph of an upright glyph's cluster: set after it as the shaper placed // it, and turned with it. m = shift * turn * Affine::translate((after - origin.x, 0.0)) * unshift * m; self.upright_cluster = Some((g.byte, next + 1, turn, after)); } else { // Turned about the centre of its cell, which the column's centre line runs through: // the middle of its own cell, not of its advance (tracking and justification add // space after the cell, and must not push the glyph off the centre line). let em = self.style_at(g.byte).size; let cell = if takes_cell(g) { upright_cell(g) } else { advance }; // A leading space taken off moves it up the column after the turn (moving the // turning point instead would move it across the column too). let turn = Affine::translate((-g.lead, 0.0)) * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, Point::new(origin.x + cell * 0.5, origin.y - upright_centre(g, cell, em))); m = shift * turn * unshift * m; // Its own advance across, from where the cell centres it: where the shaper set the // next glyph of its cluster. let across = g.face.advance(g.gid) * g.sx; self.upright_cluster = (g.len > 0).then_some((g.byte, next + 1, turn, origin.x + (cell - across) * 0.5 + across)); } } let outline = if src.elements().is_empty() || blank { BezPath::new() } else { let mut p = BezPath::with_capacity(src.elements().len()); for el in src.elements() { p.push(m * *el); } p }; let font_id = g.face.id(); // Its own width along the baseline: its font advance through its size, scale and the glyph // scaling `pre` adds. In vertical type, an upright glyph's own length down the column is // its cell with the Proportional Metrics change to it, and a tate-chu-yoko glyph takes its // advance, as an inline graphic does. let upright = self.vertical && g.inline.is_none() && g.tcy.is_none() && takes_cell(g) && stands_upright(g); let width = if g.inline.is_some() || (self.vertical && g.tcy.is_some()) { advance } else if upright { let proportional = g.vpal.filter(|_| g.face.vertical_glyph(g.gid).is_some()).map_or(0.0, |a| a.advance); Some(upright_cell(g) + proportional).filter(|w| w.is_finite() && *w > 0.0).unwrap_or(advance) } else { let [a, b, ..] = pre.as_coeffs(); Some(g.face.advance(g.gid) * g.sx * a.hypot(b)).filter(|w| w.is_finite()).unwrap_or(advance) }; // Where its own width starts along its advance: the shaper's placement, a leading space // taken off and right-to-left manual kerning move the glyph along the line. An upright or // tate-chu-yoko glyph in vertical type starts its cell at the start of its advance. let width_start = if g.inline.is_none() && !upright && !(self.vertical && g.tcy.is_some()) { let [a, b, ..] = pre.as_coeffs(); Some((g.kern_shift + g.dx - g.lead) * a.hypot(b)).filter(|s| s.is_finite()).unwrap_or(0.0) } else { 0.0 }; // On a path, an upright glyph turns about its pen: its cell runs along the path between // where the glyph's transform sets the cell's foot and its top, moved by the shaper's // placement and a baseline shift, and slanted by the path's effect. let (width, width_start) = match (upright && self.on_path, g.sy) { (true, sy) if sy != 0.0 => { // In font units, with the Proportional Metrics change to its length. let (cell, top) = upright_extent(g); let proportional = g.vpal.filter(|_| g.face.vertical_glyph(g.gid).is_some()).map_or(0.0, |a| a.advance) / sy; let tangent = Vec2::new(angle.cos(), angle.sin()); let along = |y: f64| (m * Point::new(0.0, y) - origin).dot(tangent); let (start, end) = (along(cell + proportional - top), along(-top)); if start.is_finite() && end > start { (end - start, start) } else { (width, width_start) } } _ => (width, width_start), }; if let Some(art) = g.inline.as_ref().and_then(|ib| ib.art) { self.out.inlines.push(InlineGlyph { run: g.run, byte: g.byte, glyph: self.out.glyphs.len(), xf: m, bounds: m.transform_rect_bbox(art) }); } self.out.glyphs.push(PositionedGlyph { outline, run: g.run, byte: g.byte, origin, advance, len: g.len, angle, line, font_id, gid: g.gid, xf: m, // Vertical type keeps its logical order down the column. rtl: g.level.is_rtl() && !self.vertical, scale: 1.0, image, colour, bend: None, unturned, pivot, carets: if g.inline.is_some() { Vec::new() } else { ligature_carets(self.text, g, g.level.is_rtl() && !self.vertical, upright) }, width, width_start, }); } /// Draw a kashida `len` points long from `at` (the pen on the baseline) on line `line`, as /// tatweels in the face, size and style of glyph `g`. Short, Medium and Long kashidas are one /// tatweel stretched along the baseline; a Stylistic one is tatweels side by side. Each tatweel /// is a glyph at `g`'s byte offset that stands for no character (length 0) and does not /// advance: `g`'s advance takes the kashida in. fn emit_kashida(&mut self, g: &SGlyph, kind: Kashida, len: f64, at: Point, line: usize) { let Some((gid, t)) = kashida::tatweel(g) else { return }; let mut k = g.clone(); k.gid = gid; k.adv = t; k.dx = 0.0; k.dy = 0.0; k.kern_shift = 0.0; k.len = 0; k.lead = 0.0; k.ch = kashida::TATWEEL; k.inline = None; k.tcy = None; if kind == Kashida::Stylistic { let n = (len / t).round().clamp(1.0, kashida::MAX_TATWEELS) as usize; for m in 0..n { let x = at.x + m as f64 * t; self.emit(&k, Affine::translate((x, at.y)), Point::new(x, at.y), 0.0, 0.0, line); } return; } self.emit(&k, Affine::translate((at.x, at.y)) * Affine::scale_non_uniform(len / t, 1.0), at, 0.0, 0.0, line); } } #[cfg(test)] mod wrapping_tests;