mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user