From f043a1a392d9ca32f7e5bec0c750dbcfab153176 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 11 Oct 2026 06:58:57 +0700 Subject: [PATCH] #1032: spot colours defined in Lab keep their Lab values A .ai or PDF whose spot colours are defined in Lab opened them wrong: - the PDF part gives such an ink an ICC Lab profile as its alternate space ([/ICCBased profile], 3 components), which was read as RGB, so the Lab values were clamped into wildly wrong colours (PANTONE 249 C came in yellow). A profile whose Alternate is [/Lab ...], or whose header names the Lab data colour space, is now read as Lab; - the editing data writes them as `c m y k L a b (name) tint 2 Xx`, type 2 being Lab, and only the CMYK equivalent was read. Type 2 now gives the Lab colour, in the swatches and on the art. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/eps/src/import/ai/paint.rs | 29 ++++++++++++++++------ crates/pdf/src/import_color.rs | 37 +++++++++++++++++++++++----- crates/pdf/src/tests_import_color.rs | 30 ++++++++++++++++++++++ docs/file-format-parity.md | 5 ++-- 5 files changed, 87 insertions(+), 16 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 490733ef..ea0669c5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | Date | What landed | |---|---| -| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). PDF import keeps the style a font name says when the file doesn't embed the font (#1023). Shift selects ruler guides and art together, and Delete removes both (#1022). Open filled paths are hit by their fill on every side, the rotate cursor bows around the corner it is at, the flip icons match their flips, the Color Picker previews on the selection, and Properties › Transform reaches Scale Corners and Scale Strokes & Effects (#1036). Offset Path and the other booleans finish on outlines where the sweep used to loop until memory ran out (linesweeper 0.5, #1028). Floating panel groups stack into sets that move together, collapse to their tabs and dock as extra dock columns that scroll (#1020). Ruler guides live on layers: listed in the Layers panel, shown, hidden, locked, duplicated, merged and deleted with their layer, and moved between layers (#1021). A `.ai` whose editing data uses global process colours (`Xk`/`XK`) or pattern fills keeps its sublayers: the colours come in as global swatches at their tint, the patterns as pattern swatches (#1025). A `.ai` in PostScript form opens through its layers, read from the program itself, also when it names the prolog defining its operators without including it, as Rhino and other CAD apps write it (#1027). A fill or stroke chosen while the Type tool has characters selected colours just those characters (#1063). Scaled type shows its scaled font size, leading, baseline shift and horizontal scale in the Character panel, and a size typed there is the size you get (#1034). Character panel menu › Proportional Metrics sets Japanese and Chinese full-width glyphs on the font's proportional widths in horizontal type (#966). Align, Distribute, Move, Arrange, Lock, Hide, Ungroup and other Object commands that would change nothing no longer record an undo step or mark the document changed (#1060). | +| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). PDF import keeps the style a font name says when the file doesn't embed the font (#1023). Shift selects ruler guides and art together, and Delete removes both (#1022). Open filled paths are hit by their fill on every side, the rotate cursor bows around the corner it is at, the flip icons match their flips, the Color Picker previews on the selection, and Properties › Transform reaches Scale Corners and Scale Strokes & Effects (#1036). Offset Path and the other booleans finish on outlines where the sweep used to loop until memory ran out (linesweeper 0.5, #1028). Floating panel groups stack into sets that move together, collapse to their tabs and dock as extra dock columns that scroll (#1020). Ruler guides live on layers: listed in the Layers panel, shown, hidden, locked, duplicated, merged and deleted with their layer, and moved between layers (#1021). A `.ai` whose editing data uses global process colours (`Xk`/`XK`) or pattern fills keeps its sublayers: the colours come in as global swatches at their tint, the patterns as pattern swatches (#1025). A `.ai` in PostScript form opens through its layers, read from the program itself, also when it names the prolog defining its operators without including it, as Rhino and other CAD apps write it (#1027). A fill or stroke chosen while the Type tool has characters selected colours just those characters (#1063). Scaled type shows its scaled font size, leading, baseline shift and horizontal scale in the Character panel, and a size typed there is the size you get (#1034). Character panel menu › Proportional Metrics sets Japanese and Chinese full-width glyphs on the font's proportional widths in horizontal type (#966). Align, Distribute, Move, Arrange, Lock, Hide, Ungroup and other Object commands that would change nothing no longer record an undo step or mark the document changed (#1060). Spot colours defined in Lab open with their Lab values from `.ai` and PDF files, instead of as wrong RGB colours or their CMYK equivalents (#1032). | | 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) | | 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day | | 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections | diff --git a/crates/eps/src/import/ai/paint.rs b/crates/eps/src/import/ai/paint.rs index af6f86e2..783e7e44 100644 --- a/crates/eps/src/import/ai/paint.rs +++ b/crates/eps/src/import/ai/paint.rs @@ -2,10 +2,10 @@ //! //! - Colours: `g`/`G` grey (1 is white), `k`/`K` CMYK, `x`/`X` a named CMYK colour at a tint //! (`c m y k (name) tint`; tint 0 is the full colour), `Xa`/`XA` RGB (`c m y k r g b`, the CMYK -//! values its equivalent), `Xx`/`XX` a named colour of either model -//! (`c m y k [r g b] (name) tint type`), `Xk`/`XK` a global process colour (its operands as -//! `Xx`'s: a global swatch that isn't an ink of its own). Lower case paints fills, upper case -//! strokes. +//! values its equivalent), `Xx`/`XX` a named colour of any model +//! (`c m y k [r g b | L a b] (name) tint type`, type 0 CMYK, 1 RGB, 2 Lab), `Xk`/`XK` a global +//! process colour (its operands as `Xx`'s: a global swatch that isn't an ink of its own). Lower +//! case paints fills, upper case strokes. //! - Gradient definitions: `(name) type stops Bd`, its stops as `colour style [opacity 6] //! midpoint ramp Bs` (style 0 grey, 1 CMYK, 2 RGB with its CMYK, 3 and 4 named colours), `BD`. //! - A gradient on an object: `[flag] Bb` (1: the stroke), `flag (name) x y angle length a b c d @@ -57,8 +57,8 @@ pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option)> { } } -/// `c m y k (name) tint x` or `c m y k [r g b] (name) tint type Xx` (type 1: RGB); `rgb` forces -/// the model. +/// `c m y k (name) tint x` or `c m y k [r g b | L a b] (name) tint type Xx` (type 1: RGB, 2: Lab); +/// `rgb` forces RGB or CMYK. fn named(vals: &[V], rgb: Option) -> Option<(Paint, Option)> { let at = vals.iter().rposition(|v| matches!(v, V::Str(_)))?; let name = vals.get(at)?.text()?; @@ -67,9 +67,14 @@ fn named(vals: &[V], rgb: Option) -> Option<(Paint, Option)> { let unit = |v: f32| v.clamp(0.0, 1.0); let tint = 1.0 - unit(*after.first()?); let n = before.len(); - let color = if rgb.unwrap_or(after.get(1).is_some_and(|t| *t == 1.0)) { + let kind = after.get(1).copied(); + let color = if rgb.unwrap_or(kind == Some(1.0)) { let [r, g, b] = before.get(n.checked_sub(3)?..)? else { return None }; Color::rgb(unit(*r), unit(*g), unit(*b)) + } else if rgb.is_none() && kind == Some(2.0) { + // Type 2: a colour defined in Lab, its L a b values after the CMYK equivalent (#1032). + let [l, a, b] = before.get(n.checked_sub(3)?..)? else { return None }; + Color::lab(l.clamp(0.0, 100.0), a.clamp(-128.0, 127.0), b.clamp(-128.0, 127.0)) } else { // The CMYK values come first (an RGB equivalent may follow them). let from = if n >= 7 { n - 7 } else { n.checked_sub(4)? }; @@ -266,6 +271,16 @@ mod tests { v.extend([V::Str(b"Orange".to_vec()), V::Num(0.0), V::Num(1.0)]); let (p, _) = color_op("Xk", &v).unwrap(); assert_eq!(p.color(), Some(Color::rgb(1.0, 0.5, 0.0))); + // A spot colour defined in Lab (type 2): its L a b values, not its CMYK equivalent (#1032). + let mut v = nums(&[0.52, 0.98, 0.28, 0.1, 32.9412, 45.0, -18.0]); + v.extend([V::Str(b"Plum Ink".to_vec()), V::Num(0.0), V::Num(2.0)]); + let (p, named) = color_op("Xx", &v).unwrap(); + let named = named.unwrap(); + assert_eq!((named.color, named.spot, named.tint), (Color::lab(32.9412, 45.0, -18.0), true, 1.0)); + assert_eq!(p.color(), Some(Color::lab(32.9412, 45.0, -18.0))); + v[8] = V::Num(0.25); + let (_, named) = color_op("XX", &v).unwrap(); + assert_eq!(named.map(|n| (n.color, n.tint)), Some((Color::lab(32.9412, 45.0, -18.0), 0.75))); } #[test] diff --git a/crates/pdf/src/import_color.rs b/crates/pdf/src/import_color.rs index c4966fbe..f38eadb1 100644 --- a/crates/pdf/src/import_color.rs +++ b/crates/pdf/src/import_color.rs @@ -389,12 +389,16 @@ fn alt_model(o: &Object<'_>) -> Option { Object::Array(a) => { let mut it = a.flex_iter(); match it.next::>()?.as_str() { - "ICCBased" => match it.next::>()?.dict().get::(b"N")? { - 1 => Some(Alt::Gray), - 3 => Some(Alt::Rgb), - 4 => Some(Alt::Cmyk), - _ => None, - }, + "ICCBased" => { + let profile = it.next::>()?; + match profile.dict().get::(b"N")? { + 1 => Some(Alt::Gray), + 3 if lab_profile(&profile) => Some(Alt::Lab), + 3 => Some(Alt::Rgb), + 4 => Some(Alt::Cmyk), + _ => None, + } + } "Lab" => Some(Alt::Lab), "CalRGB" => Some(Alt::Rgb), "CalGray" => Some(Alt::Gray), @@ -406,6 +410,27 @@ fn alt_model(o: &Object<'_>) -> Option { } } +/// Is the ICC profile of an `ICCBased` space a Lab one (its alternate space `Lab`, else its header's +/// data colour space `Lab `)? Spot colours defined in Lab come with such a profile as their +/// alternate space (#1032), and their tint transforms give Lab values. +fn lab_profile(profile: &Stream<'_>) -> bool { + use std::io::Read; + if let Some(Object::Array(a)) = profile.dict().get::>(b"Alternate") { + return a.flex_iter().next::>().is_some_and(|n| n.as_str() == "Lab"); + } + // The header's first 20 bytes, without decoding the rest of the profile. + let raw = profile.raw_data(); + let mut head = vec![]; + match profile.filters().as_slice() { + [] => head.extend(raw.iter().take(20)), + [hayro_syntax::Filter::FlateDecode] => { + let _ = flate2::read::ZlibDecoder::new(&*raw).take(20).read_to_end(&mut head); // a damaged profile reads as not Lab + } + _ => return false, + } + head.get(16..20) == Some(b"Lab ") +} + /// An alternate-space colour (the tint transform's output) as a VectorCraft colour. fn alt_color(alt: Alt, v: &[f32]) -> Option { let c = |i: usize| v.get(i).copied().filter(|x| x.is_finite()); diff --git a/crates/pdf/src/tests_import_color.rs b/crates/pdf/src/tests_import_color.rs index 4a0acc4d..d9d690ce 100644 --- a/crates/pdf/src/tests_import_color.rs +++ b/crates/pdf/src/tests_import_color.rs @@ -215,3 +215,33 @@ fn colour_spaces_are_read_from_the_interpreters_debug_form() { assert_eq!((space, comps), ("ColorSpace(DeviceCmyk)", vec![0.101960786, 0.2, 0.3, 0.4])); assert_eq!(crate::import_color::split_color("Color { color_space: X, components: [NaN], opacity: 1.0 }"), None); } + +/// Spot colours defined in Lab can have an ICC Lab profile as their alternate space, `[/ICCBased +/// profile]` with 3 components: the tint transform gives Lab values, which are not RGB (#1032). The +/// profile is told by its `Alternate` (`[/Lab …]`), else by its header's data colour space. +#[test] +fn spot_colours_with_an_icc_lab_alternate_keep_their_lab_values() { + let f = first_extra(1); + // A profile's first 20 bytes: size, CMM, version, class, then the data colour space. + let profile = |head: &str, alternate: &str| format!("<< /N 3 {alternate} /Length {} >>\nstream\n{head}\nendstream", head.len()); + let lab_header = profile("0000none0000spacLab XYZ ", ""); + let lab_alternate = profile("0000none0000spacXYZ XYZ ", "/Alternate [/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-128 127 -128 127] >>]"); + let rgb = profile("0000none0000mntrRGB XYZ ", ""); + let tint = |c1: &str| format!("<< /FunctionType 2 /Domain [0 1] /C0 [100 0 0] /C1 [{c1}] /N 1 >>"); + let spaces = format!( + "/CS0 [/Separation /Plum [/ICCBased {f} 0 R] {}] /CS1 [/Separation /Sky [/ICCBased {} 0 R] {}] /CS2 [/Separation /Leafy [/ICCBased {} 0 R] << /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [0.5 0.9 0.1] /N 1 >>]", + tint("32.9412 45 -18"), + f + 1, + tint("51 -26 -45"), + f + 2 + ); + let page = page_with(&spaces, &[("CS0", "1"), ("CS1", "0.5"), ("CS2", "1")]); + let d = import(&pdf_with(&[page], &[&lab_header, &lab_alternate, &rgb], None)).unwrap(); + let f = fills(&d); + assert_eq!(solid(&f[0]).1, Some("Plum")); + assert!(near(d.swatch("Plum").unwrap().paint.color().unwrap(), Color::lab(32.941, 45.0, -18.0)), "{:?}", d.swatch("Plum")); + assert_eq!((solid(&f[1]).1, solid(&f[1]).2), (Some("Sky"), 0.5)); + assert!(near(d.swatch("Sky").unwrap().paint.color().unwrap(), Color::lab(51.0, -26.0, -45.0)), "{:?}", d.swatch("Sky")); + // An RGB profile stays RGB. + assert!(near(d.swatch("Leafy").unwrap().paint.color().unwrap(), Color::rgb(0.5, 0.9, 0.1)), "{:?}", d.swatch("Leafy")); +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 6830f7fe..2adb0684 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-10 · **Change:** major (first checklist, from the `FORMATS` table and Illustrator 2026's documented formats) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Lab spot colours in `.ai` and PDF, #1032) · **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 @@ -44,7 +44,7 @@ private editing data. VectorCraft: ### Illustrator's private editing data -the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths, clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, global process colours as global swatches at their tint), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), opacity, blend modes, isolation and knockout, embedded images with their alpha channel, guides, and every artboard where it is. An object with several fills or strokes, effects or a brush comes in as its drawn look (a group named after it). Type is made from the file's text document, whole and editable, shown or not: point type, area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing; the page's drawing of it (often in pieces) is left out. Where the text document can't say (a frame of another kind, or a document that doesn't agree with the page), type that shows comes from the page into its text object's place (no group around it), and hidden type is left out with a note. Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. Symbols, pattern fills whose pattern it can't read, placed files and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5%, where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. +the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths, clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), opacity, blend modes, isolation and knockout, embedded images with their alpha channel, guides, and every artboard where it is. An object with several fills or strokes, effects or a brush comes in as its drawn look (a group named after it). Type is made from the file's text document, whole and editable, shown or not: point type, area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing; the page's drawing of it (often in pieces) is left out. Where the text document can't say (a frame of another kind, or a document that doesn't agree with the page), type that shows comes from the page into its text object's place (no group around it), and hidden type is left out with a note. Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. Symbols, pattern fills whose pattern it can't read, placed files and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5%, where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. ## Format by format @@ -80,6 +80,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Spot colours defined in Lab keep their Lab values, from the editing data and from a PDF part whose ink alternate is an ICC Lab profile (#1032) | | 2026-10-11 | minor | `.ai` in PostScript form opens through its layers, also without its prolog (#1027) | | 2026-10-11 | minor | `.ai` editing data: global process colours and pattern fills read, so they no longer drop sublayers (#1025) | | 2026-10-10 | major | First checklist; moved the `.ai` editing-data scope from the ROADMAP's "Out of scope" here; added the user-reported `.ai` failures |