diff --git a/crates/eps/src/import/ai/paint.rs b/crates/eps/src/import/ai/paint.rs index 178238dc1..8c863ba99 100644 --- a/crates/eps/src/import/ai/paint.rs +++ b/crates/eps/src/import/ai/paint.rs @@ -67,20 +67,29 @@ pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option)> { "x" => named(vals, Some(false)), "xx" => named(vals, None), "xk" => named(vals, None).map(|(p, n)| (p, n.map(|n| Named { spot: false, ..n }))), - "xs" | "xz" => registration(vals), + "xs" | "xz" => registration(op, vals), _ => None, } } +fn is_registration(name: &str) -> bool { + matches!(name, vectorcraft_color::swatch::REGISTRATION | "Registration" | "/All" | "All" | "/[Registration]") +} + /// `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 at = vals.iter().rposition(|v| matches!(v, V::Str(_) | V::Name(_)))?; let name = vals.get(at)?.text()?; let before: Vec = vals.get(..at)?.iter().filter_map(V::num).map(|v| v as f32).collect(); let after: Vec = vals.get(at + 1..)?.iter().filter_map(V::num).map(|v| v as f32).collect(); let unit = |v: f32| v.clamp(0.0, 1.0); let tint = 1.0 - unit(*after.first()?); + if is_registration(&name) { + let full = Color::cmyk(1.0, 1.0, 1.0, 1.0); + let paint = Paint::Solid { color: full.tinted(tint), swatch: Some(vectorcraft_color::swatch::REGISTRATION.into()), tint }; + return Some((paint, None)); + } let n = before.len(); let kind = after.get(1).copied(); let color = if rgb.unwrap_or(kind == Some(1.0)) { @@ -102,9 +111,20 @@ fn named(vals: &[V], rgb: Option) -> Option<(Paint, Option)> { /// `… (name) tint Xs` / `… (name) tint type Xz`: the built-in Registration swatch at the tint (its /// own colour, whatever values the operands give). -fn registration(vals: &[V]) -> Option<(Paint, Option)> { - let at = vals.iter().rposition(|v| matches!(v, V::Str(_)))?; - let tint = 1.0 - vals.get(at + 1).and_then(V::num).unwrap_or(0.0).clamp(0.0, 1.0) as f32; +fn registration(op: &str, vals: &[V]) -> Option<(Paint, Option)> { + let at = vals.iter().rposition(|v| matches!(v, V::Str(_) | V::Name(_))); + let tint = if let Some(i) = at { + 1.0 - vals.get(i + 1).and_then(V::num).unwrap_or(0.0).clamp(0.0, 1.0) as f32 + } else { + let mut nums = vals.iter().rev().filter_map(V::num); + let t = if op.eq_ignore_ascii_case("xz") { + let _type = nums.next(); + nums.next().unwrap_or(0.0) + } else { + nums.next().unwrap_or(0.0) + }; + 1.0 - t.clamp(0.0, 1.0) as f32 + }; let full = Color::cmyk(1.0, 1.0, 1.0, 1.0); Some((Paint::Solid { color: full.tinted(tint), swatch: Some(vectorcraft_color::swatch::REGISTRATION.into()), tint }, None)) } @@ -512,6 +532,29 @@ mod tests { let mut v = nums(&[1.0, 1.0, 1.0, 1.0]); v.extend([V::Str(b"[Registration]".to_vec()), V::Num(0.0)]); assert!(matches!(color_op("Xs", &v), Some((Paint::Solid { tint, .. }, None)) if tint == 1.0)); + let mut v = nums(&[1.0, 1.0, 1.0, 1.0]); + v.extend([V::Str(b"[Registration]".to_vec()), V::Num(1.0)]); + assert!( + matches!(color_op("Xs", &v), Some((Paint::Solid { tint, color, .. }, None)) if tint == 0.0 && color == Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(0.0)) + ); + let mut v = nums(&[1.0, 1.0, 1.0, 1.0]); + v.extend([V::Name("All".into()), V::Num(1.0)]); + assert!(matches!(color_op("Xs", &v), Some((Paint::Solid { tint, .. }, None)) if tint == 0.0)); + let mut v = nums(&[1.0, 1.0, 1.0, 1.0]); + v.extend([V::Str(b"[Registration]".to_vec()), V::Num(0.2), V::Num(0.0)]); + let (p, named) = color_op("Xx", &v).unwrap(); + assert!(named.is_none()); + assert!(matches!(p, Paint::Solid { swatch: Some(s), tint, .. } if s == "[Registration]" && (tint - 0.8).abs() < 1e-6)); + let mut v = nums(&[1.0, 1.0, 1.0, 1.0]); + v.extend([V::Name("All".into()), V::Num(0.5), V::Num(0.0)]); + let (p, named) = color_op("Xx", &v).unwrap(); + assert!(named.is_none()); + assert!(matches!(p, Paint::Solid { swatch: Some(s), tint, .. } if s == "[Registration]" && (tint - 0.5).abs() < 1e-6)); + // Name-free Xs and Xz operands + let v = nums(&[1.0, 1.0, 1.0, 1.0, 0.3]); + assert!(matches!(color_op("Xs", &v), Some((Paint::Solid { tint, .. }, None)) if (tint - 0.7).abs() < 1e-6)); + let v = nums(&[1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.2, 1.0]); + assert!(matches!(color_op("Xz", &v), Some((Paint::Solid { tint, .. }, None)) if (tint - 0.8).abs() < 1e-6)); } #[test] @@ -528,6 +571,14 @@ mod tests { assert_eq!(named.name, "MySpot"); assert!((named.tint - 0.6).abs() < 1e-6); assert!(matches!(p, Paint::Solid { swatch: Some(_), .. })); + let mut v_name = nums(&[0.0, 0.5, 1.0, 0.0]); + v_name.push(V::Name("MySpot".into())); + v_name.push(V::Num(0.4)); + let (p, named) = color_op("x", &v_name).unwrap(); + let named = named.unwrap(); + assert_eq!(named.name, "MySpot"); + assert!((named.tint - 0.6).abs() < 1e-6); + assert!(matches!(p, Paint::Solid { swatch: Some(_), .. })); assert!(color_op("k", &nums(&[1.0])).is_none()); // A global process colour: CMYK (type 0) or RGB (type 1), not an ink. let mut v = nums(&[0.0, 0.0, 0.0, 1.0]); diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index d9ddedc92..d5d28aab6 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -523,7 +523,7 @@ impl<'a> Reader<'a> { fn color(&mut self, w: &str, vals: &[V]) { let Some((p, named)) = paint::color_op(w, vals) else { return }; // Registration (`Xs`, `Xz`) says nothing of the document's colour model. - let registration = matches!(w, "Xs" | "XS" | "Xz" | "XZ"); + let registration = matches!(w, "Xs" | "XS" | "Xz" | "XZ") || p.is_registration(); match p.color().filter(|_| !registration) { Some(vectorcraft_color::Color::Rgb { .. }) => self.rgb_colors += 1, Some(vectorcraft_color::Color::Cmyk { .. }) => self.cmyk_colors += 1, diff --git a/crates/eps/src/import/ai/style.rs b/crates/eps/src/import/ai/style.rs index a23101e26..857837afe 100644 --- a/crates/eps/src/import/ai/style.rs +++ b/crates/eps/src/import/ai/style.rs @@ -59,7 +59,7 @@ use vectorcraft_effects::is_pathfinder; use super::brush::PathBrush; use super::lex::{Lexer, Tok}; -use super::obj::V; +use super::obj::{self, V}; use super::paint; const GLOW: &str = "Adobe Outer Glow"; // brand-ok: the filter's name in the files @@ -432,6 +432,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap { b"FillStyle" => style_entry = Some(Kind::Fill), b"StrokeStyle" | b"BlendStyle" => style_entry = Some(Kind::Other), b"Name" if name.is_none() && filters.is_empty() => name = operands.iter().rev().find_map(V::text), + b"All" | b"[Registration]" | b"Registration" => operands.push(V::Name(obj::text(n))), b"CompoundFilter" => pending = Some(true), b"BasicFilter" => pending = Some(false), b"Part" => in_basic = false, @@ -605,7 +606,8 @@ fn block_data(lex: &mut Lexer, encoded: &[u8]) -> Option> { fn paint_op(f: &mut Filter, op: &str, vals: &[V]) { let last = vals.iter().rev().find_map(V::num); match op { - "Xa" | "k" | "g" | "x" | "Xk" | "XA" | "K" | "G" | "X" | "XK" => { + "Xa" | "k" | "g" | "x" | "Xx" | "Xk" | "Xs" | "Xz" | "XA" | "K" | "G" | "X" | "XX" | "XK" | "XS" | "XZ" | "xa" | "xx" | "xk" | "xs" + | "xz" => { let Some((p, _)) = paint::color_op(op, vals) else { return }; if op.ends_with(|c: char| c.is_ascii_lowercase()) { f.fill = Some(p); diff --git a/crates/eps/src/import/ai/style/tests.rs b/crates/eps/src/import/ai/style/tests.rs index 0845ca0d9..ae79a345e 100644 --- a/crates/eps/src/import/ai/style/tests.rs +++ b/crates/eps/src/import/ai/style/tests.rs @@ -1,4 +1,5 @@ use super::*; +use vectorcraft_color::Color; #[test] fn a_variable_width_stroke_keeps_its_profile() { @@ -1393,3 +1394,52 @@ fn a_style_is_read_as_a_writer_checks_it() { let _ = read(junk, Some(&own)); } } + +#[test] +fn art_styles_read_registration_and_named_colors() { + let reg_0 = fill("1 1 1 1 ([Registration]) 1 Xs"); + let reg_100 = fill("1 1 1 1 ([Registration]) 0 Xs"); + let reg_name = fill("1 1 1 1 /All 1 Xs"); + let named_spot = fill("0 0.5 1 0 (Shemen) 0 0 Xx"); + let stroke_reg = basic( + "Stroke Style Filter", + "/StrokeStyle : 0 R\n1 1 1 1 ([Registration]) 1 XS\n0 1 0 0 0 Xy\n0 J 0 j 1 w 10 M []0 d\n0 XR\n1 0 Xd\n/Def ;\n (StrokeStyle) ,\n", + ); + let data = format!( + "/KnownStyle :\n(Reg0) /Name ,\n/ActiveStyle :\n{}\n\ + /KnownStyle :\n(Reg100) /Name ,\n/ActiveStyle :\n{}\n\ + /KnownStyle :\n(RegName) /Name ,\n/ActiveStyle :\n{}\n\ + /KnownStyle :\n(Shemen) /Name ,\n/ActiveStyle :\n{}\n\ + /KnownStyle :\n(StrokeReg) /Name ,\n/ActiveStyle :\n{}\n", + compound("Chain Style Filter", &[compound("Stack Style Filter", &[reg_0])]), + compound("Chain Style Filter", &[compound("Stack Style Filter", &[reg_100])]), + compound("Chain Style Filter", &[compound("Stack Style Filter", &[reg_name])]), + compound("Chain Style Filter", &[compound("Stack Style Filter", &[named_spot])]), + compound("Chain Style Filter", &[compound("Stack Style Filter", &[stroke_reg])]), + ); + let styles = art_styles(data.as_bytes()); + let st0 = &styles["Reg0"]; + assert!(st0.plain); + let [StyleItem::Fill(Paint::Solid { swatch, tint, color })] = st0.items.as_slice() else { panic!("{st0:?}") }; + assert_eq!((swatch.as_deref(), *tint, *color), (Some(vectorcraft_color::swatch::REGISTRATION), 0.0, Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(0.0))); + + let st100 = &styles["Reg100"]; + assert!(st100.plain); + let [StyleItem::Fill(Paint::Solid { swatch, tint, color })] = st100.items.as_slice() else { panic!("{st100:?}") }; + assert_eq!((swatch.as_deref(), *tint, *color), (Some(vectorcraft_color::swatch::REGISTRATION), 1.0, Color::cmyk(1.0, 1.0, 1.0, 1.0))); + + let st_name = &styles["RegName"]; + assert!(st_name.plain); + let [StyleItem::Fill(Paint::Solid { swatch, tint, .. })] = st_name.items.as_slice() else { panic!("{st_name:?}") }; + assert_eq!((swatch.as_deref(), *tint), (Some(vectorcraft_color::swatch::REGISTRATION), 0.0)); + + let st_spot = &styles["Shemen"]; + assert!(st_spot.plain); + let [StyleItem::Fill(Paint::Solid { swatch, tint, color })] = st_spot.items.as_slice() else { panic!("{st_spot:?}") }; + assert_eq!((swatch.as_deref(), *tint, *color), (Some("Shemen"), 1.0, Color::cmyk(0.0, 0.5, 1.0, 0.0))); + + let st_stroke = &styles["StrokeReg"]; + assert!(st_stroke.plain); + let [StyleItem::Stroke { paint: Paint::Solid { swatch, tint, .. }, .. }] = st_stroke.items.as_slice() else { panic!("{st_stroke:?}") }; + assert_eq!((swatch.as_deref(), *tint), (Some(vectorcraft_color::swatch::REGISTRATION), 0.0)); +} diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 0ae436f72..e5c927670 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -6069,6 +6069,30 @@ fn an_object_whose_transparency_differs_from_its_listed_style_is_not_linked() { assert_eq!(a[0].extras().graphic_style, None, "the style has full opacity"); } +#[test] +fn ai_import_registration_style_does_not_carry_over_previous_color_6205() { + // Regression test for #6205: + // A background shape sets a spot colour ("Shemen"). + // A following object has an art style with [Registration] at 0% tint. + // The object must resolve the Registration style ([Registration] 0% / #ffffff) + // rather than falling back to the prior spot color. + let defs = [fill_style("RegWhite", "1 1 1 1 ([Registration]) 1 Xs\n")]; + let background = format!("0 0.67 0.73 0.26 (Shemen) 0 0 Xx\n{}f\n", rect_path(0.0, 0.0, 100.0, 100.0)); + let styled_obj = styled_square("RegWhite"); + let content = format!("{background}{styled_obj}"); + let r = read_ok(&format!("{}{}", styles_and_list(&defs, &["RegWhite"]), layer(&content))); + let a = art(&r.doc); + assert_eq!(a.len(), 2, "{a:#?}"); + // First object is background in Shemen spot + let Paint::Solid { swatch: s0, .. } = fill(&a[0]) else { panic!("{:#?}", a[0]) }; + assert_eq!(s0.as_deref(), Some("Shemen")); + // Second object is styled in RegWhite: [Registration] at 0% tint (#ffffff) + let Paint::Solid { swatch, tint, color } = fill(&a[1]) else { panic!("{:#?}", a[1]) }; + assert_eq!(swatch.as_deref(), Some(vectorcraft_color::swatch::REGISTRATION)); + assert_eq!(tint, 0.0); + assert_eq!(color, Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(0.0)); +} + #[test] fn a_listed_style_keeps_its_transparency_and_links_objects_that_have_it() { let blend = "/BasicFilter :\n(Blend Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n\