mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Merge pull request #4251 from dotCooCoo/fix/pdf-drawn-nbsp-thin-space
Part of #2384: imported PDF text keeps its place beside a drawn no-break or thin space
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