PDF import: even gaps alone still read as word spaces

Glyphs placed one by one with even gaps could be letters set apart or
words of one letter each, and a file's positions can't tell them apart.
The spacing a line's letters share is now taken off its gaps only when
the line also shows where its words break: with spaces it draws (as
with Tc), or with gaps wider than the rest (tracked words placed glyph
by glyph). Otherwise every gap past a fifth of an em is a space, as
before, so A, B, C and D placed with ordinary word spaces between them
still come in as "A B C D".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
KnottyDyes
2026-10-22 00:10:57 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent b024756e9f
commit 73cbba8b8d
3 changed files with 32 additions and 5 deletions
+11 -4
View File
@@ -385,13 +385,20 @@ impl TextLine {
let mut t = TextObject::point(Point::ORIGIN, &first.text, first.style);
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.
// space (one set at exactly a fifth still counts, whatever the rounding). Letter spacing is
// read only in text left in the file's order.
let space = |gap: &Gap, common: f64| gap.width / gap.em - common > 0.199;
let directed = vectorcraft_text::logical_order(&t.plain_text()).is_some();
let common = if directed { 0.0 } else { common_gap(&self.gaps) };
let mut common = if directed { 0.0 } else { common_gap(&self.gaps) };
// Evenly spaced glyphs alone could be letters set apart or words of one letter each: the
// shared spacing is letter spacing only on a line that also shows where its words break,
// with spaces it draws or with gaps wider than the rest.
if self.drawn_spaces.is_empty() && !self.gaps.iter().any(|gap| space(gap, common)) {
common = 0.0;
}
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.
if gap.width / gap.em - common <= 0.199 {
if !space(gap, common) {
continue;
}
// After the earlier spaces, at the end of the run before it (the run it follows).
+20
View File
@@ -127,6 +127,26 @@ fn common_tracking_does_not_turn_gaps_into_spaces() {
}
}
/// Words of one letter each, placed one by one with ordinary word spaces between them, are words:
/// even gaps alone don't read as letter spacing.
#[test]
fn evenly_spaced_one_letter_words_keep_their_spaces() {
let font = vectorcraft_text::FontDb::global().find_postscript("SourceSans3-Regular").unwrap();
let size = 12.0_f32;
for gap in [0.25_f32, 0.3, 0.35] {
let mut x = 20.0;
let mut lines = vec![];
for text in ["A", "B", "C", "D"] {
lines.push((x, 60.0, size, text));
x += font.advance(font.glyph_for(text.chars().next().unwrap())) as f32 / font.units_per_em() as f32 * size + gap * size;
}
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(), "A B C D", "gap {gap} em");
}
}
/// 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]
+1 -1
View File
@@ -784,7 +784,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; art reaching past a plain PDF's page clipped to its crop box, as PDF viewers show it, each such object in a clip group of its own (a PDF-compatible `.ai` opens whole: #675); a `.ai` on its trim boxes with the bleed boxes round them as the document's bleed, as Crop To › Trim opens a PDF (#1266); masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, an `.ai`'s white page under its layers left out rather than coming in as art, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). Overprint is kept from PDF import (opened, the PDF part of an `.ai`, placed) to PDF export: the graphics state's `/OP` and `/op` become the overprint of fills, strokes and type, so a spot colour over a process colour still overprints; a page whose overprint can't be read for certain knocks out with a note, as does `/OPM 0` being read as nonzero overprint (#1388; hand-written fixtures, `pdf/src/tests_import_overprint.rs`). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; art reaching past a plain PDF's page clipped to its crop box, as PDF viewers show it, each such object in a clip group of its own (a PDF-compatible `.ai` opens whole: #675); a `.ai` on its trim boxes with the bleed boxes round them as the document's bleed, as Crop To › Trim opens a PDF (#1266); masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, an `.ai`'s white page under its layers left out rather than coming in as art, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs and the images in an `.ai` or EPS file's editing data, an alpha channel coming in as a grayscale opacity mask) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). Overprint is kept from PDF import (opened, the PDF part of an `.ai`, placed) to PDF export: the graphics state's `/OP` and `/op` become the overprint of fills, strokes and type, so a spot colour over a process colour still overprints; a page whose overprint can't be read for certain knocks out with a note, as does `/OPM 0` being read as nonzero overprint (#1388; hand-written fixtures, `pdf/src/tests_import_overprint.rs`). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.
In imported PDF type, a space glyph the file draws between two words also keeps the next word where the file has it when the gap beside the space is wide, as VectorCraft's own export draws a tracked space: the space gets tracking of its own, and the wrapped lines of a justified paragraph still come back as one area type object when the word gaps on each line are alike (part of #2384).
Letter-spaced PDF type, whether set with character spacing (`Tc`) or glyph by glyph, keeps its word spaces and gains none between its letters: the spacing a line's letters share is taken off each gap before a gap reads as a space. Glyphs set more than half an em apart, such as figures in columns, keep their spaces.
Letter-spaced PDF type, whether set with character spacing (`Tc`) or glyph by glyph, keeps its word spaces and gains none between its letters: the spacing a line's letters share is taken off each gap before a gap reads as a space. That holds on a line that also shows where its words break, with spaces the file draws or gaps wider than the rest; evenly spaced glyphs alone, which could be words of one letter, keep every gap as a space, as do glyphs set more than half an em apart, such as figures in columns.
SVG import's style cascade, used for hidden objects, non-scaling strokes and type, tests each element only against the `<style>` rules for its id, its classes and its element name, and the rules whose selector ends in none of these (`*`, `[x]`).
A `.svgz` file copied in a file manager pastes as its art, as a copied `.svg` file does.
In a `.ai` or EPS opened from its editing data, type has its text object's opacity and blend mode, whether it is made from the file's text document or taken from the page. Where such type has transparency and comes in with other art (the rest of its look, or page type and strokes that go with it), a group of them has it. Where the page draws its type, or the strokes around it, with transparency that is neither its text object's nor its text object's combined with that of the objects around it, the page's type and strokes keep their own instead (`crates/eps/src/import/tests_native.rs`). Where the page draws type of no text object over text objects made from the text document, that type is left out only when, for each piece of it, one of those text objects has the piece's Latin letters and digits in its text, in capitals or not; other such type is kept as the page's other type of no text object is.