mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3753 from Taha-Mahmoodi/fix/pdf-import-drift
PDF import: keep a line's glyphs on course when the file's widths are a little off the font's advances
This commit is contained in:
@@ -623,7 +623,8 @@ fn restyled(t: &TextObject, kern: Option<f64>, own: &[f64]) -> Vec<TextRun> {
|
||||
/// font's kerning or none, whichever the file's spacing follows, and tracking that is the same
|
||||
/// after every character when the file spaced them evenly, else each character's own (word
|
||||
/// spacing, `TJ` adjustments, character tightening, spaces read from gaps), measured from the
|
||||
/// file's widths (not the font's advances, which may be a hair off them).
|
||||
/// file's widths (not the font's advances, which may be a hair off them) and kept on course where
|
||||
/// the differences add up ([`on_course`]).
|
||||
///
|
||||
/// The file put the glyph of each character with a [`Slot`] at `baseline` (one origin per glyph),
|
||||
/// measured along `path` (type on a path: one curve per glyph) or along `line`'s baseline from its
|
||||
@@ -760,7 +761,8 @@ fn place_each(
|
||||
let by_file =
|
||||
|pens: &[Option<(f64, f64)>]| -> Vec<Option<(f64, f64)>> { pens.iter().zip(&drift).map(|(p, d)| p.map(|(x, a)| (x + d, a))).collect() };
|
||||
// The font's kerning when the file followed it (or there is none to follow), else none.
|
||||
let kerned = spacing(&by_file(&pens(&auto, &chars)))?;
|
||||
let kerned_pens = pens(&auto, &chars);
|
||||
let kerned = spacing(&by_file(&kerned_pens))?;
|
||||
let (kern, own) = match even(&kerned) {
|
||||
Some(own) => (None, own),
|
||||
// Without kerning the line lays out the same.
|
||||
@@ -774,11 +776,8 @@ fn place_each(
|
||||
}
|
||||
}
|
||||
};
|
||||
let own = on_course(own, if kern.is_some() { &unkerned_pens } else { &kerned_pens }, &target, size);
|
||||
let runs = restyled(t, kern, &own);
|
||||
// As the type is: laid out so already.
|
||||
if kern.is_none() && runs.iter().all(|r| r.style.tracking == 0.0) {
|
||||
return Some((runs, auto));
|
||||
}
|
||||
// Laid out so, are they where the file put them? (A ligature the layout forms where the file
|
||||
// drew two glyphs moves those after it.)
|
||||
let mut probe = t.clone();
|
||||
@@ -792,6 +791,65 @@ fn place_each(
|
||||
ok.then_some((probe.runs, check))
|
||||
}
|
||||
|
||||
/// `own` (each character's spacing, points, measured from the file's widths) kept on course: laid
|
||||
/// out from `pens` (where each character goes untracked), the glyphs land within a hundredth of an
|
||||
/// em of `target` (where the file put them). The font's advances may each be a little off the
|
||||
/// file's widths (by up to the 1% that still counts as the same font), and a line spaced evenly to
|
||||
/// a hair takes the same value after every character: small differences that add up along a line.
|
||||
/// Off course, each stretch of the line takes a share of the difference after every character
|
||||
/// (one run), as long a stretch as one share keeps on course.
|
||||
fn on_course(mut own: Vec<f64>, pens: &[Option<(f64, f64)>], target: &[Option<(f64, Option<f64>)>], size: f64) -> Vec<f64> {
|
||||
let tolerance = size * 0.01;
|
||||
// How much further along than `own` lays it out each glyph the file placed must go.
|
||||
let mut at = 0.0;
|
||||
let mut behind = vec![];
|
||||
for (i, ((p, t), e)) in pens.iter().zip(target).zip(&own).enumerate() {
|
||||
if let (Some((p, _)), Some((t, _))) = (p, t) {
|
||||
behind.push((i, t - p - at));
|
||||
}
|
||||
at += e;
|
||||
}
|
||||
if behind.iter().all(|(_, d)| d.abs() <= tolerance) {
|
||||
return own;
|
||||
}
|
||||
// The character the stretch starts at, how far the shares before it moved it, the last glyph
|
||||
// the stretch keeps on course, and the shares (lowest, highest) that keep each since on it.
|
||||
let (mut from, mut moved, mut last) = (0, 0.0, 0);
|
||||
let (mut lo, mut hi) = (f64::NEG_INFINITY, f64::INFINITY);
|
||||
for &(i, d) in &behind {
|
||||
if i <= from {
|
||||
continue;
|
||||
}
|
||||
let course = |from: usize, moved: f64| {
|
||||
let n = (i - from) as f64;
|
||||
((d - moved - tolerance) / n, (d - moved + tolerance) / n)
|
||||
};
|
||||
let (l, h) = course(from, moved);
|
||||
if l > hi || h < lo {
|
||||
// No share keeps this glyph on course too: the stretch ends at the last one it does.
|
||||
let share = (lo + hi) / 2.0;
|
||||
for e in own.get_mut(from..last).into_iter().flatten() {
|
||||
*e += share;
|
||||
}
|
||||
moved += share * (last - from) as f64;
|
||||
from = last;
|
||||
(lo, hi) = course(from, moved);
|
||||
} else {
|
||||
(lo, hi) = (lo.max(l), hi.min(h));
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
// The last stretch takes its share after the characters past the last glyph the file placed
|
||||
// too (it changes nothing there).
|
||||
if lo.is_finite() && hi.is_finite() {
|
||||
let share = (lo + hi) / 2.0;
|
||||
for e in own.get_mut(from..).into_iter().flatten() {
|
||||
*e += share;
|
||||
}
|
||||
}
|
||||
own
|
||||
}
|
||||
|
||||
/// The middle value of `v` (the upper one of an even count).
|
||||
pub(crate) fn median(mut v: Vec<f64>) -> Option<f64> {
|
||||
v.sort_by(f64::total_cmp);
|
||||
|
||||
@@ -589,6 +589,40 @@ fn widths_rounded_off_the_installed_fonts_advances_keep_one_tracking() {
|
||||
assert!(t[0].runs[1].style.tracking > 500.0, "the em's gap on the space: {:?}", t[0].runs);
|
||||
}
|
||||
|
||||
/// A long line whose file widths are each a little off the font's advances (0.8% wider or
|
||||
/// narrower, within the 1% that still counts as the same font) opens with its glyphs where the
|
||||
/// file draws them, in one run: the differences add up along the line (with only the spacing the
|
||||
/// file set, none, the glyphs would end points away from the file's), so the line takes their
|
||||
/// share after every character. A line the font kerns (laid out without its kerning, as the file
|
||||
/// sets it) and one it doesn't (laid out as the type is). Source Sans 3 is embedded: what the file
|
||||
/// draws and what the type draws are the same outlines, so their contours tell where each glyph is.
|
||||
#[test]
|
||||
fn a_long_line_of_widths_a_little_off_the_fonts_advances_lands_in_one_run() {
|
||||
let n = first_extra(1);
|
||||
let resources = format!("/Font << /F1 {} 0 R >>", n + 2);
|
||||
for scale in [1.008, 0.992] {
|
||||
let widths: Vec<String> = (32u8..=126).map(|c| (width(c as char) * scale).round().to_string()).collect();
|
||||
let font = format!(
|
||||
"<< /Type /Font /Subtype /TrueType /BaseFont /SourceSans3-Regular /FirstChar 32 /LastChar 126 /Widths [{}] /FontDescriptor {} 0 R /Encoding /WinAnsiEncoding >>",
|
||||
widths.join(" "),
|
||||
n + 1
|
||||
);
|
||||
for line in [
|
||||
"Gather the quick brown fox over the lazy dogs while the waves roll in",
|
||||
"minimum minimum minimum minimum minimum minimum minimum minimum",
|
||||
] {
|
||||
let content = format!("BT /F1 12 Tf 10 50 Td ({line}) Tj ET");
|
||||
let page = PdfPage { resources: resources.clone(), ..PdfPage::new(600.0, 200.0, &content) };
|
||||
let bytes = pdf_with(&[page], &[&font_file(), &descriptor(n), &font], None);
|
||||
let t = texts(&open_as(&bytes, TextAs::Text));
|
||||
assert_eq!(t.iter().map(TextObject::plain_text).collect::<Vec<_>>(), [line]);
|
||||
assert_eq!(t[0].runs.len(), 1, "{scale} {line:?}: {:?}", t[0].runs);
|
||||
let d = displacement(&bytes);
|
||||
assert!(d < 12.0 * 0.02, "{scale} {line:?}: glyphs {d:.3} pt from where the file draws them");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A text-heavy page: `columns` columns of `rows` lines of 10 pt type, alternately plain lines and
|
||||
/// paragraphs wrapped in a frame (left aligned, and justified with word spacing).
|
||||
fn text_heavy(columns: usize, rows: usize) -> Vec<u8> {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user