mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Part of #2384: imported PDF text keeps its place beside a drawn no-break or thin space
A space the file draws between two words gets tracking of its own so the word after it stays where the file put it, but only U+0020 counted as a space. A drawn no-break space (U+00A0, U+202F) or fixed-width space (U+2000 to U+200A, thin and hair spaces included) took the old path: the gaps on both sides of it also read as spaces, so the line came in with an extra space before and after it and the words after it out of place. These characters now count as spaces in the line's gap and drawn-space rules, and a space of more than one byte is split into its own run whole. The wrapped-line space tolerance in area type, a fiftieth of an em, is named SPACE_TRACKING_TOLERANCE.
This commit is contained in:
@@ -153,6 +153,10 @@ fn block(nodes: &[Arc<Node>], lines: &HashMap<NodeId, LineFacts>) -> (usize, Opt
|
||||
/// the file's widths are rounded, or it moved a glyph a hair (`TJ`).
|
||||
const EVEN_TRACKING: f64 = 10.0;
|
||||
|
||||
/// How far the tracking of a wrapped line's spaces may stray (thousandths of an em, a fiftieth of
|
||||
/// an em) and still be one value: each space the file draws beside a wider gap is tracked apart.
|
||||
const SPACE_TRACKING_TOLERANCE: f64 = 20.0;
|
||||
|
||||
/// A line's spacing: the tracking after its letters, and how much more its spaces take (word
|
||||
/// spacing, `Tw`; `None` when it has none). `None` when its letters are tracked apart (set by
|
||||
/// hand), or its spaces more than `slack` apart.
|
||||
@@ -194,7 +198,7 @@ fn area_type(nodes: &[Arc<Node>], lines: &HashMap<NodeId, LineFacts>, b: &Block)
|
||||
// type's own.
|
||||
// A wrapped line's spaces may each be tracked apart, within a fiftieth of an em of each other.
|
||||
let spacings: Vec<(f64, Option<f64>)> =
|
||||
texts.iter().map(|(t, f)| line_spacing(t, if f.wraps { 20.0 } else { EVEN_TRACKING })).collect::<Option<_>>()?;
|
||||
texts.iter().map(|(t, f)| line_spacing(t, if f.wraps { SPACE_TRACKING_TOLERANCE } else { EVEN_TRACKING })).collect::<Option<_>>()?;
|
||||
let trackings: Vec<f64> = spacings.iter().map(|(t, _)| *t).collect();
|
||||
let words: Vec<Option<f64>> = spacings.iter().map(|(_, w)| *w).collect();
|
||||
let wrapped: Vec<f64> = texts.iter().filter(|(_, f)| f.wraps).map(|(_, f)| f.length).collect();
|
||||
|
||||
@@ -276,7 +276,7 @@ impl TextLine {
|
||||
self.column = true;
|
||||
// Letter-spaced Japanese is common: a gap reads as a space only when a whole em is left
|
||||
// out (the tracking set in `finish` keeps narrower gaps).
|
||||
if step > size * 0.9 && !run.ends_with(' ') && !text.starts_with(' ') {
|
||||
if step > size * 0.9 && !run.ends_with(is_space) && !text.starts_with(is_space) {
|
||||
run.push(' ');
|
||||
}
|
||||
if last.takes(look) {
|
||||
@@ -334,15 +334,16 @@ impl TextLine {
|
||||
if !on_line && !on_curve {
|
||||
return false;
|
||||
}
|
||||
if !run.ends_with(' ') && !text.starts_with(' ') {
|
||||
if !run.ends_with(is_space) && !text.starts_with(is_space) {
|
||||
self.gaps.push(Gap { at: bytes, width: gap, em: self.last_size.min(at.size), on_line });
|
||||
}
|
||||
// A space the file draws between two glyphs spans the gaps on both sides of it.
|
||||
let drawn = self.drawn_space.take();
|
||||
if on_line {
|
||||
if text == " " && !run.ends_with(' ') {
|
||||
let mut chars = text.chars();
|
||||
if chars.next().is_some_and(is_space) && chars.next().is_none() && !run.ends_with(is_space) {
|
||||
self.drawn_space = Some((bytes, gap + advance));
|
||||
} else if let Some((b, before)) = drawn.filter(|_| !text.starts_with(' ')) {
|
||||
} else if let Some((b, before)) = drawn.filter(|_| !text.starts_with(is_space)) {
|
||||
self.drawn_spaces.push((b, before + gap));
|
||||
}
|
||||
}
|
||||
@@ -904,12 +905,13 @@ fn tracked(runs: Vec<TextRun>, tracking: f64, spaces: &[(usize, f64)]) -> Vec<Te
|
||||
let mut from = 0;
|
||||
for &(b, own) in spaces.iter().filter(|(b, _)| (off..end).contains(b)) {
|
||||
let at = b - off;
|
||||
let (Some(before), Some(space)) = (r.text.get(from..at), r.text.get(at..at + 1)) else { continue };
|
||||
let len = r.text.get(at..).and_then(|t| t.chars().next()).map_or(1, char::len_utf8);
|
||||
let (Some(before), Some(space)) = (r.text.get(from..at), r.text.get(at..at + len)) else { continue };
|
||||
if !before.is_empty() {
|
||||
out.push(TextRun { text: before.to_string(), style: style.clone(), inline: None });
|
||||
}
|
||||
out.push(TextRun { text: space.to_string(), style: CharStyle { tracking: own, ..style.clone() }, inline: None });
|
||||
from = at + 1;
|
||||
from = at + len;
|
||||
}
|
||||
if let Some(rest) = r.text.get(from..).filter(|t| !t.is_empty()) {
|
||||
out.push(TextRun { text: rest.to_string(), style, inline: None });
|
||||
@@ -919,6 +921,12 @@ fn tracked(runs: Vec<TextRun>, tracking: f64, spaces: &[(usize, f64)]) -> Vec<Te
|
||||
out
|
||||
}
|
||||
|
||||
/// A space a file draws as a glyph: U+0020, the no-break spaces (U+00A0, U+202F) or one of the
|
||||
/// fixed-width spaces U+2000 to U+200A (en, em, thin, hair and the others).
|
||||
fn is_space(c: char) -> bool {
|
||||
matches!(c, ' ' | '\u{a0}' | '\u{2000}'..='\u{200a}' | '\u{202f}')
|
||||
}
|
||||
|
||||
fn round(v: f64) -> f64 {
|
||||
(v * 1000.0).round() / 1000.0
|
||||
}
|
||||
|
||||
@@ -226,6 +226,21 @@ fn words_set_ems_apart_beside_the_spaces_the_file_draws_keep_their_place() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn words_set_ems_apart_beside_a_drawn_no_break_or_thin_space_keep_their_place() {
|
||||
// As above, the file drawing a no-break space and a thin space alone, with a gap on each side.
|
||||
let (x1, x2) = (48.0, 84.0);
|
||||
let lines: &[(f32, f32, f32, &str)] =
|
||||
&[(20.0, 50.0, 10.0, "ab"), (36.0, 50.0, 10.0, "\u{a0}"), (x1, 50.0, 10.0, "cd"), (70.0, 50.0, 10.0, "\u{2009}"), (x2, 50.0, 10.0, "ef")];
|
||||
let t = texts(&import(&source_sans(lines)).unwrap());
|
||||
assert_eq!(t.len(), 1);
|
||||
assert_eq!(t[0].plain_text(), "ab\u{a0}cd\u{2009}ef");
|
||||
let laid = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t[0]);
|
||||
let at = |byte: usize| (t[0].xf * laid.glyphs.iter().find(|g| g.byte == byte).unwrap().origin).x;
|
||||
assert!((at(4) - f64::from(x1)).abs() < 0.05, "the second word starts where the file has it: {} vs {x1}", at(4));
|
||||
assert!((at(9) - f64::from(x2)).abs() < 0.05, "the third word starts where the file has it: {} vs {x2}", at(9));
|
||||
}
|
||||
|
||||
/// A document of area type `text` (Source Sans 3, 10 pt) `width` wide at (20, 30).
|
||||
fn area_doc(text: &str, justify: Justify, width: f64, space_before: f64) -> Document {
|
||||
let style = CharStyle { size: 10.0, fill: Paint::solid(Color::BLACK), ..Default::default() };
|
||||
|
||||
Reference in New Issue
Block a user