From 0be78766f4172ab4eb5bed85c84fdfe170f0a959 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 13:13:15 -0800 Subject: [PATCH] .ai import: a compound of clipping paths keeps their knockout shape flag Clipping paths in a compound path that holds a drawn look become one compound path. It took the paths' shared stroke, opacity and blend mode but dropped the knockout shape flag. It now has the flag when every clipping path has it. --- crates/eps/src/import/ai/read/containers.rs | 4 +++- crates/eps/src/import/ai/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index 35c6bf7de..84d974876 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -281,7 +281,8 @@ impl<'a> Reader<'a> { _ => { let rule = clip.first().map_or(FillRule::NonZero, |(_, n)| path_rule(n)); // The compound path keeps the clipping paths' stroke, opacity and blend mode when they all have the same - // ones: it has one appearance for all its paths. + // ones, and the knockout shape flag when they all have it: it has one appearance for all its paths. + let shape = clip.iter().all(|(_, n)| n.knockout_shape); let look = |n: &Node| (n.appearance.clone(), n.opacity, n.blend); let shared = clip.split_first().and_then(|((_, first), others)| others.iter().all(|(_, n)| look(n) == look(first)).then(|| look(first))); @@ -292,6 +293,7 @@ impl<'a> Reader<'a> { n.opacity = opacity; n.blend = blend; } + n.knockout_shape = shape; Some(n) } }; diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 04493dcb2..51503bfdd 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -1309,6 +1309,26 @@ fn a_clipping_paths_stroke_stays_on_the_clipping_path() { assert_eq!(strokes("", "5 w\n"), [plain, (Some((blue, 5.0)), 1.0, BlendMode::Normal)], "strokes that differ"); } +/// Clipping paths in a compound path that holds a drawn look become one compound path, which keeps the knockout shape +/// flag when they all have it. +#[test] +fn the_compound_of_clipping_paths_keeps_their_knockout_shape() { + let (under, over) = (rect_path(0.0, 0.0, 30.0, 30.0), rect_path(5.0, 5.0, 25.0, 25.0)); + let (a, b) = (rect_path(0.0, 0.0, 10.0, 10.0), rect_path(5.0, 0.0, 20.0, 10.0)); + let clipping = |first: &str, second: &str| { + let look = format!("0 Ae\nu\n{RED}{under}f\n0 0 0 0 0 0 1 Xa\n{over}f\nU\n9 () XW\n"); + let doc = read_ok(&layer(&format!("0 Ae\n*u\n{look}{first}{a}h\nW\nn\n{second}{b}h\nW\nn\n*U\n"))).doc; + let NodeKind::Group { children, clip: true } = &art(&doc)[0].kind else { panic!("{:?}", art(&doc)[0].kind) }; + assert!(matches!(children[0].kind, NodeKind::Compound { .. }), "{:?}", children[0].kind); + (children[0].opacity, children[0].knockout_shape) + }; + let shape = "0 0.5 0 0 1 Xy\n"; + assert_eq!(clipping(shape, ""), (0.5, true)); + assert_eq!(clipping("", ""), (1.0, false)); + assert_eq!(clipping(shape, "0 0.5 0 0 0 Xy\n"), (0.5, false), "the flag on one of them only"); + assert_eq!(clipping(shape, "0 0.8 0 0 1 Xy\n"), (1.0, true), "opacities that differ"); +} + /// Several clipping paths in a clipping group, a layer or an opacity mask's art clip to their intersection: each `W` /// reduces the clip set by the ones before it (AI7 spec ยง5.6.2). Each clipping path after the first clips a clipping /// group inside the one before it, and the innermost one holds the art; a clipping path's painted fill stays at its own