Refactor text for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-27 23:47:07 +00:00
parent 6b397ff7c0
commit 9800f56dfb
11 changed files with 1071 additions and 665 deletions
+94 -657
View File
@@ -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<u32>,
last: Option<u32>,
font: FontData,
coords: Vec<i16>,
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<usize> = 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<usize> = paras
.iter()
.scan(0, |a, r| {
let s = *a;
*a += r.len;
Some(s)
})
.collect();
let para_style_at = |off: usize| &paras[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<f32> = 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<SmallCapsMode> = 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<usize>, f32)> = Vec::new();
// Offsets in `ptext` of the spaces standing in for tabs.
let mut tabs: Vec<usize> = 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<String> = 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<RunBrush>, fcx: &mut parley::FontContext, tab_spacing: &[f32]| -> Layout<RunBrush> {
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<RunBrush>| -> (Vec<Slot>, 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<f32> = first.iter().map(|t| t.width).collect();
for _ in 0..4 {
let spacing: Vec<f32> = 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<f32> = 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<KernSlot> = Vec::new();
let mut line_runs: Vec<usize> = 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<f32> = 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<skrifa::instance::NormalizedCoord> =
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::<f32>() - 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, 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<VGlyph> = Vec::new();
let mut extra = 0.0f32; // horizontal-scale growth and kerning along the line
let mut seen_runs: Vec<usize> = 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<u32> = 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<f32> = 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<crate::area::AreaShape>,
}
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<f32>,
pending_space: f32,
stop: bool,
}
/// Where one line of area type goes: its baseline, the left edge of its span, and the width
+1 -1
View File
@@ -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)]
+522
View File
@@ -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<u32>,
last: Option<u32>,
font: parley::FontData,
coords: Vec<i16>,
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<Slot>,
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<usize>,
/// 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<KernCursor>,
seen_runs: Vec<usize>,
vinfo: Vec<VGlyph>,
/// 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<KernSlot> {
let env = para.env;
let mut slots: Vec<KernSlot> = Vec::new();
let mut line_runs: Vec<usize> = 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<f32> {
let (l, r) = (a.last?, b.first?);
let coords: Vec<NormalizedCoord> = 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<KernCursor> {
let mut cursors: Vec<KernCursor> = 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<usize> {
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<f32> = 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<f32> {
let env = self.env;
let mut kern_px: Vec<f32> = 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<f32> {
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<Slot>, flow: &mut Flow) -> Option<LineFrame> {
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::<f32>() - 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<u32> = 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);
}
}
}
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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.
+438
View File
@@ -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<CharStyle> {
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<Item = usize>) -> Vec<usize> {
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<SmallCapsMode> {
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<usize>, f32)>,
/// Offsets in `ptext` of the spaces standing in for tabs.
tabs: Vec<usize>,
}
/// 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<usize>, 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<usize>, 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<usize>, f32)>,
/// Offsets in the paragraph text of the spaces standing in for tabs.
pub(super) tabs: Vec<usize>,
/// Runs intersecting the paragraph: `si0..si_end`.
si0: usize,
si_end: usize,
fallback: Vec<String>,
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<f32>,
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<usize>,
}
impl<'a> Para<'a> {
pub(super) fn new(env: &'a Env<'a>, fonts: &FontDb, index: usize, range: Range<usize>, 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<usize>) -> &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<RunBrush>, fcx: &mut parley::FontContext, tab_spacing: &[f32]) -> Layout<RunBrush> {
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<RunBrush>, flow: &Flow) -> (Vec<Slot>, 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<RunBrush>) -> Vec<TabWidth> {
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<RunBrush>, fcx: &mut parley::FontContext, flow: &Flow) -> (Layout<RunBrush>, Vec<Slot>, 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<RunBrush>,
fcx: &mut parley::FontContext,
layout: &Layout<RunBrush>,
flow: &Flow,
) -> Option<(Layout<RunBrush>, Vec<Slot>, bool)> {
let first = self.tab_widths(layout);
let mut want: Vec<f32> = first.iter().map(|t| t.width).collect();
for _ in 0..4 {
let spacing: Vec<f32> = 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<f32> = 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 }
}
}
+4 -1
View File
@@ -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.
+1 -1
View File
@@ -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).
+4 -1
View File
@@ -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.
+1 -1
View File
@@ -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).
+4 -1
View File
@@ -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<E> {