From b6b1ec7300eb08220696269b336bb343458bfe86 Mon Sep 17 00:00:00 2001 From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:54:30 +0430 Subject: [PATCH 1/2] PDF import: keep a line's glyphs on course when the file's widths are a little off the font's advances Cause: since #3742 place_each measures a line's spacing from the file's own widths, so tracking is only the file's Tc/Tw/TJ. Where the widths are systematically off the installed font's advances (up to the 1% that still counts as the same font), those differences add up along a line and nothing made up for them. With no kerning and every tracking value 0, place_each returned the untracked layout early, without the 0.02 em placement check (CodeRabbit's review of #3742): a line of embedded Source Sans 3 with /Widths 0.8% wide opened 3.7 pt off. With the early return removed the check rejects the line and the line-wide fit takes over: one whole-thousandth tracking value with the font's kerning, 0.30 pt off (as before #3715). Before #3742 main placed it exactly but with a run per character (49 and 62 runs). Change: the early return is gone; every line goes through the check. Before it, on_course() keeps the file's spacing where the glyphs stay within 0.01 em of the file's; otherwise each stretch of the line takes one share after every character (the longest stretch one share keeps within 0.01 em), so ordinary text stays one run, and a line whose differences change course (30 m then 30 i) takes a run per stretch (2). Verification: new test a_long_line_of_widths_a_little_off_the_fonts_advances_lands_in_one_run (embedded Source Sans 3, widths x1.008 and x0.992, a kerned and an unkerned line): one run each, glyphs under 0.24 pt (0.02 em) from where the file draws them. Without on_course it fails after #3742, with and without the early return ("glyphs 0.300 pt"); before #3742 it fails on the run count. It replaces an Inter test drafted for the review: Inter isn't embedded there, so the file's outlines are the interpreter's stand-in stretched to the widths, not Inter, and the contours differ by 0.6 pt even with exact widths. tests_text_placement and text_becomes_point_type pass; cargo xtask ci: all 10 steps passed. Thanks to the CodeRabbit review on #3742 for the early-return finding. --- crates/pdf/src/import_text.rs | 70 +++++++++++++++++++++++--- crates/pdf/src/tests_text_placement.rs | 34 +++++++++++++ docs/file-format-parity.md | 2 +- 3 files changed, 99 insertions(+), 7 deletions(-) diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index 02b4204ea..7117a14be 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -623,7 +623,8 @@ fn restyled(t: &TextObject, kern: Option, own: &[f64]) -> Vec { /// 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> { 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, pens: &[Option<(f64, f64)>], target: &[Option<(f64, Option)>], size: f64) -> Vec { + 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) -> Option { v.sort_by(f64::total_cmp); diff --git a/crates/pdf/src/tests_text_placement.rs b/crates/pdf/src/tests_text_placement.rs index 1b9e2a29c..fc0057a90 100644 --- a/crates/pdf/src/tests_text_placement.rs +++ b/crates/pdf/src/tests_text_placement.rs @@ -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 = (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::>(), [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 { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index dd6f5d13b..ab70cd995 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -130,7 +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-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); where the widths are a little off the font's advances (up to the 1% that still counts as the same font) and the differences add up along a line, each stretch of the line takes their share after every character, so its glyphs stay within a hundredth of an em of the file's in a run per stretch (one on ordinary text), instead of a run per character, untracked type points off, or a line-wide fit tenths of a point off; tests with built-in Inter and embedded Source Sans 3, 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 | From fb74be021feedef6bccf8f07076e1bca921f8f34 Mon Sep 17 00:00:00 2001 From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:35:26 +0430 Subject: [PATCH 2/2] Docs: file-format-parity's status line carries the PDF import spacing change Review follow-up (CodeRabbit): the revision rows were dated 2026-10-23 but the status line still read 2026-10-21 and didn't mention the change. --- docs/file-format-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index ab70cd995..c5730d1a6 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (PDF text opens where the file draws it, #722; PDF/X-1a and PDF/X-3 flattening keeps a CMYK document's inks where it rasterizes, #2160; EPS and `.ai` editing data: type keeps stacked art styles as a live appearance; point type frames without a matrix read; `editingData: "always"` for EPS; PDF invisible text, as a scanned page's recognized text is, opens as type with no fill or stroke and is written back as invisible text, #2675; DXF circular cubic paths export as native CIRCLE/ARC where stroke semantics permit (R13–2018 for weighted strokes; R12 weighted strokes remain POLYLINE) (#1299); non-circular curves stay SPLINE; scores and hours unchanged; an Illustrator 3 file without layers, as image editors export their paths, opens with its art on one layer, storytold/photocraft#6036; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; batch exporting, #1935; legacy `.ai` runs as PostScript with a prolog of its own, #868; legacy `.ai` of version 7 saved, with type as text, hidden objects and an artboard picker, and its type read, #868; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: SVG `:not()` selectors, #1557, stacked selectors, #1703, linked files, #1870; DXF VPORT, #1789; nesting caps, #1852; hostile PDF, `.ai`, EPS, Affinity and native data, #1862, #1863, #1871–#1873; failed saves, #1685, #1693, #1694; type in symbols after reopening, #1738; scores unchanged; verified PngSuite downloads, #1729; EPS stitched shadings, #1653; oversized images in WMF/EMF refused from the header, #1776; PDF name trees with shared nodes, #1781; native size limits for files read from memory, #1783; private temporary files on Unix, #1778; ruzstd 0.9.1, #1807; scores unchanged; SVG import: CSS transforms and the `font` shorthand, #1565, percentage lengths in symbols, #1596, Inkscape flowed text and pages, #1568, #1601, filter warnings, #1598; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment in JPEG, TIFF and PSD, #1584; saves to folders that refuse the temporary file, #1620 in part; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (PDF import: a line's spacing is measured from the file's own glyph widths and kept within a hundredth of an em of where the file puts each glyph, so type opens as one run where it is placed; previously: PDF text opens where the file draws it, #722; PDF/X-1a and PDF/X-3 flattening keeps a CMYK document's inks where it rasterizes, #2160; EPS and `.ai` editing data: type keeps stacked art styles as a live appearance; point type frames without a matrix read; `editingData: "always"` for EPS; PDF invisible text, as a scanned page's recognized text is, opens as type with no fill or stroke and is written back as invisible text, #2675; DXF circular cubic paths export as native CIRCLE/ARC where stroke semantics permit (R13–2018 for weighted strokes; R12 weighted strokes remain POLYLINE) (#1299); non-circular curves stay SPLINE; scores and hours unchanged; an Illustrator 3 file without layers, as image editors export their paths, opens with its art on one layer, storytold/photocraft#6036; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; batch exporting, #1935; legacy `.ai` runs as PostScript with a prolog of its own, #868; legacy `.ai` of version 7 saved, with type as text, hidden objects and an artboard picker, and its type read, #868; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: SVG `:not()` selectors, #1557, stacked selectors, #1703, linked files, #1870; DXF VPORT, #1789; nesting caps, #1852; hostile PDF, `.ai`, EPS, Affinity and native data, #1862, #1863, #1871–#1873; failed saves, #1685, #1693, #1694; type in symbols after reopening, #1738; scores unchanged; verified PngSuite downloads, #1729; EPS stitched shadings, #1653; oversized images in WMF/EMF refused from the header, #1776; PDF name trees with shared nodes, #1781; native size limits for files read from memory, #1783; private temporary files on Unix, #1778; ruzstd 0.9.1, #1807; scores unchanged; SVG import: CSS transforms and the `font` shorthand, #1565, percentage lengths in symbols, #1596, Inkscape flowed text and pages, #1568, #1601, filter warnings, #1598; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment in JPEG, TIFF and PSD, #1584; saves to folders that refuse the temporary file, #1620 in part; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/mod.rs`). Illustrator's list comes from its public