From d5a672fda2d961eac5919d87008e6a2a21b69000 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:00:40 +0300 Subject: [PATCH] eps: an opacity mask in the editing data is named, not drawn whole (#2004) --- crates/eps/src/import/ai/read.rs | 11 ++++++++++- crates/eps/src/import/ai/tests.rs | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index e892369f1..558200ff5 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -1356,8 +1356,17 @@ impl<'a> Reader<'a> { self.frames.iter().all(|f| !matches!(&f.kind, Kind::Layer(a) if !a.visible)) } - /// An object's dictionary: its name (`AIArtName`, or the XML id `AI10_ArtUID`). + /// An object's dictionary: its name (`AIArtName`, or the XML id `AI10_ArtUID`), and the + /// opacity mask it may carry. fn art_dictionary(&mut self, o: &Obj) { + // A mask (`/Mask`, keyed `AI9_Mask`): its art is on `X=` … `X+` lines, and drawing the + // object without it shows what the mask hides, so it must be named (#2004). A mask that + // is disabled changes nothing. + if let Some(mask) = o.obj("AI9_Mask").or_else(|| o.obj("Mask")) + && mask.nums("Disabled").first().copied() != Some(1.0) + { + self.unreadable("opacity masks"); + } let name = match o.text("AIArtName") { Some(name) => name, None => match o.obj("AI10_ArtUID").and_then(Obj::content_text) { diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index aa482aaa3..c9ed212ee 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -667,6 +667,14 @@ fn what_it_doesnt_read_is_an_error_where_it_shows() { assert!(e.contains("pattern fills"), "{e}"); let e = read(stream(&layer("/SymbolInstance :\n(Dot) /SymbolRef ,\n;\n")).as_bytes()).unwrap_err(); assert!(e.contains("symbols"), "{e}"); + // An opacity mask (#2004): drawing the layer's art whole would show what the mask hides, so + // the mask is named and the file is read from its PDF part instead. + let mask = "%_/ArtDictionary :\n%_/XMLUID : (M) ; (AI10_ArtUID) ,\n%_/Mask :\n%_1 /Bool (Linked) ,\n%_0 /Bool (Disabled) ,\n%_X=\n\ + %_0 0 0 1 k\n%_0 0 m\n%_5 0 L\n%_5 5 L\n%_0 5 L\n%_0 0 L\n%_f\n%_X+\n%_ (Art) ,\n%_1 /Bool (Clipping) ,\n%_0 /Bool (Inverted) ,\n%_; (AI9_Mask) ,\n%_;\n"; + let e = read(stream(&layer(mask)).as_bytes()).unwrap_err(); + assert!(e.contains("opacity masks"), "{e}"); + // A mask that is disabled changes nothing: the art reads. + read_ok(&layer(&mask.replace("%_0 /Bool (Disabled)", "%_1 /Bool (Disabled)"))); // On a layer that doesn't show, it is left out of it. let hidden = "%AI5_BeginLayer\n0 1 1 1 0 0 0 1 255 79 79 0 50 0 Lb\n(Off) Ln\n1 2 frobnicate\n(J) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\nLB\n%AI5_EndLayer--\n"; let r = read_ok(hidden);