Merge pull request #3742 from Taha-Mahmoodi/fix/pdf-import-run-split

PDF import: keep one tracking value on letters whose file widths round off the installed font's advances
This commit is contained in:
BFlat
2026-10-23 09:23:34 +07:00
committed by GitHub
3 changed files with 54 additions and 3 deletions
+27 -3
View File
@@ -622,7 +622,8 @@ fn restyled(t: &TextObject, kern: Option<f64>, own: &[f64]) -> Vec<TextRun> {
/// The runs of `t` that put its glyphs where the file did (#722), and their layout: with the
/// 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).
/// 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).
///
/// 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
@@ -735,14 +736,37 @@ fn place_each(
if !same_font {
return None;
}
// Where the glyphs go on the file's own widths: each character's pen moved by how much wider
// the file says the characters before it are than the font's advances (a file gives widths to
// the thousandth of an em, so a font of 2048 units an em is a hair off), where the file gives
// a character's width and the layout sets it as a glyph of its own. Spacing measured from them
// is the file's (its `Tc`, `Tw`, `TJ`): measured from the installed font's advances, each
// character would also take its own hair of tracking (a run each), and keep it once a font
// rounded otherwise is installed.
let end = pen;
let mut wider = 0.0;
let mut drift = Vec::with_capacity(chars.len());
for (i, t) in target.iter().enumerate() {
drift.push(wider);
let (Some(Some((start, _))), Some((_, Some(width)))) = (unkerned_pens.get(i), t) else { continue };
let next = match unkerned_pens.get(i + 1) {
Some(n) => n.map(|(p, _)| p),
None => Some(end),
};
if let Some(next) = next {
wider += width - (next - start);
}
}
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(&pens(&auto, &chars))?;
let kerned = spacing(&by_file(&pens(&auto, &chars)))?;
let (kern, own) = match even(&kerned) {
Some(own) => (None, own),
// Without kerning the line lays out the same.
None if !kerned_any => (None, kerned),
None => {
let unkerned = spacing(&unkerned_pens)?;
let unkerned = spacing(&by_file(&unkerned_pens))?;
match even(&unkerned) {
Some(own) => (Some(0.0), own),
None if changes(&unkerned) < changes(&kerned) => (Some(0.0), unkerned),
+26
View File
@@ -563,6 +563,32 @@ fn text_boxes_word_spaced_throughout_open_as_one() {
lands(&content, "", &[PARAGRAPH_TEXT]);
}
/// A font the file names but doesn't carry, drawn with the installed one of that name, whose
/// advances (2048 units an em) its widths give only to the thousandth of an em: the spacing the
/// file set is measured from its widths, so letters a hair off the font's widths still take one
/// tracking value, not each character its own (a run each). Inter is built in, so this doesn't
/// depend on the fonts installed.
#[test]
fn widths_rounded_off_the_installed_fonts_advances_keep_one_tracking() {
let inter = FontDb::global().find_postscript("Inter-Regular").expect("Inter is built in");
let upm = inter.units_per_em();
assert!(upm != 1000.0, "advances finer than the file's widths: {upm}");
let widths: Vec<String> = (32u8..=126).map(|c| (inter.advance(inter.glyph_for(c as char)) * 1000.0 / upm).round().to_string()).collect();
let font = format!(
"<< /Type /Font /Subtype /TrueType /BaseFont /Inter-Regular /FirstChar 32 /LastChar 126 /Widths [{}] /Encoding /WinAnsiEncoding >>",
widths.join(" ")
);
let resources = format!("/Font << /IN {} 0 R >>", first_extra(1));
let content = "BT /IN 12 Tf 20 50 Td [(Next) -1000 (line here)] TJ ET";
let bytes = pdf_with(&[PdfPage { resources, ..PdfPage::new(300.0, 200.0, content) }], &[&font], None);
let t = texts(&open_as(&bytes, TextAs::Text));
assert_eq!(t.len(), 1);
let runs: Vec<&str> = t[0].runs.iter().map(|r| r.text.as_str()).collect();
assert_eq!(runs, ["Next", " ", "line here"], "{:?}", t[0].runs);
assert!(t[0].runs[0].style.tracking.abs() < 0.5 && t[0].runs[2].style.tracking.abs() < 0.5, "{:?}", t[0].runs);
assert!(t[0].runs[1].style.tracking > 500.0, "the em's gap on the space: {:?}", t[0].runs);
}
/// 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> {
+1
View File
@@ -130,6 +130,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
| Date | Change | Summary |
|---|---|---|
| 2026-10-23 | minor | PDF import: a line's spacing is measured from the file's own glyph widths, so type in a font the file doesn't carry, drawn with an installed font whose advances its widths round off (Helvetica on macOS: 2048 units an em), keeps one tracking value on its letters instead of a run per character (#722 follow-up); test with built-in Inter, independent of the fonts installed; scores and hours unchanged |
| 2026-10-22 | minor | EPS import: a CR LF ending after `readline` counts as one line ending (PostScript Language Reference §3.8), so counted metadata that follows it is no longer shifted by a byte and the editable vectors import instead of the preview; scores and estimates unchanged. |
| 2026-10-21 | minor | PDF export: flattening for PDF/X-1a and PDF/X-3 in a CMYK document draws the areas it rasterizes as ink amounts (a CMYK image), so a CMYK colour under an image with see-through pixels keeps its numbers, K included, instead of going through RGB and back (#2160); own-fixture test; scores and hours unchanged |
| 2026-10-21 | minor | EPS and `.ai` editing data: type with an art style of several fills or strokes, effects on a fill (Offset Path, Transform with copies, Drop Shadow, Gaussian Blur, Bas Relief), pattern and gradient fills, per-fill blend modes and a Warp comes in as type with that live appearance instead of its drawn look (outlines), the style read as a tree of filters; other objects keep the rule for plain styles. Point type whose frame has no matrix (written so by newer files) is read; such stories were left out. Where a styled object's drawn look lay, the page isn't held to look like VectorCraft's drawing of the style. `editingData: "always"` also opens an Illustrator EPS from its editing data. Own-fixture tests; a user's file used locally only; scores and hours unchanged |