diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 7dadaf1bd5..d31b888fad 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -17,36 +17,33 @@ use std::borrow::Cow; use std::ops::Range; -use parley::{ - Alignment, AlignmentOptions, FontData, FontFamily, FontFeatures, FontStyle, FontVariations, FontWeight, IndentOptions, Layout, LayoutContext, - PositionedLayoutItem, StyleProperty, -}; +use parley::{FontData, FontFamily, FontFeatures, FontStyle, FontVariations, FontWeight, Layout, LayoutContext, PositionedLayoutItem, StyleProperty}; use photocraft_doc::TextLayer; -use photocraft_doc::text::{Caps, CharStyle, FontBaseline, Kerning, Orientation, ParagraphStyle, TextAlign, TextDirection, TextShape}; +use photocraft_doc::text::{Caps, CharStyle, FontBaseline, Kerning, Orientation, ParagraphStyle, TextShape}; use skrifa::raw::types::Tag; use crate::fonts::FontDb; mod area; +mod lines; mod navigate; mod paragraphs; +mod shaping; mod style; mod vertical; #[cfg(test)] pub(crate) use area::AREA_PASSES; -use area::{LineMetrics, MAX_AREA_PASSES, area_metrics, break_area, line_metrics}; pub use navigate::{byte_index, char_index, hit_char, line_edge, line_index, line_step, text_point_inside, word_boundary}; +use paragraphs::next_tab_stop; pub use paragraphs::{DEFAULT_TAB_STOP_PT, split_paragraphs}; -use paragraphs::{next_tab_stop, strip_break, tab_widths}; +use shaping::{Para, resolve_run_styles, running_starts, small_caps_modes}; +use style::SmallCapsMode; +#[cfg(test)] +use style::{SMCP_PROBES, feature_list, small_caps_mode, variation_list}; #[cfg(test)] -use style::{SMCP_PROBES, variation_list}; -use style::{ - SYNTHETIC_SMALL_CAPS_SCALE, SmallCapsMode, drawn_style, effective_horizontal_scale, feature_list, first_ascent, probe_smcp, small_caps_mode, style_props, -}; pub(crate) use style::{hangul_runs, squeeze_punctuation, synthetic_bold}; pub use vertical::{VClass, vertical_class}; -use vertical::{VGlyph, VMetrics}; /// Index of the run containing byte offset `off`, given each run's start offset (non-decreasing): /// the last run starting at or before `off`, or 0 when none does. Binary search, so a layer with @@ -270,29 +267,6 @@ pub(crate) struct Layouter { optical: crate::optical::Cache, } -/// A cluster of a line in visual order, for kerning. -struct KernSlot { - /// Index of the run in the paragraph layout. - run: usize, - /// Byte offset in the paragraph text. - start: usize, - first: Option, - last: Option, - font: FontData, - coords: Vec, - size: f32, - rtl: bool, - blank: bool, - /// Advance along the line (px, horizontal scale included). - advance: f32, - /// What horizontal scale adds to the shaped advance (px; negative below 100%). - growth: f32, - /// A tab, shaped as a space; it advances to the next tab stop. - tab: bool, - /// Vertical type, upright glyph (its outline doesn't run along the column). - upright: bool, -} - impl Layouter { pub fn new() -> Self { Self { lcx: LayoutContext::new(), optical: crate::optical::Cache::default() } @@ -304,48 +278,9 @@ impl Layouter { let paras = t.paragraph_runs(); let vertical = t.orientation == Orientation::Vertical; let mut out = TextLayout { px_per_pt: k, vertical, ..Default::default() }; - // Resolve families (PostScript names from PSDs, unknown families). - for r in &runs { - let mut s = r.style.clone(); - if let Some(ps) = s.postscript_name.clone() - && !fonts.faces(&s.font_family).iter().any(|f| f.postscript_name.as_deref() == Some(&ps)) - { - let f = fonts.resolve_postscript(&ps); - // An exact face match wins; a guessed family only fills a missing family. - if f.exact || (!fonts.has_family(&s.font_family) && fonts.has_family(&f.family)) { - s.font_family = f.family; - s.weight = f.weight; - s.italic = f.italic; - } - } - // Drawn with a fallback for now; a host that serves this family fetches it (`served`). - if !fonts.has_family(&s.font_family) { - crate::served::request(&s.font_family); - } - fonts.select_named_face(&mut s); - drawn_style(&mut s); - out.styles.push(s); - } - let run_starts: Vec = runs - .iter() - .scan(0, |a, r| { - let s = *a; - *a += r.len; - Some(s) - }) - .collect(); - let style_at = |off: usize| run_index_at(&run_starts, off); - let para_starts: Vec = paras - .iter() - .scan(0, |a, r| { - let s = *a; - *a += r.len; - Some(s) - }) - .collect(); - let para_style_at = |off: usize| ¶s[para_starts.iter().rposition(|&s| s <= off).unwrap_or(0)].style; - - let text = &t.text; + let styles = resolve_run_styles(fonts, &runs); + let run_starts = running_starts(runs.iter().map(|r| r.len)); + let para_starts = running_starts(paras.iter().map(|r| r.len)); let (box_rect, is_box) = match t.shape { TextShape::Box { x, y, width, height } => ((x, y, width, height), true), // Path text is laid out as one point-text line, then bent onto the path. Area text @@ -358,591 +293,93 @@ impl Layouter { TextShape::Area { path } => crate::area::AreaShape::new(path, vertical), _ => None, }; - // Box and area text wrap, align within their lines and hide what doesn't fit. - let wraps = is_box || area.is_some(); - let mut prev_baseline: Option = None; - let mut pending_space = 0.0f32; - let mut stop = false; - // Fixed for the whole layout, and the font probe only matters for styles that use small - // caps. A real `smcp` substitution must receive lowercase source text; only the fallback - // path uppercases it and scales it down. - let small_caps: Vec = out - .styles - .iter() - .map(|style| match style.caps { - Caps::SmallCaps => small_caps_mode(style.caps, probe_smcp(fonts, style)), - _ => SmallCapsMode::None, - }) - .collect(); - for (pi, prange) in split_paragraphs(text).into_iter().enumerate() { - if stop { + let small_caps = small_caps_modes(fonts, &styles); + let env = Env { + text: &t.text, + runs: &runs, + styles: &styles, + run_starts: &run_starts, + small_caps: &small_caps, + k, + vertical, + is_box, + // Box and area text wrap, align within their lines and hide what doesn't fit. + wraps: is_box || area.is_some(), + box_rect, + area, + }; + let mut flow = Flow::default(); + for (pi, prange) in split_paragraphs(&t.text).into_iter().enumerate() { + if flow.stop { break; } - let ps = para_style_at(prange.start).clone(); - let content_end = strip_break(text, &prange); - let content = &text[prange.start..content_end]; - let prefix = match ps.direction { - TextDirection::Auto => "", - TextDirection::Ltr => LRM, - TextDirection::Rtl => RLM, - }; - let mut ptext = String::with_capacity(prefix.len() + content.len()); - ptext.push_str(prefix); - // Ranges in `ptext` for lowercase characters rendered as synthetic small caps. - // They have the same UTF-8 length as their uppercase form, so the layer's byte-based - // run and caret offsets remain unchanged. - let mut synthetic_small_caps: Vec<(Range, f32)> = Vec::new(); - // Offsets in `ptext` of the spaces standing in for tabs. - let mut tabs: Vec = Vec::new(); - let tab_px = DEFAULT_TAB_STOP_PT * k; - for (i, ch) in content.char_indices() { - // A forced line break ends the line but not the paragraph. The line breaker knows - // it as a newline, which has the same length, so text offsets don't move. - if ch == FORCED_LINE_BREAK { - ptext.push('\n'); - continue; - } - // Imported PSD text can contain literal tab controls. Font shaping may - // render those as .notdef boxes; a space preserves the one-byte source - // and style/caret offsets while supplying a real whitespace advance. The space - // is widened to its tab stop after line breaking (`tab_px` below). - if ch == '\t' { - tabs.push(ptext.len()); - ptext.push(' '); - continue; - } - let style = &out.styles[style_at(prange.start + i)]; - let caps = style.caps; - if caps == Caps::AllCaps || small_caps[style_at(prange.start + i)] == SmallCapsMode::Synthetic { - let up: String = ch.to_uppercase().collect(); - if up.len() == ch.len_utf8() { - ptext.push_str(&up); - if caps == Caps::SmallCaps && ch.is_lowercase() { - let end = ptext.len(); - synthetic_small_caps.push((end - up.len()..end, style.size_pt * k * SYNTHETIC_SMALL_CAPS_SCALE)); - } - continue; - } - } - ptext.push(ch); - } - let first_style = &out.styles[style_at(prange.start)]; - let first_px = first_style.size_pt * k; - // Scripts the loaded fonts may lack (Arabic, Japanese, …): a host serving a font for them - // fetches it (`served`); it joins the fallback stack once it arrives. - crate::served::request_for_text(&ptext); - let fallback: Vec = fonts.fallback_stack().map(str::to_string).collect(); - let si0 = style_at(prange.start); - let si_end = run_starts.partition_point(|&rs| rs < content_end).max(si0 + 1).min(out.styles.len()); - // Shapes the paragraph, each tab widened by its letter spacing in `tab_spacing` (px). - let build = |lcx: &mut LayoutContext, fcx: &mut parley::FontContext, tab_spacing: &[f32]| -> Layout { - let mut b = lcx.ranged_builder(fcx, &ptext, 1.0, false); - // Paragraph-start style as the default (covers the direction mark and empty - // paragraphs), then every run piece intersecting this paragraph. - for p in style_props(&out.styles[si0], k, &fallback, si0 as u32, small_caps[si0]) { - b.push_default(p); - } - // Paragraph text breaks a word wider than the box at the box edge, as Photoshop - // does (#6694): parley's default only breaks at UAX #14 opportunities and lets the - // word run out of the box. Point text has no width to overflow. - if is_box { - b.push_default(StyleProperty::OverflowWrap(parley::style::OverflowWrap::BreakWord)); - } - for (ri, st) in out.styles.iter().enumerate().take(si_end).skip(si0) { - let rs = run_starts[ri]; - let (a, z) = (rs.max(prange.start), (rs + runs[ri].len).min(content_end)); - if a >= z { - continue; - } - let range = (a - prange.start + prefix.len())..(z - prange.start + prefix.len()); - for p in style_props(st, k, &fallback, ri as u32, small_caps[ri]) { - b.push(p, range.clone()); - } - if vertical { - // Upright pieces get the vertical alternates and their class in the brush. - let piece = ptext.get(range.clone()).unwrap_or(""); - let mut seg: Option<(usize, VClass)> = None; - let flush = |b: &mut parley::RangedBuilder<'_, RunBrush>, from: usize, to: usize, cls: VClass| { - if cls == VClass::Rotate || from >= to { - return; - } - let mut feats = feature_list(st, small_caps[ri]); - feats.push("\"vert\" 1".into()); - let r = (range.start + from)..(range.start + to); - b.push(StyleProperty::FontFeatures(FontFeatures::Source(Cow::Owned(feats.join(", ")))), r.clone()); - b.push(StyleProperty::Brush(RunBrush(ri as u32, cls as u8)), r); - }; - for (i, ch) in piece.char_indices() { - let cls = vertical_class(ch); - match seg { - Some((_, c)) if c == cls => {} - Some((from, c)) => { - flush(&mut b, from, i, c); - seg = Some((i, cls)); - } - None => seg = Some((i, cls)), - } - } - if let Some((from, c)) = seg { - flush(&mut b, from, piece.len(), c); - } - } - } - for (range, size) in &synthetic_small_caps { - b.push(StyleProperty::FontSize(*size), range.clone()); - } - // Korean breaks lines at spaces, between words, as Photoshop sets it; Unicode's - // default rules break between any two Hangul syllables, mid-word (#5959). - for range in hangul_runs(&ptext) { - b.push(StyleProperty::WordBreak(parley::WordBreak::KeepAll), range); - } - for (&at, &spacing) in tabs.iter().zip(tab_spacing) { - b.push(StyleProperty::LetterSpacing(spacing), at..at + 1); - } - b.build(&ptext) - }; - let indent_start = ps.start_indent_pt * k; - let indent_end = ps.end_indent_pt * k; - let first_indent = ps.first_line_indent_pt * k; - // Box extent along the lines: width, or height for vertical type (columns). - let (line_origin, line_len) = if vertical { (box_rect.1, box_rect.3) } else { (box_rect.0, box_rect.2) }; - let avail = if is_box { Some((line_len - indent_start - indent_end).max(1.0)) } else { None }; - // Horizontal scale is applied after shaping, so paragraph text breaks at the box - // (or area span) width divided by it (#5565). Mixed scales break at the largest, so - // no line overflows the box. - let break_scale = if wraps { - (si0..si_end) - .filter(|&ri| { - let rs = run_starts.get(ri).copied().unwrap_or(0); - let end = rs.saturating_add(runs.get(ri).map_or(0, |r| r.len)); - ri == si0 || (rs.max(prange.start) < end.min(content_end)) - }) - .filter_map(|ri| out.styles.get(ri)) - .map(effective_horizontal_scale) - .fold(0.0f32, f32::max) - } else { - 1.0 - }; - let break_scale = if break_scale > 0.0 { break_scale } else { 1.0 }; - // Area type: the first row goes no higher than one leading (plus paragraph spacing) - // below the previous paragraph's last line. - let space_before = if prev_baseline.is_some() { ps.space_before_pt * k } else { 0.0 }; - let area_from = |m: LineMetrics| match (&area, prev_baseline) { - (Some(_), Some(b)) => b + m.leading + pending_space + space_before, - (Some(a), None) => a.bounds[1] + m.ascent, - (None, _) => 0.0, - }; - // Line-breaking passes left for this paragraph's area type (see `break_area`). - let area_budget = std::cell::Cell::new(MAX_AREA_PASSES); - let break_lines = |layout: &mut Layout| -> (Vec, bool) { - if first_indent != 0.0 { - layout.set_text_indent(first_indent / break_scale, IndentOptions::default()); - } - let Some(a) = &area else { - layout.break_all_lines(avail.map(|a| a / break_scale)); - return (Vec::new(), false); - }; - // Plan every row with the paragraph's real line metrics (its tallest font and - // largest leading, measured on the unwrapped paragraph), so no wrapped line comes - // out taller than the row it was planned in and no second plan is needed. - layout.break_all_lines(None); - let m = area_metrics(layout, &out.styles, &ps, k, first_style, vertical); - break_area(layout, a, area_from(m), m, (indent_start, indent_end), first_px, break_scale, &area_budget) - }; - // Paragraph text: a tab is never wider than a space plus one tab interval, so breaking - // lines at that width keeps the expanded tabs inside the box, but can wrap early. - // Break again with the tab widths that layout gives until they agree; if they don't - // settle, keep the safe layout. - let wide = if wraps { tab_px } else { 0.0 }; - let mut layout = build(&mut self.lcx, &mut fonts.fcx, &vec![wide; tabs.len()]); - let (mut area_slots, mut area_full) = break_lines(&mut layout); - if wraps && !tabs.is_empty() { - let first = tab_widths(&layout, &tabs, &out.styles, indent_start, first_indent, tab_px); - let mut want: Vec = first.iter().map(|t| t.width).collect(); - for _ in 0..4 { - let spacing: Vec = first.iter().zip(&want).map(|(t, w)| w / t.scale - (t.advance - tab_px)).collect(); - let mut next = build(&mut self.lcx, &mut fonts.fcx, &spacing); - let (next_slots, next_full) = break_lines(&mut next); - let got: Vec = tab_widths(&next, &tabs, &out.styles, indent_start, first_indent, tab_px).iter().map(|t| t.width).collect(); - if got.iter().zip(&want).all(|(a, b)| (a - b).abs() < 0.01) { - layout = next; - area_slots = next_slots; - area_full = next_full; - break; - } - want = got; - } - } - let alignment = if wraps { - match ps.align { - TextAlign::Left => Alignment::Left, - TextAlign::Center => Alignment::Center, - TextAlign::Right => Alignment::Right, - _ => Alignment::Justify, - } - } else { - Alignment::Left - }; - layout.align(alignment, AlignmentOptions { align_when_overflowing: !wraps }); - - // Stack lines. - if prev_baseline.is_some() { - pending_space += ps.space_before_pt * k; - } - let nlines = layout.len(); - for (li, line) in layout.lines().enumerate() { - // Area type: the slot the breaker gave this line. - let slot = area_slots.get(li).copied(); - if area.is_some() && slot.is_none() { - stop = true; - break; - } - let m = *line.metrics(); - let LineMetrics { ascent, descent, leading } = line_metrics(&line, &out.styles, &ps, k, first_style, vertical); - let baseline = match (slot, prev_baseline) { - (Some(s), _) => s.baseline, - // The first column's right edge touches the box's right edge. - // (In vertical type the ascent is half the column width.) - (None, None) if is_box && vertical => -(box_rect.0 + box_rect.2) + ascent, - // Photoshop's "first baseline: ascent": the top of the tallest ascender - // (height of 'd') touches the box top, not the font's hhea ascent. - (None, None) if is_box => box_rect.1 + first_ascent(&line).unwrap_or(ascent), - (None, None) => 0.0, - (None, Some(b)) => b + leading + pending_space, - }; - pending_space = 0.0; - // Photoshop keeps a horizontal line whose baseline is inside the box and lets - // its descenders hang below the bottom edge (#1317: three 35 pt lines in a - // 91.3 pt box, the last baseline at 88). Requiring the descent to fit dropped - // that line. - let overflow = if vertical { baseline + descent > -box_rect.0 + 0.5 } else { baseline > box_rect.1 + box_rect.3 + 0.5 }; - if is_box && overflow { - stop = true; - break; - } - prev_baseline = Some(baseline); - let last_line = li + 1 == nlines; - let adv = m.advance - m.trailing_whitespace; - let map = |o: usize| (prange.start + o.saturating_sub(prefix.len())).min(content_end); - - // Kerning after each cluster (px), per run in visual cluster order: manual - // kerning plus optical pair kerning. Applied after shaping (like horizontal - // scale) in line space, so it moves the following glyphs, carets and the line - // extent; in vertical type that is along the column. Optical kerning measures - // horizontal outlines, so it applies to rotated (Latin) glyphs only there. - let mut slots: Vec = Vec::new(); - let mut line_runs: Vec = Vec::new(); - for item in line.items() { - let PositionedLayoutItem::GlyphRun(gr) = item else { - continue; - }; - let run = gr.run(); - if line_runs.contains(&run.index()) { - continue; - } - line_runs.push(run.index()); - let hs = out.styles.get(gr.style().brush.0 as usize).map_or(1.0, |st| if st.horizontal_scale > 0.0 { st.horizontal_scale } else { 1.0 }); - for c in run.visual_clusters() { - let mut gl = c.glyphs(); - let first = gl.next().map(|g| g.id); - let last = gl.last().map(|g| g.id).or(first); - slots.push(KernSlot { - run: run.index(), - start: c.text_range().start, - first, - last, - font: run.font().clone(), - coords: run.normalized_coords().to_vec(), - size: run.font_size(), - rtl: c.is_rtl(), - blank: first.is_none() || c.is_space_or_nbsp() || c.text_range().end <= prefix.len(), - upright: vertical && c.first_style().brush.1 != VClass::Rotate as u8, - advance: c.advance() * hs, - growth: c.advance() * (hs - 1.0), - tab: tabs.binary_search(&c.text_range().start).is_ok(), - }); - } - } - let mut kern_px: Vec = vec![0.0; slots.len()]; - for j in 0..slots.len().saturating_sub(1) { - let (a, b) = (&slots[j], &slots[j + 1]); - if a.start < prefix.len() { - continue; - } - let st = &out.styles[style_at(map(a.start))]; - let next = &out.styles[style_at(map(b.start))]; - let mut units = if st.kern.is_finite() { st.kern } else { 0.0 }; - // Optical pairs: both characters optical (a mode change splits shaping - // runs, which ends automatic kerning, as with Metrics). - if st.kerning == Kerning::Optical - && next.kerning == Kerning::Optical - && st.kern == 0.0 - && next.kern == 0.0 - && !a.rtl - && !b.rtl - && !a.blank - && !b.blank - && !a.upright - && !b.upright - && a.font.data.id() == b.font.data.id() - && a.font.index == b.font.index - && a.coords == b.coords - && (a.size - b.size).abs() < 1e-3 - && let (Some(l), Some(r)) = (a.last, b.first) - { - let coords: Vec = - a.coords.iter().map(|&c| skrifa::instance::NormalizedCoord::from_bits(c)).collect(); - if let Some(k) = self.optical.pair(a.font.data.id(), a.font.data.as_ref(), a.font.index, &coords, l, r) { - units += k + crate::optical::size_adjust(st.size_pt); - } - } - kern_px[j] = units / 1000.0 * a.size; - } - // Tabs: the "kerning" after a tab takes the pen to the next stop, measured from - // the anchor or the box edge as if the line were left-aligned (alignment then - // moves the whole line). - let mut pos = indent_start + if li == 0 { first_indent } else { 0.0 }; - let mut trailing_tabs = 0.0f32; - let mut trailing_growth = 0.0f32; - for (j, s) in slots.iter().enumerate() { - // Trailing whitespace is outside the line's extent (`adv`). - let trailing = s.blank && slots[j + 1..].iter().all(|n| n.blank); - if trailing { - trailing_growth += s.growth; - } - if s.tab { - kern_px[j] = next_tab_stop(pos, tab_px) - pos - s.advance; - if slots[j + 1..].iter().all(|n| n.blank) { - trailing_tabs += kern_px[j]; - } - } - pos += s.advance + kern_px.get(j).copied().unwrap_or(0.0); - } - let line_kern: f32 = kern_px.iter().sum::() - trailing_tabs; - // Per run on the line: (run index, glyph → slot, glyphs emitted, first slot whose - // kerning isn't applied yet). - let mut cursors: Vec<(usize, Vec, usize, usize)> = Vec::new(); - for item in line.items() { - let PositionedLayoutItem::GlyphRun(gr) = item else { - continue; - }; - let run = gr.run(); - if cursors.iter().any(|c| c.0 == run.index()) { - continue; - } - let base = slots.iter().position(|s| s.run == run.index()).unwrap_or(0); - let mut g2s = Vec::new(); - for (ci, c) in run.visual_clusters().enumerate() { - g2s.extend(c.glyphs().map(|_| base + ci)); - } - cursors.push((run.index(), g2s, 0, base)); - } - // Parley aligned box lines without the kerning. - let kern_align = if wraps { - match ps.align { - TextAlign::Center => line_kern / 2.0, - TextAlign::Right => line_kern, - _ => 0.0, - } - } else { - 0.0 - }; - // Box and area lines start at their box or span edge; the line's own width - // (`line_avail`) takes the justify-last-line slack. - let line_avail = slot.map(|s| s.width).or(avail); - let dx = if wraps { - let origin = slot.map_or(line_origin, |s| s.x); - let base = origin + indent_start - kern_align; - let slack = line_avail.unwrap_or(0.0) - adv * break_scale - line_kern; - // Parley aligned the unscaled line in `avail / break_scale`: scale its - // indent and alignment offset back to the box. - let scaled_offset = m.offset * (break_scale - 1.0); - base + if last_line { - match ps.align { - TextAlign::JustifyCenter => slack * 0.5 - m.offset, - TextAlign::JustifyRight => slack - m.offset, - _ => scaled_offset, - } - } else { - scaled_offset - } - } else { - let target = match ps.align { - TextAlign::Center | TextAlign::JustifyCenter => -(adv + line_kern) / 2.0, - TextAlign::Right | TextAlign::JustifyRight => -(adv + line_kern), - _ => indent_start, - }; - target - m.offset - }; - let justify_all = wraps && last_line && ps.align == TextAlign::JustifyAll; - let line_index = out.lines.len(); - let lr = line.text_range(); - let g0 = out.glyphs.len(); - let c0 = out.clusters.len(); - let mut vinfo: Vec = Vec::new(); - let mut extra = 0.0f32; // horizontal-scale growth and kerning along the line - let mut seen_runs: Vec = Vec::new(); - for item in line.items() { - let PositionedLayoutItem::GlyphRun(gr) = item else { - continue; - }; - let run = gr.run(); - let si = gr.style().brush.0; - let st = &out.styles[si as usize]; - let hs = if st.horizontal_scale > 0.0 { st.horizontal_scale } else { 1.0 }; - let synth = run.synthesis(); - let face = out.faces.len() as u32; - out.faces.push(GlyphFace { - font: run.font().clone(), - coords: run.normalized_coords().to_vec(), - size_px: run.font_size(), - embolden: synthetic_bold(synth.embolden(), st.weight, run.font_attrs().weight.value()), - skew_deg: synth.skew().unwrap_or(0.0), - }); - let run_x0 = dx + gr.offset() + extra; - if !seen_runs.contains(&run.index()) { - seen_runs.push(run.index()); - let mut cx = run_x0; - let base = slots.iter().position(|s| s.run == run.index()).unwrap_or(0); - for (ci, c) in run.visual_clusters().enumerate() { - let a = c.advance() * hs + kern_px.get(base + ci).copied().unwrap_or(0.0); - let r = c.text_range(); - if r.end > prefix.len() || prefix.is_empty() { - out.clusters.push(ClusterInfo { range: map(r.start)..map(r.end), x: cx, advance: a, line: line_index, rtl: c.is_rtl() }); - } - cx += a; - } - } - let rm = run.metrics(); - let vm = if vertical { Some(VMetrics::new(run.font(), run.normalized_coords(), run.font_size())) } else { None }; - let class = gr.style().brush.1; - // Glyphs of the run's own characters (not substituted by `vert`). - let plain: Vec = match (&vm, class) { - (Some(v), 2) => ptext.get(run.text_range()).unwrap_or("").chars().filter_map(|c| v.cmap(c)).collect(), - _ => Vec::new(), - }; - let mut pen = gr.offset(); - let mut cursor = cursors.iter_mut().find(|c| c.0 == run.index()); - for g in gr.glyphs() { - // Kerning of the clusters before this glyph's cluster. - if let Some(c) = cursor.as_deref_mut() { - if let Some(&slot) = c.1.get(c.2) { - while c.3 < slot { - extra += kern_px.get(c.3).copied().unwrap_or(0.0); - c.3 += 1; - } - } - c.2 += 1; - } - out.glyphs.push(PlacedGlyph { - face, - id: g.id, - x: dx + pen + g.x + extra, - y: baseline + g.y, - style: si, - orient: GlyphOrient::Horizontal, - }); - if let Some(v) = &vm { - let upright = class == 1 || (class == 2 && !plain.contains(&g.id)); - vinfo.push(VGlyph { - upright, - advance: g.advance * hs, - origin: if upright { v.origin(g.id) } else { 0.0 }, - // Rotated text is centred on the column: its baseline sits - // (ascent − descent) / 2 from the centre line. - centre: (rm.ascent - rm.descent) * 0.5, - }); - } - extra += g.advance * (hs - 1.0); - pen += g.advance; - } - // The run's last glyph: its remaining clusters' kerning follows it. - if let Some(c) = cursor - && c.2 >= c.1.len() - { - let end = slots.iter().rposition(|s| s.run == c.0).map_or(c.3, |p| p + 1); - while c.3 < end { - extra += kern_px.get(c.3).copied().unwrap_or(0.0); - c.3 += 1; - } - } - let run_x1 = dx + gr.offset() + gr.advance() + extra; - let shift = st.baseline_shift_pt * k; - if vertical { - // Underline to the right of the column, strikethrough through its centre - // (stored directly in text space). - let half = run.font_size() * 0.5; - let mut push = |v0: f32, v1: f32| out.decorations.push(DecorationRect { x0: -v1, y0: run_x0, x1: -v0, y1: run_x1, style: si }); - if gr.style().underline.is_some() { - let v1 = baseline - half; - push(v1 - rm.underline_size.max(1.0), v1); - } - if gr.style().strikethrough.is_some() { - let sz = rm.strikethrough_size.max(1.0); - push(baseline - sz * 0.5, baseline + sz * 0.5); - } - } else if gr.style().underline.is_some() { - let y0 = baseline - rm.underline_offset - shift; - out.decorations.push(DecorationRect { x0: run_x0, y0, x1: run_x1, y1: y0 + rm.underline_size.max(1.0), style: si }); - } - if !vertical && gr.style().strikethrough.is_some() { - let y0 = baseline - rm.strikethrough_offset - shift; - out.decorations.push(DecorationRect { x0: run_x0, y0, x1: run_x1, y1: y0 + rm.strikethrough_size.max(1.0), style: si }); - } - } - if vertical { - extra -= squeeze_punctuation(text, &mut out.clusters[c0..], &mut out.glyphs[g0..]); - } - if justify_all { - let slack = line_avail.unwrap_or(0.0) - (adv + extra); - let n = out.clusters.len() - c0; - if n > 1 && slack > 0.0 { - let step = slack / (n - 1) as f32; - let starts: Vec = out.clusters[c0..].iter().map(|c| c.x).collect(); - for (i, c) in out.clusters[c0..].iter_mut().enumerate() { - c.x += step * i as f32; - } - for g in &mut out.glyphs[g0..] { - let i = starts.iter().rposition(|&s| s <= g.x + 1e-3).unwrap_or(0); - g.x += step * i as f32; - } - extra += slack; - } - } - if vertical { - // Line space → text space (90° clockwise); upright glyphs centred on the column. - for (g, v) in out.glyphs[g0..].iter_mut().zip(&vinfo) { - let (u, cross) = (g.x, g.y); - if v.upright { - g.x = -baseline - v.advance * 0.5; - g.y = u + v.origin; - g.orient = GlyphOrient::Upright; - } else { - g.x = -(cross + v.centre); - g.y = u; - g.orient = GlyphOrient::Rotated; - } - } - } - let x0 = dx + m.offset; - out.lines.push(LineInfo { - range: map(lr.start)..map(lr.end).min(content_end), - baseline, - x0, - x1: x0 + adv + extra - trailing_tabs - trailing_growth, - ascent, - descent, - paragraph: pi, - }); - } - if area_full { - // The shape is full: later paragraphs are hidden too. - stop = true; - } - pending_space += ps.space_after_pt * k; + let ps = paras[para_starts.iter().rposition(|&s| s <= prange.start).unwrap_or(0)].style.clone(); + self.layout_paragraph(fonts, &mut out, &mut flow, Para::new(&env, fonts, pi, prange, ps)); } + out.styles = styles; out } + + /// Breaks one paragraph and stacks its lines below the previous paragraph's. + fn layout_paragraph(&mut self, fonts: &mut FontDb, out: &mut TextLayout, flow: &mut Flow, para: Para<'_>) { + let env = para.env; + let (mut layout, area_slots, area_full) = para.break_paragraph(&mut self.lcx, &mut fonts.fcx, flow); + layout.align(para.alignment(), para.align_options()); + if flow.prev_baseline.is_some() { + flow.pending_space += para.ps.space_before_pt * env.k; + } + let nlines = layout.len(); + for (li, line) in layout.lines().enumerate() { + // Area type: the slot the breaker gave this line. + let slot = area_slots.get(li).copied(); + if env.area.is_some() && slot.is_none() { + flow.stop = true; + break; + } + let Some(frame) = para.frame_line(&line, li, nlines, slot, flow) else { + flow.stop = true; + break; + }; + para.place_line(&mut self.optical, out, &line, &frame); + } + if area_full { + // The shape is full: later paragraphs are hidden too. + flow.stop = true; + } + flow.pending_space += para.ps.space_after_pt * env.k; + } +} + +/// What a layout run shares between its paragraphs. +struct Env<'a> { + text: &'a str, + runs: &'a [photocraft_doc::text::TextRun], + styles: &'a [CharStyle], + /// Start offset of each run. + run_starts: &'a [usize], + small_caps: &'a [SmallCapsMode], + k: f32, + vertical: bool, + is_box: bool, + /// Box and area text wrap, align within their lines and hide what doesn't fit. + wraps: bool, + box_rect: (f32, f32, f32, f32), + area: Option, +} + +impl Env<'_> { + /// Index of the run (and style) containing byte offset `off`. + fn style_at(&self, off: usize) -> usize { + run_index_at(self.run_starts, off) + } +} + +/// Where the previous paragraph left off. +#[derive(Default)] +struct Flow { + prev_baseline: Option, + pending_space: f32, + stop: bool, } /// Where one line of area type goes: its baseline, the left edge of its span, and the width diff --git a/crates/text/src/layout/area.rs b/crates/text/src/layout/area.rs index d91d497e26..f6dbe9cf69 100644 --- a/crates/text/src/layout/area.rs +++ b/crates/text/src/layout/area.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CharStyle, Layout, ParagraphStyle, PositionedLayoutItem, RunBrush, Slot}; /// A line's ascent, descent and leading (px); for area type, the largest over a paragraph. #[derive(Clone, Copy, Debug)] diff --git a/crates/text/src/layout/lines.rs b/crates/text/src/layout/lines.rs new file mode 100644 index 0000000000..ec42eba987 --- /dev/null +++ b/crates/text/src/layout/lines.rs @@ -0,0 +1,522 @@ +//! Placing one broken line of a paragraph: kerning and tab stops, glyph and cluster positions, +//! decorations, and the line's vertical-type and justification adjustments. + +use parley::{PositionedLayoutItem, Run}; +use photocraft_doc::text::{CharStyle, Kerning, TextAlign}; +use skrifa::instance::NormalizedCoord; + +use super::area::{LineMetrics, line_metrics}; +use super::shaping::Para; +use super::style::{first_ascent, squeeze_punctuation, synthetic_bold}; +use super::vertical::{VGlyph, VMetrics}; +use super::{ClusterInfo, DecorationRect, Flow, GlyphFace, GlyphOrient, LineInfo, PlacedGlyph, RunBrush, Slot, TextLayout, VClass}; + +type Line<'a> = parley::Line<'a, RunBrush>; +type GlyphRun<'a> = parley::GlyphRun<'a, RunBrush>; + +/// A cluster of a line in visual order, for kerning. +struct KernSlot { + /// Index of the run in the paragraph layout. + run: usize, + /// Byte offset in the paragraph text. + start: usize, + first: Option, + last: Option, + font: parley::FontData, + coords: Vec, + size: f32, + rtl: bool, + blank: bool, + /// Advance along the line (px, horizontal scale included). + advance: f32, + /// What horizontal scale adds to the shaped advance (px; negative below 100%). + growth: f32, + /// A tab, shaped as a space; it advances to the next tab stop. + tab: bool, + /// Vertical type, upright glyph (its outline doesn't run along the column). + upright: bool, +} + +/// Where a line sits in its paragraph and on the page. +pub(super) struct LineFrame { + pub(super) index: usize, + pub(super) last: bool, + /// The slot the area breaker gave this line. + pub(super) slot: Option, + pub(super) baseline: f32, + pub(super) ascent: f32, + pub(super) descent: f32, +} + +/// The clusters of one run, for applying kerning as glyphs are emitted. +struct KernCursor { + run: usize, + /// Slot of the cluster each glyph of the run belongs to. + glyph_slot: Vec, + /// Glyphs emitted so far. + emitted: usize, + /// First slot whose kerning isn't applied yet. + applied: usize, +} + +impl KernCursor { + /// Kerning of the clusters before the next glyph's cluster. + fn before_glyph(&mut self, kern_px: &[f32], extra: &mut f32) { + if let Some(&slot) = self.glyph_slot.get(self.emitted) { + while self.applied < slot { + *extra += kern_px.get(self.applied).copied().unwrap_or(0.0); + self.applied += 1; + } + } + self.emitted += 1; + } + + /// After the run's last glyph: its remaining clusters' kerning follows it. + fn after_run(&mut self, slots: &[KernSlot], kern_px: &[f32], extra: &mut f32) { + if self.emitted < self.glyph_slot.len() { + return; + } + let end = slots.iter().rposition(|s| s.run == self.run).map_or(self.applied, |p| p + 1); + while self.applied < end { + *extra += kern_px.get(self.applied).copied().unwrap_or(0.0); + self.applied += 1; + } + } +} + +/// What is accumulated while a line's glyph runs are emitted. +struct LineState { + cursors: Vec, + seen_runs: Vec, + vinfo: Vec, + /// Horizontal-scale growth and kerning along the line. + extra: f32, +} + +/// Fixed facts about the line being emitted. +struct LineCtx<'a> { + dx: f32, + baseline: f32, + slots: &'a [KernSlot], + kern_px: &'a [f32], + line_index: usize, +} + +/// The horizontal extent of one glyph run on the line. +struct RunSpan { + x0: f32, + x1: f32, + style: u32, +} + +/// A run style's horizontal scale; unset (zero or negative) is 100%. +fn scale_of(st: &CharStyle) -> f32 { + if st.horizontal_scale > 0.0 { st.horizontal_scale } else { 1.0 } +} + +/// The clusters of `line` in visual order, per run. +fn collect_kern_slots(line: &Line<'_>, para: &Para<'_>) -> Vec { + let env = para.env; + let mut slots: Vec = Vec::new(); + let mut line_runs: Vec = Vec::new(); + for item in line.items() { + let PositionedLayoutItem::GlyphRun(gr) = item else { + continue; + }; + let run = gr.run(); + if line_runs.contains(&run.index()) { + continue; + } + line_runs.push(run.index()); + let hs = env.styles.get(gr.style().brush.0 as usize).map_or(1.0, scale_of); + for c in run.visual_clusters() { + let mut gl = c.glyphs(); + let first = gl.next().map(|g| g.id); + let last = gl.last().map(|g| g.id).or(first); + slots.push(KernSlot { + run: run.index(), + start: c.text_range().start, + first, + last, + font: run.font().clone(), + coords: run.normalized_coords().to_vec(), + size: run.font_size(), + rtl: c.is_rtl(), + blank: first.is_none() || c.is_space_or_nbsp() || c.text_range().end <= para.prefix.len(), + upright: env.vertical && c.first_style().brush.1 != VClass::Rotate as u8, + advance: c.advance() * hs, + growth: c.advance() * (hs - 1.0), + tab: para.tabs.binary_search(&c.text_range().start).is_ok(), + }); + } + } + slots +} + +/// Both characters of a pair kern optically: a mode change splits shaping runs, which ends +/// automatic kerning, as with Metrics. +fn optical_pair(st: &CharStyle, next: &CharStyle, a: &KernSlot, b: &KernSlot) -> bool { + st.kerning == Kerning::Optical + && next.kerning == Kerning::Optical + && st.kern == 0.0 + && next.kern == 0.0 + && !a.rtl + && !b.rtl + && !a.blank + && !b.blank + && !a.upright + && !b.upright + && a.font.data.id() == b.font.data.id() + && a.font.index == b.font.index + && a.coords == b.coords + && (a.size - b.size).abs() < 1e-3 +} + +/// Optical kerning between two adjacent clusters (font units, with the size adjustment), if the +/// font has the pair. +fn optical_kern(optical: &mut crate::optical::Cache, st: &CharStyle, a: &KernSlot, b: &KernSlot) -> Option { + let (l, r) = (a.last?, b.first?); + let coords: Vec = a.coords.iter().map(|&c| NormalizedCoord::from_bits(c)).collect(); + let k = optical.pair(a.font.data.id(), a.font.data.as_ref(), a.font.index, &coords, l, r)?; + Some(k + crate::optical::size_adjust(st.size_pt)) +} + +/// One kerning cursor per run on `line`. +fn kern_cursors(line: &Line<'_>, slots: &[KernSlot]) -> Vec { + let mut cursors: Vec = Vec::new(); + for item in line.items() { + let PositionedLayoutItem::GlyphRun(gr) = item else { + continue; + }; + let run = gr.run(); + if cursors.iter().any(|c| c.run == run.index()) { + continue; + } + let base = slots.iter().position(|s| s.run == run.index()).unwrap_or(0); + cursors.push(KernCursor { run: run.index(), glyph_slot: glyph_slots(run, base), emitted: 0, applied: base }); + } + cursors +} + +/// For each glyph of `run`, the slot of its cluster (the run's clusters start at slot `base`). +fn glyph_slots(run: &Run<'_, RunBrush>, base: usize) -> Vec { + let mut g2s = Vec::new(); + for (ci, c) in run.visual_clusters().enumerate() { + g2s.extend(c.glyphs().map(|_| base + ci)); + } + g2s +} + +/// Distributes `slack` between the clusters of the line from `c0` (and their glyphs from `g0`); +/// returns what was added to the line's length. +fn justify_line(out: &mut TextLayout, c0: usize, g0: usize, slack: f32) -> f32 { + let n = out.clusters.len() - c0; + if n <= 1 || slack <= 0.0 { + return 0.0; + } + let step = slack / (n - 1) as f32; + let starts: Vec = out.clusters[c0..].iter().map(|c| c.x).collect(); + for (i, c) in out.clusters[c0..].iter_mut().enumerate() { + c.x += step * i as f32; + } + for g in &mut out.glyphs[g0..] { + let i = starts.iter().rposition(|&s| s <= g.x + 1e-3).unwrap_or(0); + g.x += step * i as f32; + } + slack +} + +/// Line space → text space (90° clockwise); upright glyphs centred on the column. +fn rotate_to_text_space(glyphs: &mut [PlacedGlyph], vinfo: &[VGlyph], baseline: f32) { + for (g, v) in glyphs.iter_mut().zip(vinfo) { + let (u, cross) = (g.x, g.y); + if v.upright { + g.x = -baseline - v.advance * 0.5; + g.y = u + v.origin; + g.orient = GlyphOrient::Upright; + } else { + g.x = -(cross + v.centre); + g.y = u; + g.orient = GlyphOrient::Rotated; + } + } +} + +/// Underline and strikethrough rects of one run. +fn push_decorations(out: &mut TextLayout, vertical: bool, gr: &GlyphRun<'_>, baseline: f32, shift: f32, span: &RunSpan) { + let rm = gr.run().metrics(); + let (x0, x1, si) = (span.x0, span.x1, span.style); + if vertical { + // Underline to the right of the column, strikethrough through its centre + // (stored directly in text space). + let half = gr.run().font_size() * 0.5; + let mut push = |v0: f32, v1: f32| out.decorations.push(DecorationRect { x0: -v1, y0: x0, x1: -v0, y1: x1, style: si }); + if gr.style().underline.is_some() { + let v1 = baseline - half; + push(v1 - rm.underline_size.max(1.0), v1); + } + if gr.style().strikethrough.is_some() { + let sz = rm.strikethrough_size.max(1.0); + push(baseline - sz * 0.5, baseline + sz * 0.5); + } + return; + } + if gr.style().underline.is_some() { + let y0 = baseline - rm.underline_offset - shift; + out.decorations.push(DecorationRect { x0, y0, x1, y1: y0 + rm.underline_size.max(1.0), style: si }); + } + if gr.style().strikethrough.is_some() { + let y0 = baseline - rm.strikethrough_offset - shift; + out.decorations.push(DecorationRect { x0, y0, x1, y1: y0 + rm.strikethrough_size.max(1.0), style: si }); + } +} + +impl Para<'_> { + /// Kerning after each cluster (px): manual kerning plus optical pair kerning. Applied after + /// shaping (like horizontal scale) in line space, so it moves the following glyphs, carets and + /// the line extent; in vertical type that is along the column. Optical kerning measures + /// horizontal outlines, so it applies to rotated (Latin) glyphs only there. + fn kerning(&self, optical: &mut crate::optical::Cache, slots: &[KernSlot]) -> Vec { + let env = self.env; + let mut kern_px: Vec = vec![0.0; slots.len()]; + for j in 0..slots.len().saturating_sub(1) { + let (a, b) = (&slots[j], &slots[j + 1]); + if a.start < self.prefix.len() { + continue; + } + let st = &env.styles[env.style_at(self.map_offset(a.start))]; + let next = &env.styles[env.style_at(self.map_offset(b.start))]; + let mut units = if st.kern.is_finite() { st.kern } else { 0.0 }; + if optical_pair(st, next, a, b) { + units += optical_kern(optical, st, a, b).unwrap_or(0.0); + } + kern_px[j] = units / 1000.0 * a.size; + } + kern_px + } + + /// Tabs: the "kerning" after a tab takes the pen to the next stop, measured from the anchor or + /// the box edge as if the line were left-aligned (alignment then moves the whole line). + /// Returns the tab width and horizontal-scale growth in the line's trailing whitespace, which + /// is outside its extent. + fn apply_tab_stops(&self, slots: &[KernSlot], kern_px: &mut [f32], line_index: usize) -> (f32, f32) { + let mut pos = self.indent_start + if line_index == 0 { self.first_indent } else { 0.0 }; + let mut trailing_tabs = 0.0f32; + let mut trailing_growth = 0.0f32; + for (j, s) in slots.iter().enumerate() { + let rest_blank = slots[j + 1..].iter().all(|n| n.blank); + if s.blank && rest_blank { + trailing_growth += s.growth; + } + if s.tab { + kern_px[j] = super::next_tab_stop(pos, self.tab_px) - pos - s.advance; + if rest_blank { + trailing_tabs += kern_px[j]; + } + } + pos += s.advance + kern_px.get(j).copied().unwrap_or(0.0); + } + (trailing_tabs, trailing_growth) + } + + /// Where the line starts along its extent: box and area lines at their box or span edge, + /// point lines around the anchor. + fn line_start(&self, f: &LineFrame, offset: f32, adv: f32, line_kern: f32) -> f32 { + if !self.env.wraps { + let target = match self.ps.align { + TextAlign::Center | TextAlign::JustifyCenter => -(adv + line_kern) / 2.0, + TextAlign::Right | TextAlign::JustifyRight => -(adv + line_kern), + _ => self.indent_start, + }; + return target - offset; + } + // Parley aligned box lines without the kerning. + let kern_align = match self.ps.align { + TextAlign::Center => line_kern / 2.0, + TextAlign::Right => line_kern, + _ => 0.0, + }; + let origin = f.slot.map_or(self.line_origin, |s| s.x); + let base = origin + self.indent_start - kern_align; + // The line's own width takes the justify-last-line slack. + let slack = self.line_avail(f).unwrap_or(0.0) - adv * self.break_scale - line_kern; + // Parley aligned the unscaled line in `avail / break_scale`: scale its + // indent and alignment offset back to the box. + let scaled_offset = offset * (self.break_scale - 1.0); + base + if f.last { + match self.ps.align { + TextAlign::JustifyCenter => slack * 0.5 - offset, + TextAlign::JustifyRight => slack - offset, + _ => scaled_offset, + } + } else { + scaled_offset + } + } + + /// The width the line may fill: its area slot's, else the box's. + fn line_avail(&self, f: &LineFrame) -> Option { + f.slot.map(|s| s.width).or(self.avail) + } + + /// Where line `index` of `nlines` goes, advancing `flow` past it; `None` if a box clips it. + pub(super) fn frame_line(&self, line: &Line<'_>, index: usize, nlines: usize, slot: Option, flow: &mut Flow) -> Option { + let env = self.env; + let LineMetrics { ascent, descent, leading } = line_metrics(line, env.styles, &self.ps, env.k, self.first_style(), env.vertical); + let (box_x, box_y, box_w, box_h) = env.box_rect; + let baseline = match (slot, flow.prev_baseline) { + (Some(s), _) => s.baseline, + // The first column's right edge touches the box's right edge. + // (In vertical type the ascent is half the column width.) + (None, None) if env.is_box && env.vertical => -(box_x + box_w) + ascent, + // Photoshop's "first baseline: ascent": the top of the tallest ascender + // (height of 'd') touches the box top, not the font's hhea ascent. + (None, None) if env.is_box => box_y + first_ascent(line).unwrap_or(ascent), + (None, None) => 0.0, + (None, Some(b)) => b + leading + flow.pending_space, + }; + flow.pending_space = 0.0; + // Photoshop keeps a horizontal line whose baseline is inside the box and lets + // its descenders hang below the bottom edge (#1317: three 35 pt lines in a + // 91.3 pt box, the last baseline at 88). Requiring the descent to fit dropped + // that line. + let overflow = if env.vertical { baseline + descent > -box_x + 0.5 } else { baseline > box_y + box_h + 0.5 }; + if env.is_box && overflow { + return None; + } + flow.prev_baseline = Some(baseline); + Some(LineFrame { index, last: index + 1 == nlines, slot, baseline, ascent, descent }) + } + + /// Appends one line's clusters, glyphs, decorations and `LineInfo` to `out`. + pub(super) fn place_line(&self, optical: &mut crate::optical::Cache, out: &mut TextLayout, line: &Line<'_>, f: &LineFrame) { + let env = self.env; + let m = *line.metrics(); + let adv = m.advance - m.trailing_whitespace; + let slots = collect_kern_slots(line, self); + let mut kern_px = self.kerning(optical, &slots); + let (trailing_tabs, trailing_growth) = self.apply_tab_stops(&slots, &mut kern_px, f.index); + let line_kern: f32 = kern_px.iter().sum::() - trailing_tabs; + let dx = self.line_start(f, m.offset, adv, line_kern); + let lc = LineCtx { dx, baseline: f.baseline, slots: &slots, kern_px: &kern_px, line_index: out.lines.len() }; + let (g0, c0) = (out.glyphs.len(), out.clusters.len()); + let mut state = LineState { cursors: kern_cursors(line, &slots), seen_runs: Vec::new(), vinfo: Vec::new(), extra: 0.0 }; + for item in line.items() { + if let PositionedLayoutItem::GlyphRun(gr) = item { + self.emit_run(out, &lc, &mut state, &gr); + } + } + if env.vertical { + state.extra -= squeeze_punctuation(env.text, &mut out.clusters[c0..], &mut out.glyphs[g0..]); + } + if env.wraps && f.last && self.ps.align == TextAlign::JustifyAll { + let slack = self.line_avail(f).unwrap_or(0.0) - (adv + state.extra); + state.extra += justify_line(out, c0, g0, slack); + } + if env.vertical { + rotate_to_text_space(&mut out.glyphs[g0..], &state.vinfo, f.baseline); + } + let lr = line.text_range(); + let x0 = dx + m.offset; + out.lines.push(LineInfo { + range: self.map_offset(lr.start)..self.map_offset(lr.end).min(self.content_end), + baseline: f.baseline, + x0, + x1: x0 + adv + state.extra - trailing_tabs - trailing_growth, + ascent: f.ascent, + descent: f.descent, + paragraph: self.index, + }); + } + + /// Emits one glyph run of the line: its face, clusters (once per shaping run), glyphs and + /// decorations. + fn emit_run(&self, out: &mut TextLayout, lc: &LineCtx<'_>, state: &mut LineState, gr: &GlyphRun<'_>) { + let env = self.env; + let run = gr.run(); + let si = gr.style().brush.0; + let st = &env.styles[si as usize]; + let hs = scale_of(st); + let synth = run.synthesis(); + let face = out.faces.len() as u32; + out.faces.push(GlyphFace { + font: run.font().clone(), + coords: run.normalized_coords().to_vec(), + size_px: run.font_size(), + embolden: synthetic_bold(synth.embolden(), st.weight, run.font_attrs().weight.value()), + skew_deg: synth.skew().unwrap_or(0.0), + }); + let run_x0 = lc.dx + gr.offset() + state.extra; + if !state.seen_runs.contains(&run.index()) { + state.seen_runs.push(run.index()); + self.push_clusters(out, lc, run, hs, run_x0); + } + self.push_glyphs(out, lc, state, gr, face, hs); + let span = RunSpan { x0: run_x0, x1: lc.dx + gr.offset() + gr.advance() + state.extra, style: si }; + push_decorations(out, env.vertical, gr, lc.baseline, st.baseline_shift_pt * env.k, &span); + } + + /// The caret clusters of a shaping run, with kerning and horizontal scale in their advances. + fn push_clusters(&self, out: &mut TextLayout, lc: &LineCtx<'_>, run: &Run<'_, RunBrush>, hs: f32, run_x0: f32) { + let mut cx = run_x0; + let base = lc.slots.iter().position(|s| s.run == run.index()).unwrap_or(0); + for (ci, c) in run.visual_clusters().enumerate() { + let a = c.advance() * hs + lc.kern_px.get(base + ci).copied().unwrap_or(0.0); + let r = c.text_range(); + if r.end > self.prefix.len() || self.prefix.is_empty() { + out.clusters.push(ClusterInfo { + range: self.map_offset(r.start)..self.map_offset(r.end), + x: cx, + advance: a, + line: lc.line_index, + rtl: c.is_rtl(), + }); + } + cx += a; + } + } + + /// The glyphs of one run, with the kerning of the clusters before each. + fn push_glyphs(&self, out: &mut TextLayout, lc: &LineCtx<'_>, state: &mut LineState, gr: &GlyphRun<'_>, face: u32, hs: f32) { + let run = gr.run(); + let rm = run.metrics(); + let vm = if self.env.vertical { Some(VMetrics::new(run.font(), run.normalized_coords(), run.font_size())) } else { None }; + let class = gr.style().brush.1; + // Glyphs of the run's own characters (not substituted by `vert`). + let plain: Vec = match (&vm, class) { + (Some(v), 2) => self.text_of(run.text_range()).chars().filter_map(|c| v.cmap(c)).collect(), + _ => Vec::new(), + }; + let mut pen = gr.offset(); + let mut cursor = state.cursors.iter_mut().find(|c| c.run == run.index()); + for g in gr.glyphs() { + if let Some(c) = cursor.as_deref_mut() { + c.before_glyph(lc.kern_px, &mut state.extra); + } + out.glyphs.push(PlacedGlyph { + face, + id: g.id, + x: lc.dx + pen + g.x + state.extra, + y: lc.baseline + g.y, + style: gr.style().brush.0, + orient: GlyphOrient::Horizontal, + }); + if let Some(v) = &vm { + let upright = class == 1 || (class == 2 && !plain.contains(&g.id)); + state.vinfo.push(VGlyph { + upright, + advance: g.advance * hs, + origin: if upright { v.origin(g.id) } else { 0.0 }, + // Rotated text is centred on the column: its baseline sits + // (ascent − descent) / 2 from the centre line. + centre: (rm.ascent - rm.descent) * 0.5, + }); + } + state.extra += g.advance * (hs - 1.0); + pen += g.advance; + } + if let Some(c) = cursor { + c.after_run(lc.slots, lc.kern_px, &mut state.extra); + } + } +} diff --git a/crates/text/src/layout/navigate.rs b/crates/text/src/layout/navigate.rs index e7d9c00333..e87bf9b086 100644 --- a/crates/text/src/layout/navigate.rs +++ b/crates/text/src/layout/navigate.rs @@ -1,4 +1,4 @@ -use super::*; +use super::TextLayout; /// Byte offset of character `index` (the text length when `index` is past the end). pub fn byte_index(text: &str, index: usize) -> usize { diff --git a/crates/text/src/layout/paragraphs.rs b/crates/text/src/layout/paragraphs.rs index 2be1fbbc5a..9d9e017d1c 100644 --- a/crates/text/src/layout/paragraphs.rs +++ b/crates/text/src/layout/paragraphs.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CharStyle, Layout, PositionedLayoutItem, Range, RunBrush}; /// Photoshop's default tab stops: every half inch, measured from the anchor of point text or the /// left edge of a paragraph box. PSD type carries no custom tab stops, so these are the only ones. diff --git a/crates/text/src/layout/shaping.rs b/crates/text/src/layout/shaping.rs new file mode 100644 index 0000000000..283c7e2ef3 --- /dev/null +++ b/crates/text/src/layout/shaping.rs @@ -0,0 +1,438 @@ +//! Per-paragraph preparation of a text layer's layout: the paragraph's shaped text (prefix, case +//! mapping, tabs), the parley shaping pass over it, and its line breaking. + +use std::borrow::Cow; +use std::cell::Cell; +use std::ops::Range; + +use parley::{Alignment, AlignmentOptions, FontFeatures, Layout, LayoutContext, StyleProperty}; +use photocraft_doc::text::{CharStyle, ParagraphStyle, TextAlign, TextDirection}; + +use super::area::{LineMetrics, MAX_AREA_PASSES, area_metrics, break_area}; +use super::paragraphs::{TabWidth, strip_break, tab_widths}; +use super::style::{ + SYNTHETIC_SMALL_CAPS_SCALE, SmallCapsMode, drawn_style, effective_horizontal_scale, feature_list, hangul_runs, probe_smcp, small_caps_mode, style_props, +}; +use super::vertical::vertical_class; +use super::{Env, FORCED_LINE_BREAK, Flow, LRM, RLM, RunBrush, Slot, VClass}; +use crate::fonts::FontDb; +use crate::layout::DEFAULT_TAB_STOP_PT; + +/// The character styles of `runs`, with families resolved (PostScript names from PSDs, unknown +/// families) and drawn the way they are laid out. +pub(super) fn resolve_run_styles(fonts: &mut FontDb, runs: &[photocraft_doc::text::TextRun]) -> Vec { + runs.iter() + .map(|r| { + let mut s = r.style.clone(); + adopt_postscript_face(fonts, &mut s); + // Drawn with a fallback for now; a host that serves this family fetches it (`served`). + if !fonts.has_family(&s.font_family) { + crate::served::request(&s.font_family); + } + fonts.select_named_face(&mut s); + drawn_style(&mut s); + s + }) + .collect() +} + +/// A style naming a PostScript face the family doesn't carry takes the family the face resolves to. +fn adopt_postscript_face(fonts: &mut FontDb, s: &mut CharStyle) { + let Some(ps) = s.postscript_name.clone() else { + return; + }; + if fonts.faces(&s.font_family).iter().any(|f| f.postscript_name.as_deref() == Some(&ps)) { + return; + } + let f = fonts.resolve_postscript(&ps); + // An exact face match wins; a guessed family only fills a missing family. + if f.exact || (!fonts.has_family(&s.font_family) && fonts.has_family(&f.family)) { + s.font_family = f.family; + s.weight = f.weight; + s.italic = f.italic; + } +} + +/// Start offset of each item, given the items' lengths. +pub(super) fn running_starts(lens: impl Iterator) -> Vec { + lens.scan(0, |a, len| { + let s = *a; + *a += len; + Some(s) + }) + .collect() +} + +/// How each style sets small caps. Fixed for the whole layout, and the font probe only matters for +/// styles that use small caps. A real `smcp` substitution must receive lowercase source text; only +/// the fallback path uppercases it and scales it down. +pub(super) fn small_caps_modes(fonts: &mut FontDb, styles: &[CharStyle]) -> Vec { + styles + .iter() + .map(|style| match style.caps { + photocraft_doc::text::Caps::SmallCaps => small_caps_mode(style.caps, probe_smcp(fonts, style)), + _ => SmallCapsMode::None, + }) + .collect() +} + +/// The text handed to the shaper for one paragraph. +struct ParaText { + ptext: String, + /// Ranges in `ptext` for lowercase characters rendered as synthetic small caps. They have the + /// same UTF-8 length as their uppercase form, so the layer's byte-based run and caret offsets + /// remain unchanged. + synthetic_small_caps: Vec<(Range, f32)>, + /// Offsets in `ptext` of the spaces standing in for tabs. + tabs: Vec, +} + +/// The shaped text of the paragraph at `range` (`content_end` excludes its break), led by `prefix`. +fn paragraph_text(env: &Env<'_>, prefix: &str, range: &Range, content_end: usize) -> ParaText { + let content = &env.text[range.start..content_end]; + let mut ptext = String::with_capacity(prefix.len() + content.len()); + ptext.push_str(prefix); + let mut synthetic_small_caps = Vec::new(); + let mut tabs = Vec::new(); + for (i, ch) in content.char_indices() { + // A forced line break ends the line but not the paragraph. The line breaker knows + // it as a newline, which has the same length, so text offsets don't move. + if ch == FORCED_LINE_BREAK { + ptext.push('\n'); + continue; + } + // Imported PSD text can contain literal tab controls. Font shaping may + // render those as .notdef boxes; a space preserves the one-byte source + // and style/caret offsets while supplying a real whitespace advance. The space + // is widened to its tab stop after line breaking (`tab_px`). + if ch == '\t' { + tabs.push(ptext.len()); + ptext.push(' '); + continue; + } + let si = env.style_at(range.start + i); + let synthetic = env.small_caps[si] == SmallCapsMode::Synthetic; + push_cased(&mut ptext, &mut synthetic_small_caps, ch, &env.styles[si], synthetic, env.k); + } + ParaText { ptext, synthetic_small_caps, tabs } +} + +/// Appends `ch` as its style sets it: uppercased for all caps and synthetic small caps (when the +/// uppercase form has the same length, so offsets don't move), recording small-cap pieces. +fn push_cased(ptext: &mut String, small_caps: &mut Vec<(Range, f32)>, ch: char, style: &CharStyle, synthetic: bool, k: f32) { + let caps = style.caps; + if caps == photocraft_doc::text::Caps::AllCaps || synthetic { + let up: String = ch.to_uppercase().collect(); + if up.len() == ch.len_utf8() { + ptext.push_str(&up); + if caps == photocraft_doc::text::Caps::SmallCaps && ch.is_lowercase() { + let end = ptext.len(); + small_caps.push((end - up.len()..end, style.size_pt * k * SYNTHETIC_SMALL_CAPS_SCALE)); + } + return; + } + } + ptext.push(ch); +} + +/// One run piece of vertical type, whose upright stretches get the vertical alternates and their +/// class in the brush. +struct VerticalPiece<'a> { + style: &'a CharStyle, + run: usize, + small_caps: SmallCapsMode, + /// Offset of the piece in the paragraph text. + start: usize, +} + +impl VerticalPiece<'_> { + fn push_upright(&self, b: &mut parley::RangedBuilder<'_, RunBrush>, from: usize, to: usize, cls: VClass) { + if cls == VClass::Rotate || from >= to { + return; + } + let mut feats = feature_list(self.style, self.small_caps); + feats.push("\"vert\" 1".into()); + let r = (self.start + from)..(self.start + to); + b.push(StyleProperty::FontFeatures(FontFeatures::Source(Cow::Owned(feats.join(", ")))), r.clone()); + b.push(StyleProperty::Brush(RunBrush(self.run as u32, cls as u8)), r); + } + + /// Splits `piece` into stretches of one vertical class and styles each. + fn push_all(&self, b: &mut parley::RangedBuilder<'_, RunBrush>, piece: &str) { + let mut seg: Option<(usize, VClass)> = None; + for (i, ch) in piece.char_indices() { + let cls = vertical_class(ch); + match seg { + Some((_, c)) if c == cls => {} + Some((from, c)) => { + self.push_upright(b, from, i, c); + seg = Some((i, cls)); + } + None => seg = Some((i, cls)), + } + } + if let Some((from, c)) = seg { + self.push_upright(b, from, piece.len(), c); + } + } +} + +/// What line placement needs of one paragraph: its text, styles, indents and the extents its lines +/// break at. +pub(super) struct Para<'a> { + pub(super) env: &'a Env<'a>, + pub(super) index: usize, + pub(super) ps: ParagraphStyle, + pub(super) start: usize, + pub(super) content_end: usize, + pub(super) prefix: &'static str, + ptext: String, + synthetic_small_caps: Vec<(Range, f32)>, + /// Offsets in the paragraph text of the spaces standing in for tabs. + pub(super) tabs: Vec, + /// Runs intersecting the paragraph: `si0..si_end`. + si0: usize, + si_end: usize, + fallback: Vec, + pub(super) indent_start: f32, + indent_end: f32, + pub(super) first_indent: f32, + /// Box extent along the lines: where they start. + pub(super) line_origin: f32, + /// Box extent along the lines, less the indents; `None` for point text. + pub(super) avail: Option, + pub(super) break_scale: f32, + pub(super) tab_px: f32, + /// Line-breaking passes left for this paragraph's area type (see `break_area`). + area_budget: Cell, +} + +impl<'a> Para<'a> { + pub(super) fn new(env: &'a Env<'a>, fonts: &FontDb, index: usize, range: Range, ps: ParagraphStyle) -> Self { + let k = env.k; + let content_end = strip_break(env.text, &range); + let prefix = match ps.direction { + TextDirection::Auto => "", + TextDirection::Ltr => LRM, + TextDirection::Rtl => RLM, + }; + let ParaText { ptext, synthetic_small_caps, tabs } = paragraph_text(env, prefix, &range, content_end); + // Scripts the loaded fonts may lack (Arabic, Japanese, …): a host serving a font for them + // fetches it (`served`); it joins the fallback stack once it arrives. + crate::served::request_for_text(&ptext); + let fallback = fonts.fallback_stack().map(str::to_string).collect(); + let si0 = env.style_at(range.start); + let si_end = env.run_starts.partition_point(|&rs| rs < content_end).max(si0 + 1).min(env.styles.len()); + let indent_start = ps.start_indent_pt * k; + let indent_end = ps.end_indent_pt * k; + let first_indent = ps.first_line_indent_pt * k; + // Box extent along the lines: width, or height for vertical type (columns). + let (box_x, box_y, box_w, box_h) = env.box_rect; + let (line_origin, line_len) = if env.vertical { (box_y, box_h) } else { (box_x, box_w) }; + let avail = if env.is_box { Some((line_len - indent_start - indent_end).max(1.0)) } else { None }; + let mut para = Self { + env, + index, + ps, + start: range.start, + content_end, + prefix, + ptext, + synthetic_small_caps, + tabs, + si0, + si_end, + fallback, + indent_start, + indent_end, + first_indent, + line_origin, + avail, + break_scale: 1.0, + tab_px: DEFAULT_TAB_STOP_PT * k, + area_budget: Cell::new(MAX_AREA_PASSES), + }; + para.break_scale = para.horizontal_break_scale(); + para + } + + pub(super) fn first_style(&self) -> &'a CharStyle { + &self.env.styles[self.si0] + } + + /// Text offset of the paragraph text's byte `o`, as the layer's text offset. + pub(super) fn map_offset(&self, o: usize) -> usize { + (self.start + o.saturating_sub(self.prefix.len())).min(self.content_end) + } + + /// The paragraph text up to the end of the run piece at text range `range`. + pub(super) fn text_of(&self, range: Range) -> &str { + self.ptext.get(range).unwrap_or("") + } + + /// Horizontal scale is applied after shaping, so paragraph text breaks at the box (or area span) + /// width divided by it (#5565). Mixed scales break at the largest, so no line overflows the box. + fn horizontal_break_scale(&self) -> f32 { + let env = self.env; + if !env.wraps { + return 1.0; + } + let scale = (self.si0..self.si_end) + .filter(|&ri| { + let rs = env.run_starts.get(ri).copied().unwrap_or(0); + let end = rs.saturating_add(env.runs.get(ri).map_or(0, |r| r.len)); + ri == self.si0 || (rs.max(self.start) < end.min(self.content_end)) + }) + .filter_map(|ri| env.styles.get(ri)) + .map(effective_horizontal_scale) + .fold(0.0f32, f32::max); + if scale > 0.0 { scale } else { 1.0 } + } + + /// Shapes the paragraph, each tab widened by its letter spacing in `tab_spacing` (px). + fn shape(&self, lcx: &mut LayoutContext, fcx: &mut parley::FontContext, tab_spacing: &[f32]) -> Layout { + let env = self.env; + let mut b = lcx.ranged_builder(fcx, &self.ptext, 1.0, false); + // Paragraph-start style as the default (covers the direction mark and empty + // paragraphs), then every run piece intersecting this paragraph. + for p in style_props(&env.styles[self.si0], env.k, &self.fallback, self.si0 as u32, env.small_caps[self.si0]) { + b.push_default(p); + } + // Paragraph text breaks a word wider than the box at the box edge, as Photoshop + // does (#6694): parley's default only breaks at UAX #14 opportunities and lets the + // word run out of the box. Point text has no width to overflow. + if env.is_box { + b.push_default(StyleProperty::OverflowWrap(parley::style::OverflowWrap::BreakWord)); + } + for ri in self.si0..self.si_end { + self.push_run_piece(&mut b, ri); + } + for (range, size) in &self.synthetic_small_caps { + b.push(StyleProperty::FontSize(*size), range.clone()); + } + // Korean breaks lines at spaces, between words, as Photoshop sets it; Unicode's + // default rules break between any two Hangul syllables, mid-word (#5959). + for range in hangul_runs(&self.ptext) { + b.push(StyleProperty::WordBreak(parley::WordBreak::KeepAll), range); + } + for (&at, &spacing) in self.tabs.iter().zip(tab_spacing) { + b.push(StyleProperty::LetterSpacing(spacing), at..at + 1); + } + b.build(&self.ptext) + } + + /// Styles the part of run `ri` inside this paragraph. + fn push_run_piece(&self, b: &mut parley::RangedBuilder<'_, RunBrush>, ri: usize) { + let env = self.env; + let rs = env.run_starts[ri]; + let (a, z) = (rs.max(self.start), (rs + env.runs[ri].len).min(self.content_end)); + if a >= z { + return; + } + let range = (a - self.start + self.prefix.len())..(z - self.start + self.prefix.len()); + for p in style_props(&env.styles[ri], env.k, &self.fallback, ri as u32, env.small_caps[ri]) { + b.push(p, range.clone()); + } + if env.vertical { + let piece = VerticalPiece { style: &env.styles[ri], run: ri, small_caps: env.small_caps[ri], start: range.start }; + piece.push_all(b, self.text_of(range)); + } + } + + /// Where an area paragraph's first row goes: no higher than one leading (plus paragraph + /// spacing) below the previous paragraph's last line. + fn area_origin(&self, m: LineMetrics, flow: &Flow) -> f32 { + let space_before = if flow.prev_baseline.is_some() { self.ps.space_before_pt * self.env.k } else { 0.0 }; + match (&self.env.area, flow.prev_baseline) { + (Some(_), Some(b)) => b + m.leading + flow.pending_space + space_before, + (Some(a), None) => a.bounds[1] + m.ascent, + (None, _) => 0.0, + } + } + + /// Breaks `layout` into lines; for area type also returns each line's slot and whether the + /// shape ran full. + fn break_lines(&self, layout: &mut Layout, flow: &Flow) -> (Vec, bool) { + let env = self.env; + if self.first_indent != 0.0 { + layout.set_text_indent(self.first_indent / self.break_scale, parley::IndentOptions::default()); + } + let Some(a) = &env.area else { + layout.break_all_lines(self.avail.map(|a| a / self.break_scale)); + return (Vec::new(), false); + }; + // Plan every row with the paragraph's real line metrics (its tallest font and + // largest leading, measured on the unwrapped paragraph), so no wrapped line comes + // out taller than the row it was planned in and no second plan is needed. + layout.break_all_lines(None); + let m = area_metrics(layout, env.styles, &self.ps, env.k, self.first_style(), env.vertical); + let from = self.area_origin(m, flow); + let first_px = self.first_style().size_pt * env.k; + break_area(layout, a, from, m, (self.indent_start, self.indent_end), first_px, self.break_scale, &self.area_budget) + } + + /// The widths of the tabs on the broken lines of `layout`. + fn tab_widths(&self, layout: &Layout) -> Vec { + tab_widths(layout, &self.tabs, self.env.styles, self.indent_start, self.first_indent, self.tab_px) + } + + /// Shapes and breaks the paragraph into lines (and area slots). + /// + /// A tab is never wider than a space plus one tab interval, so breaking lines at that width + /// keeps the expanded tabs inside the box, but can wrap early. Break again with the tab widths + /// that layout gives until they agree; if they don't settle, keep the safe layout. + pub(super) fn break_paragraph(&self, lcx: &mut LayoutContext, fcx: &mut parley::FontContext, flow: &Flow) -> (Layout, Vec, bool) { + let wide = if self.env.wraps { self.tab_px } else { 0.0 }; + let mut layout = self.shape(lcx, fcx, &vec![wide; self.tabs.len()]); + let (mut slots, mut full) = self.break_lines(&mut layout, flow); + if self.env.wraps + && !self.tabs.is_empty() + && let Some((settled, settled_slots, settled_full)) = self.settle_tabs(lcx, fcx, &layout, flow) + { + layout = settled; + slots = settled_slots; + full = settled_full; + } + (layout, slots, full) + } + + /// Re-breaks until the tab widths the layout gives agree with the ones it was shaped for. + fn settle_tabs( + &self, + lcx: &mut LayoutContext, + fcx: &mut parley::FontContext, + layout: &Layout, + flow: &Flow, + ) -> Option<(Layout, Vec, bool)> { + let first = self.tab_widths(layout); + let mut want: Vec = first.iter().map(|t| t.width).collect(); + for _ in 0..4 { + let spacing: Vec = first.iter().zip(&want).map(|(t, w)| w / t.scale - (t.advance - self.tab_px)).collect(); + let mut next = self.shape(lcx, fcx, &spacing); + let (next_slots, next_full) = self.break_lines(&mut next, flow); + let got: Vec = self.tab_widths(&next).iter().map(|t| t.width).collect(); + if got.iter().zip(&want).all(|(a, b)| (a - b).abs() < 0.01) { + return Some((next, next_slots, next_full)); + } + want = got; + } + None + } + + /// How parley aligns the paragraph's lines. + pub(super) fn alignment(&self) -> Alignment { + if !self.env.wraps { + return Alignment::Left; + } + match self.ps.align { + TextAlign::Left => Alignment::Left, + TextAlign::Center => Alignment::Center, + TextAlign::Right => Alignment::Right, + _ => Alignment::Justify, + } + } + + pub(super) fn align_options(&self) -> AlignmentOptions { + AlignmentOptions { align_when_overflowing: !self.env.wraps } + } +} diff --git a/crates/text/src/layout/style.rs b/crates/text/src/layout/style.rs index fc5b775a7d..e4edac49ae 100644 --- a/crates/text/src/layout/style.rs +++ b/crates/text/src/layout/style.rs @@ -1,5 +1,8 @@ use super::paragraphs::quote; -use super::*; +use super::{ + Caps, CharStyle, ClusterInfo, Cow, FontBaseline, FontDb, FontFamily, FontFeatures, FontStyle, FontVariations, FontWeight, Kerning, PlacedGlyph, RunBrush, + StyleProperty, Tag, +}; /// Photoshop synthesizes small caps for faces without an OpenType `smcp` table. Keep the same /// readable hierarchy for every font instead of silently rendering lowercase text unchanged. diff --git a/crates/text/src/layout/vertical.rs b/crates/text/src/layout/vertical.rs index beabcfbf05..31d21f3d32 100644 --- a/crates/text/src/layout/vertical.rs +++ b/crates/text/src/layout/vertical.rs @@ -1,4 +1,4 @@ -use super::*; +use super::FontData; /// How a character is set in vertical type (a simplification of Unicode's /// `Vertical_Orientation`, UAX #50). diff --git a/crates/text/src/psd/build.rs b/crates/text/src/psd/build.rs index 658e15bd18..e4097c7de8 100644 --- a/crates/text/src/psd/build.rs +++ b/crates/text/src/psd/build.rs @@ -1,6 +1,9 @@ use super::edit::{TEMPLATE_NODES, edited_object, fits, is_stub, names_one_frame, new_object, pad_frames, td_edited_object, with_modes}; use super::import::{OPENTYPE_KEYS, TXT2_FEATURES, TXT2_STYLISTIC_SETS, kern_units, manual, object_frame, pair_runs, parse_txt2, postscript_for, real}; -use super::*; +use super::{ + AntiAlias, Caps, CharStyle, Color, ColorMode, D, Descriptor, E, FontBaseline, Id, Kerning, Orientation, ParagraphStyle, TextAlign, TextLayer, TextShape, + TySh, UnicodeString, ed, engine_data, enum_value, enumv, parse_tysh, write_tysh, +}; /// A fill colour as EngineData and `Txt2` store it: the colour type (0 gray, 1 RGB, 2 CMYK) and /// its values, alpha first. diff --git a/crates/text/src/psd/edit.rs b/crates/text/src/psd/edit.rs index c2a1afae29..1b1b90e0ff 100644 --- a/crates/text/src/psd/edit.rs +++ b/crates/text/src/psd/edit.rs @@ -1,6 +1,6 @@ use super::build::{MAX_TEXT_INDEX, utf16_len}; use super::import::parse_txt2; -use super::*; +use super::{D, E, Id, parse_tysh, write_tysh}; /// The most values a `Txt2` object may hold to serve as the template for new objects. Photoshop's /// own objects hold a few hundred (611 at most in the file from #7698). diff --git a/crates/text/src/psd/import.rs b/crates/text/src/psd/import.rs index 858561a9db..374c7408e5 100644 --- a/crates/text/src/psd/import.rs +++ b/crates/text/src/psd/import.rs @@ -1,5 +1,8 @@ use super::build::engine_antialias; -use super::*; +use super::{ + Affine, AntiAlias, Caps, CharStyle, Color, ColorMode, D, Descriptor, E, FontBaseline, Kerning, Orientation, ParagraphRun, ParagraphStyle, Point, TextAlign, + TextLayer, TextRun, TextShape, TextWarp, TySh, dnum, ed, engine_data, enum_value, guess_from_postscript, parse_tysh, +}; /// Parses the document's `Txt2` block (Photoshop's text engine data for all type layers). pub fn parse_txt2(data: &[u8]) -> Option {