diff --git a/crates/doc/src/node/attributes.rs b/crates/doc/src/node/attributes.rs index 42e41654f..6f573a61a 100644 --- a/crates/doc/src/node/attributes.rs +++ b/crates/doc/src/node/attributes.rs @@ -27,6 +27,10 @@ pub struct ObjectAttributes { /// the element's event attributes; nothing in VectorCraft runs them. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub events: Vec<(String, String)>, + /// The CSS classes the object had in an imported SVG, written back to `class` on export + /// (beside the generated `cls-N` classes). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub classes: Vec, /// Attributes in other applications' XML namespaces, in order, kept from SVG import to SVG /// export: what another tool notes on an object (a plotter's pen, a cutter's passes) survives /// a round trip through VectorCraft. Nothing in VectorCraft reads them. They are the object's, diff --git a/crates/svg/src/export/output.rs b/crates/svg/src/export/output.rs index 01d33c833..fb06f8005 100644 --- a/crates/svg/src/export/output.rs +++ b/crates/svg/src/export/output.rs @@ -121,7 +121,7 @@ impl Writer<'_> { pub(super) fn line(&mut self, s: &str) { let ind = self.indent(self.depth); self.body.push_str(&ind); - self.body.push_str(s); + self.body.push_str(&merge_class_attrs(s)); if !self.opts.minify { self.body.push('\n'); } @@ -202,6 +202,19 @@ impl Writer<'_> { if !out.is_empty() && n.is_layer() && self.layer_nest > 1 { out.push_str(&format!(" {}=\"sublayer\"", crate::import::SUBLAYER)); } + let classes: Vec<&str> = n + .extras() + .attrs + .as_deref() + .map_or(&[][..], |a| a.classes.as_slice()) + .iter() + .map(String::as_str) + .filter(|c| !c.is_empty() && !c.chars().any(|ch| ch.is_control() || matches!(ch, '\u{fffe}' | '\u{ffff}'))) + .collect(); + // SVG Tiny 1.2 has `class` among its core attributes, so every profile writes them. + if !classes.is_empty() { + out.push_str(&format!(" class=\"{}\"", xml_attr_escape(&classes.join(" ")))); + } let mut keys = HashSet::new(); for (k, v) in n.extras().attrs.as_deref().map_or(&[][..], |a| a.data.as_slice()) { // Ours (`name`, `vc-…`) are written by the export itself. @@ -515,6 +528,28 @@ impl Writer<'_> { } } +/// An element that has both its own classes and a generated `cls-N` would write `class` twice: +/// join the two into one attribute (the first, the object's own, comes first). +fn merge_class_attrs(s: &str) -> std::borrow::Cow<'_, str> { + const KEY: &str = " class=\""; + let tag_end = s.find('>').unwrap_or(s.len()); + let tag = s.get(..tag_end).unwrap_or(s); + let Some(first) = tag.find(KEY) else { return s.into() }; + let first_val = first + KEY.len(); + let Some(first_len) = tag.get(first_val..).and_then(|t| t.find('"')) else { return s.into() }; + let first_end = first_val + first_len; + let Some(second) = tag.get(first_end..).and_then(|t| t.find(KEY)).map(|i| i + first_end) else { return s.into() }; + let second_val = second + KEY.len(); + let Some(second_len) = tag.get(second_val..).and_then(|t| t.find('"')) else { return s.into() }; + let second_end = second_val + second_len; + let (Some(b), Some(head), Some(mid), Some(rest)) = + (s.get(second_val..second_end), s.get(..first_end), s.get(first_end + 1..second), s.get(second_end + 1..)) + else { + return s.into(); + }; + format!("{head} {b}\"{mid}{rest}").into() +} + /// The rectangle whose corners `q` are, in order either way round, with its edges alternately /// horizontal and vertical and none of zero length (`` doesn't draw an empty one). fn axis_rect(q: &[Point]) -> Option { diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index fbc4c2836..a70d750cc 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -40,8 +40,8 @@ pub(super) use layers::DEFAULT_FONT_SIZE; pub(crate) use layers::SUBLAYER; use layers::{RootUnits, inkscape_pages, promote_sublayers, root_layer_name}; pub(super) use prepass::Edits; +use prepass::{Extras, labels, prepass}; pub use prepass::{MAX_FOREIGN, MAX_FOREIGN_VALUE}; -use prepass::{labels, prepass}; mod css; mod css_transform; @@ -323,7 +323,7 @@ struct Importer { /// Ids of the shapes whose strokes don't scale ([`vector_effect`]). non_scaling: HashSet, /// The `data-*` attributes of elements, by id ([`user_data`]). - data: HashMap>, + data: HashMap, /// The event attributes of elements, by id ([`user_events`]). events: HashMap>, /// The attributes of elements in other applications' namespaces, by id ([`foreign`]). diff --git a/crates/svg/src/import/importer.rs b/crates/svg/src/import/importer.rs index 55cf0aa91..2f8843220 100644 --- a/crates/svg/src/import/importer.rs +++ b/crates/svg/src/import/importer.rs @@ -87,12 +87,16 @@ impl Importer { Some(out) } - /// Give `n` the `data-*` attributes of element `id`, unless it has data already (what an + /// Give `n` the `data-*` attributes and classes of element `id`, unless it has data already (what an /// inner element made of it keeps). pub(super) fn keep_data(&self, id: &str, n: &mut Node) { - if let Some(data) = self.data.get(id).filter(|_| n.extras().attrs.as_ref().is_none_or(|a| a.data.is_empty())) { + let Some((data, classes)) = self.data.get(id) else { return }; + if n.extras().attrs.as_ref().is_none_or(|a| a.data.is_empty()) { n.edit_attrs(|a| a.data = data.clone()); } + if n.extras().attrs.as_ref().is_none_or(|a| a.classes.is_empty()) { + n.edit_attrs(|a| a.classes = classes.clone()); + } } /// Give `n` the event attributes of element `id`, unless it has events already (what an inner diff --git a/crates/svg/src/import/prepass.rs b/crates/svg/src/import/prepass.rs index 3f52c4e35..95c0d5720 100644 --- a/crates/svg/src/import/prepass.rs +++ b/crates/svg/src/import/prepass.rs @@ -21,7 +21,7 @@ pub(super) struct Found { /// Ids of the shapes whose strokes don't scale ([`vector_effect`]). pub(super) non_scaling: HashSet, /// The `data-*` attributes of elements, by id ([`user_data`]). - pub(super) data: HashMap>, + pub(super) data: HashMap, /// The event attributes of elements, by id ([`user_events`]). pub(super) events: HashMap>, /// The attributes of elements in other applications' namespaces, by id ([`foreign`]). @@ -190,7 +190,7 @@ pub(super) fn prepass<'s>( && !events && !foreign_attrs && !ellipses - && !["( found.links.insert(edits.id(svg, a), url.to_string()); } } - if markers.contains("data-") { + if markers.contains("data-") || markers.contains("class") { for e in elements().filter(|e| OBJECT_ELEMENTS.contains(&e.tag_name().name())) { let data = user_data(e); - if !data.is_empty() { - found.data.insert(edits.id(svg, e), data); + let classes = user_classes(e); + if !data.is_empty() || !classes.is_empty() { + found.data.insert(edits.id(svg, e), (data, classes)); } } } @@ -321,6 +322,26 @@ fn tag_name_end(svg: &str, n: XNode) -> usize { svg.get(start + 1..).and_then(|s| s.find(|c: char| c.is_whitespace() || c == '/' || c == '>')).map_or(start + 2, |i| start + 1 + i) } +/// What import keeps of an element's own attributes: its `data-*` attributes and CSS classes. +pub(super) type Extras = (Vec<(String, String)>, Vec); + +/// The most CSS classes kept per element (the file is untrusted). +const MAX_CLASSES: usize = 64; + +/// The CSS classes of element `e`, in order and without repeats. Classes our own export +/// generated (`cls-1`, with or without a prefix) aren't kept: export makes them again. A class list +/// is split on SVG's five ASCII whitespace characters only, so a no-break space stays inside a name. +fn user_classes(e: XNode) -> Vec { + let generated = |c: &str| c.rsplit_once("cls-").is_some_and(|(_, n)| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())); + let mut out: Vec = Vec::new(); + for c in e.attribute("class").into_iter().flat_map(|v| v.split(['\u{20}', '\t', '\n', '\u{c}', '\r']).filter(|c| !c.is_empty())) { + if out.len() < MAX_CLASSES && !generated(c) && !out.iter().any(|k| k == c) { + out.push(c.to_string()); + } + } + out +} + /// The elements that become objects, whose `data-*` attributes are kept. const OBJECT_ELEMENTS: &[&str] = &["g", "a", "use", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "image"]; diff --git a/crates/svg/tests/classes.rs b/crates/svg/tests/classes.rs new file mode 100644 index 000000000..b3ae4622e --- /dev/null +++ b/crates/svg/tests/classes.rs @@ -0,0 +1,45 @@ +//! CSS classes on imported elements come back on export, beside the generated `cls-N` classes. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +use usvg::roxmltree; +use vectorcraft_svg::{ExportOptions, Profile, Styling, export, import}; + +const SRC: &str = r#""#; + +fn class_of(svg: &str, id: &str) -> Option { + let doc = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }).unwrap(); + let n = doc.descendants().find(|n| n.attribute("id") == Some(id))?; + n.attribute("class").map(str::to_string) +} + +#[test] +fn imported_classes_survive_every_styling_with_generated_ones() { + let doc = import(SRC).unwrap(); + for styling in [Styling::PresentationAttributes, Styling::InlineStyle, Styling::InternalCss, Styling::StyleEntities] { + let out = export(&doc, &ExportOptions { styling, ..ExportOptions::default() }); + assert_eq!(class_of(&out, "grp").as_deref(), Some("wrap big"), "{styling:?}"); + let rect = class_of(&out, "r").unwrap(); + if styling == Styling::InternalCss { + assert!(rect.starts_with("hero ") && rect.contains("cls-") && !rect.contains("cls-7"), "{rect}"); + } else { + assert_eq!(rect, "hero"); + } + assert!(class_of(&out, "c").is_none_or(|c| c.starts_with("cls-"))); + // A second round trip changes nothing. + let again = export(&import(&out).unwrap(), &ExportOptions { styling, ..ExportOptions::default() }); + assert_eq!(class_of(&again, "r"), Some(rect), "{styling:?}"); + } +} + +#[test] +fn class_names_with_colons_slashes_and_no_break_spaces_are_kept() { + let src = ""; + let out = export(&import(src).unwrap(), &ExportOptions::default()); + assert_eq!(class_of(&out, "r").as_deref(), Some("hover:bg-blue w-1/2 a\u{a0}b")); +} + +#[test] +fn the_tiny_profile_keeps_imported_classes() { + let out = export(&import(SRC).unwrap(), &ExportOptions { profile: Profile::Tiny12, ..ExportOptions::default() }); + assert_eq!(class_of(&out, "grp").as_deref(), Some("wrap big"), "{out}"); + assert_eq!(class_of(&out, "r").as_deref(), Some("hero"), "{out}"); +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 805bbc63a..7c8a4d227 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-11-06 · **Change:** minor (EPS and `.ai` editing data: a transparency set inside a clipping group stays after its `Q`; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS and `.ai` editing data: a transparency set inside a clipping group stays after its `Q`; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **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/formats.rs`). Illustrator's list comes from its public @@ -340,6 +340,7 @@ A Scatter brush in the editing data whose last 15 numbers start with 1 comes int | 2026-10-23 | minor | SVG: attributes in other applications' XML namespaces (`plotter:pen="2"` with `xmlns:plotter`) are kept on the objects and layers they belong to, saved with them in `.vectorcraft` files and written back on export, each namespace declared once on the root under the file's prefix where it is free. SVG's, XLink's, XML's, Inkscape's and Sodipodi's own aside; at most 64 per object of at most 64 KiB each, with a warning for what is left out (the rest of #522). 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 | SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts. 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 | | 2026-10-18 | minor | PDF import: invisible text (rendering mode 3, which carries a scanned page's recognized text) opens as type with no fill or stroke instead of being dropped without a word; when it can't be type (text opened as outlines, Type 3 fonts) it is left out with a warning. PDF export writes characters with no fill or stroke as invisible text in rendering mode 3, so a scanned PDF opened and saved keeps its searchable text, in PDF/X-1a and PDF/A too (#2675); own-fixture tests; scores and hours unchanged | @@ -350,6 +351,7 @@ A Scatter brush in the editing data whose last 15 numbers start with 1 comes int | 2026-10-16 | minor | 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. Synthetic EPS/parser/minification regressions; user EPS/SVG/JPG kept local. | | 2026-10-16 | minor | Batch exporting in Export for Screens (#1935): a file name pattern (document and item names, a counter, the date), folders per format, item or both, Layers and Selection tabs with thumbnails, EPS, AI and VectorCraft rows, a PDF row with printer's marks, and export presets (built-in Logo Package and Print-Ready PDF, saved ones); scores and hours unchanged (estimated) | | 2026-10-16 | minor | `.ai` editing data: a rotated embedded image lands where its raster matrix puts it, not turned 180° around its corner; the legacy version 7 writer and its prolog write and draw rotated images with the same matrix (#2269). Scores unchanged | +| 2026-10-16 | minor | SVG: CSS classes on imported elements (`class="hero"`) are kept on the object and written back on export, beside the generated `cls-N` classes (#2414); scores unchanged (estimated) | | 2026-10-14 | minor | Legacy `.ai` of version 7 runs as a PostScript program: its prolog is a procedure set of VectorCraft's own that gives each operator the writer uses its meaning (paths, colours, compound paths, clipping groups, layers that don't print, images; gradients flat, type not set), and the page trailer's `annotatepage` (§3 of the specification) marks nothing; every file the writer's tests write is run through VectorCraft's PostScript reader and must draw what the document it was written from draws (#868) | | 2026-10-14 | minor | SVG import: `:not()` selectors (#1799, #1557), stacked descendant selectors over deep nesting no longer stall (#1834, #1703), linked files within the automation roots again (#1870); DXF export writes the VPORT view direction once (#1789); nesting capped at 32 objects with self-using symbols and patterns opened without the offending instance (#1852); deep and self-referencing PDF and `.ai` data (#1871), decompression bombs (#1873), self-drawing brushes (#1872), hostile EPS, Affinity and native data (#1863) and huge ids (#1862) handled; a failed save is reported in a dialog and the old file is kept (#1865, #1685, #1693, #1694); type inside symbols gets its layout bounds back on opening a native file (#1833, #1738). Scores unchanged (estimated) | | 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | diff --git a/docs/roadmap.md b/docs/roadmap.md index d01c002ae..e1a7d3360 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -123,7 +123,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M2 | Drawing tools + smart guides | ✅ mostly done (Polygon/Star exact-size count persistence, with repeated-drag, tool-switch and Cancel regression coverage; Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending); Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu | 12–18 | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit; finite-arithmetic follow-up regressions and full CI passed ([Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area))), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls | 4–8 | | M2 | Drawing tools + smart guides | ✅ mostly done (Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending) | 12–18 | -| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish | 3–6 (existing scope; Affinity export unestimated) | +| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts | 3–6 (existing scope; Affinity export unestimated) | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default | 4–8 | | M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); opaque solid-fill knockout paths draw once in hard-edge/supersampled raster export, with analytical-AA compositing unchanged; file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 | | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (live compound shapes: Make, Alt-click a Shape Mode, member modes, Release, Expand (#4522); single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; the slider opens at the Auto-Simplify level; a Direct Selection part of a path simplifies on its own); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open); Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog | 3–6 | @@ -224,6 +224,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. | | 2026-10-21 | minor | Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | | 2026-10-21 | minor | Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | +| 2026-10-21 | minor | SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts. Scores and estimates unchanged. | | 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged | | 2026-10-20 | minor | Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; existing opening gestures retained; scores and hours unchanged. | | 2026-10-20 | minor | M12: consolidated three duplicate milestone rows into one, retaining distinct Image Trace and Graphs details and the quick-preset wording correction; status, scores and 6–10 h estimate unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 31dc47a26..ad1ecc921 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -202,7 +202,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | | File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path comes from VectorCraft's font metrics, and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged) | | Guides, grids, smart guides, snapping, rulers | 3 | 80% | "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658), distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved); new ruler-guide Alt orientation implemented; video rulers (View › Rulers › Show Video Rulers, #2849) done, score unchanged | @@ -1949,6 +1949,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. | | 2026-10-21 | minor | Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | | 2026-10-21 | minor | Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | +| 2026-10-21 | minor | SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts. Scores and estimates unchanged. | | 2026-10-21 | minor | Snap to Glyph (#3124) removed from View navigation missing lists and menu stubs; scores and hours unchanged at this rounding | | 2026-10-21 | minor | Type core: System Layout (Character panel menu, whole document, off by default) is done; scores and hours unchanged | | 2026-10-22 | minor | Ruby (ふりがな) for live text: `type.ruby` sets/removes a reading over a byte range, above or below (right of the column in vertical type), aligned, sized and overhanging or squeezed; saved natively, laid out with line growth, drawn on the canvas and in SVG/PDF (outlines where ruby is present); scores and hours unchanged |