PDF import: wide even gaps and right-to-left lines keep their spaces

A line whose shared gap is over half an em is no longer read as letter
spaced: capping that gap at half an em left glyphs set 0.55 em apart
with no spaces. The regression now covers 0.55, 0.75 and 1 em.

Whether a line is right to left is now known before its gaps are read,
from its logical order: the paragraph direction is only set afterwards,
so right-to-left lines were read for letter spacing too.

The revision-history row moves out of the Headline table, where it had
been put by mistake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/target-app-parity.md
This commit is contained in:
KnottyDyes
2026-10-22 00:10:57 +07:00
committed by bflatastic
parent 609c98f458
commit b024756e9f
2 changed files with 18 additions and 11 deletions
+5 -4
View File
@@ -131,8 +131,8 @@ struct Gap {
}
/// The spacing a line's letters share (letter-spaced type), in ems: the gap a third of the way up
/// from the narrowest, of three or more along the line. Never below zero, and at most half an em:
/// glyphs set wider apart than that are words or columns, not tracked letters.
/// from the narrowest, of three or more along the line. Never below zero, and none at all past
/// half an em: glyphs set wider apart than that are words or columns, not tracked letters.
fn common_gap(gaps: &[Gap]) -> f64 {
let mut widths: Vec<f64> = gaps.iter().filter(|g| g.on_line).map(|g| g.width / g.em).filter(|w| w.is_finite()).collect();
if widths.len() < 3 {
@@ -140,7 +140,7 @@ fn common_gap(gaps: &[Gap]) -> f64 {
}
let pivot = widths.len() / 3;
let (_, common, _) = widths.select_nth_unstable_by(pivot, f64::total_cmp);
common.clamp(0.0, 0.5)
if *common > 0.5 { 0.0 } else { common.max(0.0) }
}
/// A line of type being gathered: runs of glyphs on one baseline.
@@ -386,7 +386,8 @@ impl TextLine {
t.runs.extend(runs);
// A gap wider than a fifth of an em, past the spacing the line's letters share, reads as a
// space. Letter spacing is read only in text left in the file's order.
let common = if t.para.direction.is_none() { common_gap(&self.gaps) } else { 0.0 };
let directed = vectorcraft_text::logical_order(&t.plain_text()).is_some();
let common = if directed { 0.0 } else { common_gap(&self.gaps) };
let mut spaces = vec![];
for gap in &self.gaps {
// A word space set at exactly a fifth of an em still counts, whatever the rounding.
+13 -7
View File
@@ -127,15 +127,21 @@ fn common_tracking_does_not_turn_gaps_into_spaces() {
}
}
/// Glyphs set an em or more apart, each on its own, are words or columns: every gap stays a space,
/// however evenly they are spaced.
/// Glyphs set more than half an em apart, each on its own, are words or columns: every gap stays a
/// space, however evenly they are spaced.
#[test]
fn evenly_wide_gaps_stay_spaces() {
let lines: Vec<_> = ["1", "2", "3", "4"].iter().enumerate().map(|(i, text)| (20.0 + i as f32 * 18.0, 60.0, 12.0, *text)).collect();
let d = import_with_report(&text_pdf(&lines, false), &ImportOptions::default()).unwrap().document;
let t = texts(&d);
assert_eq!(t.len(), 1, "{t:?}");
assert_eq!(t[0].plain_text(), "1 2 3 4");
let font = vectorcraft_text::FontDb::global().find_postscript("SourceSans3-Regular").unwrap();
let size = 12.0_f32;
let one = font.advance(font.glyph_for('1')) as f32 / font.units_per_em() as f32 * size;
for gap in [0.55_f32, 0.75, 1.0] {
let lines: Vec<_> =
["1", "2", "3", "4"].iter().enumerate().map(|(i, text)| (20.0 + i as f32 * (one + gap * size), 60.0, size, *text)).collect();
let d = import_with_report(&text_pdf(&lines, false), &ImportOptions::default()).unwrap().document;
let t = texts(&d);
assert_eq!(t.len(), 1, "gap {gap} em: {t:?}");
assert_eq!(t[0].plain_text(), "1 2 3 4", "gap {gap} em");
}
}
/// Embedded font data is untrusted: anything that isn't a CID-keyed CFF is no table, never a panic.