From 500ff6ed4c192d3635e816889f20e655cb94c3da Mon Sep 17 00:00:00 2001 From: M Balazs PC Date: Sat, 7 Nov 2026 03:31:58 +0100 Subject: [PATCH] .ai import: opacity mask art without its own paint takes the paint before its dictionary A dictionary's state starts afresh, with a black fill. Mask art is written going on from the paint before the mask's dictionary, which the writer leaves out when it doesn't change, so a mask rectangle with no colour of its own came in black and hid the white letters it masks. Mask art now starts from the paint (fill, stroke, line style, fill rule, overprint) before the dictionaries it is in; lock, visibility and transparency still start afresh. --- crates/eps/src/import/ai/read/ops.rs | 12 ++++++++++++ crates/eps/src/import/ai/tests.rs | 19 +++++++++++++++++++ docs/file-format-parity.md | 6 ++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index c9bd31704..34f579bf7 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -49,6 +49,18 @@ impl<'a> Reader<'a> { return Ok(()); } self.end_path_quietly(); + // The art goes on from the paint before the dictionaries it is in, which `begin_obj` set + // aside: the writer leaves out paint that doesn't change, so a mask rectangle with no color + // of its own takes the white of the art before it, not a fresh state's black, which hides + // what it masks. Lock, visibility and transparency start afresh, as a dictionary's art does. + if let Some(f) = self.frames.iter().rev().take_while(|f| matches!(f.kind, Kind::Obj(_))).last() { + let before = &f.gs; + let gs = &mut self.gs; + (gs.fill, gs.stroke, gs.dash) = (before.fill.clone(), before.stroke.clone(), before.dash.clone()); + (gs.width, gs.cap, gs.join, gs.miter, gs.rule) = (before.width, before.cap, before.join, before.miter, before.rule); + (gs.overprint_fill, gs.overprint_stroke) = (before.overprint_fill, before.overprint_stroke); + (self.pending_unread_fill, self.pending_unread_stroke) = (f.pending_unread_fill, f.pending_unread_stroke); + } self.open(Kind::Mask, hidden) } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index c21bbee70..17423b46b 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -7624,6 +7624,25 @@ fn vector_opacity_mask_keeps_its_art_and_does_not_paint_as_a_sibling() { } } +/// Mask art that sets no paint of its own paints with the paint before its dictionary: the writer +/// leaves out paint that doesn't change. In a user's file, white letters were masked by a rectangle +/// with no colour, which came in black and hid them. +#[test] +fn opacity_mask_art_without_its_own_paint_takes_the_paint_before_its_dictionary() { + let owner = format!("0 0 0 0 k\n0 1 0 0 K\n2 w\n{}B\n", rect_path(10.0, 20.0, 50.0, 60.0)); + let mask = format!( + "%_/ArtDictionary :\n%_/Mask :\n%_X=\n{}%_X+\n%_(Art) ,\n%_1 /Bool (Clipping) ,\n%_; (AI9_Mask) ,\n%_;\n", + rect_path(10.0, 20.0, 30.0, 60.0).lines().map(|l| format!("%_{l}\n")).collect::() + "%_B\n" + ); + let r = read_ok(&layer(&format!("{owner}{mask}"))); + let a = art(&r.doc); + let m = a[0].extras().mask.as_ref().unwrap(); + assert_eq!(fill(&m.art), fill(&a[0])); + let stroke = |n: &Node| n.appearance.stroke().map(|s| (s.paint.clone(), s.width)); + assert_eq!(stroke(&m.art), stroke(&a[0])); + assert!(stroke(&m.art).is_some_and(|(_, w)| w == 2.0)); +} + /// An opacity mask's art of one shown object is that object; other art is read into a group. #[test] fn opacity_mask_art_of_one_object_is_that_object() { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index c68fdb4be..1e4828cca 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 (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) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS and `.ai` editing data: opacity mask art without its own paint takes the paint before its dictionary; 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 @@ -50,7 +50,8 @@ private editing data. VectorCraft: A multi-layer mask (`Mb` … `Md` … `MB`) comes in as a clipping group in each layer it covers, each clipped by a copy of the mask's path. An opacity mask's art of one shown object, other than a guide or a clipping path, opens as that object in the - editing data of a `.ai`, EPS or PDF; other art opens in a group. + editing data of a `.ai`, EPS or PDF; other art opens in a group. Art in the mask that sets no paint of its + own paints with the paint before the mask's dictionary, as the writer leaves out paint that doesn't change. RGB in the PDF page's `ICCBased` spaces keeps the file's numbers in a `.ai` (the `uncalibrated_rgb` import option, on for editor documents, off for PDFs), so `.ai` files tagged with a non-sRGB RGB profile open with different, unconverted RGB colours than before. @@ -305,6 +306,7 @@ An object in a pattern's art or in a brush's art takes the file's art styles in | Date | Change | Summary | |---|---|---| +| 2026-11-07 | minor | EPS and `.ai` editing data: opacity mask art that sets no paint of its own paints with the paint before the mask's dictionary, not the black a dictionary's state starts with. A logo of white letters masked by a rectangle with no colour no longer opens hidden: a user's `.ai` (used locally, not committed) shows its six logos. Scores and estimates unchanged. | | 2026-11-06 | minor | EPS and `.ai` editing data: an art style's stroke aligned inside or outside its path (Stroke panel › Align Stroke, written as its own `Adobe Stroke Offset` filter) comes in as a stroke with that alignment, so objects whose style has one keep the style live instead of coming in as their drawn look. Without its inside/outside switch the drawn look stays. | | 2026-11-06 | minor | EPS and `.ai` editing data: `Q` restores the painting state but not the transparency (`Xy`), which the writer only sets when the next object's differs from the last one written. A Screen glow followed by a clipping group set back to Normal no longer leaves the type after the group in Screen: two users' Illustrator 26.5 and 30 EPS posters (used locally, not committed) now show their gold heading and crescent, and both open from their layers (2.5% and 2.7% from the page before, under the limit after). Scores and estimates unchanged. | | 2026-11-06 | minor | EPS and `.ai` editing data: a blend whose spine was never edited, written with its keys and no spine part, comes in as a live blend along the straight lines between its keys' centres (in its record's order) instead of as its drawn steps. Checked on six users' Illustrator EPS files (used locally, not committed): every blend in four of them now comes in live, their renders unchanged; two still have blends the reader can't match to their drawn steps. Scores and estimates unchanged. |