diff --git a/crates/pdf/src/import/builder.rs b/crates/pdf/src/import/builder.rs index 0d50c58f31..c2650138b9 100644 --- a/crates/pdf/src/import/builder.rs +++ b/crates/pdf/src/import/builder.rs @@ -361,8 +361,9 @@ impl<'p> Builder<'p> { Some(MaskSpec::Mask(m)) => (opacity, Some(m.clone())), None => (opacity, None), }; - // Isolation only shows with blending inside; knockout with several objects. - let isolate = isolate && Node::children_blend(&children, &mut |c| c.blends_through()); + // Isolation only shows with blending or overprinting inside; knockout with several + // objects. + let isolate = isolate && (Node::children_blend(&children, &mut |c| c.blends_through()) || children_overprint(&children)); let knockout = knockout && children.len() > 1; let plain = opacity >= 0.999 && blend == BlendMode::Normal && mask.is_none() && !isolate && !knockout; if children.is_empty() { @@ -1183,3 +1184,16 @@ fn visual_glyph_text(text: String) -> String { let chars: Vec = text.chars().collect(); order.iter().filter_map(|&i| chars.get(i)).collect() } + +/// Whether a visible fill or stroke among `children` overprints, characters' too, directly or +/// through children that don't isolate. In an isolated group an overprinting object is drawn on +/// the group's empty backdrop, so it covers the inks of the art below the group that it does not +/// paint; without isolation it leaves them. +fn children_overprint(children: &[Arc]) -> bool { + children.iter().any(|c| { + c.visible + && (c.appearance.items.iter().any(|i| i.visible() && i.overprint()) + || matches!(&c.kind, NodeKind::Text(t) if t.runs.iter().any(|r| r.style.overprint_fill || r.style.overprint_stroke)) + || (!c.isolate && c.children().is_some_and(|ch| children_overprint(ch)))) + }) +} diff --git a/crates/pdf/src/tests/blend.rs b/crates/pdf/src/tests/blend.rs index 1cd5c949e0..25948763d8 100644 --- a/crates/pdf/src/tests/blend.rs +++ b/crates/pdf/src/tests/blend.rs @@ -195,7 +195,8 @@ fn a_knockout_group_without_blending_inside_is_isolated_only_when_it_isolates() /// An overprinting element of a knockout group that does not isolate is drawn in a `/K` group /// without `/I true`, so it overprints the art below the group, and its graphics state still -/// overprints (`/OP`, `/op`, `/OPM 1`). An isolated group keeps `/I true`. +/// overprints (`/OP`, `/op`, `/OPM 1`). An isolated group keeps `/I true`, reads back isolated +/// and is written with `/I true` again. #[test] fn an_overprinting_element_of_a_knockout_group_that_does_not_isolate_overprints_the_art_below() { for isolate in [false, true] { @@ -219,6 +220,42 @@ fn an_overprinting_element_of_a_knockout_group_that_does_not_isolate_overprints_ }); assert_eq!(overprints, [false, false, false, true], "isolate {isolate}"); assert_eq!(knockout_groups(&back), [3], "isolate {isolate}"); + let mut found = vec![]; + back.walk(|n| { + if n.knockout == vectorcraft_doc::Knockout::On { + found.push(n.isolate); + } + }); + assert_eq!(found, [isolate], "isolate {isolate}"); + assert_eq!(text(&back).matches("/Itrue").count(), usize::from(isolate), "isolate {isolate}"); + } +} + +/// An isolated group with nothing blending inside but an overprinting fill reads back as an +/// isolated group and is written with `/I true` again; a group that does not isolate reads back +/// as its objects. +#[test] +fn an_isolated_group_with_an_overprinting_object_reads_back_isolated() { + for isolate in [false, true] { + let mut d = Document::new(200.0, 200.0); + add(&mut d, rect(Rect::new(0.0, 0.0, 200.0, 200.0), (0.9, 0.8, 0.2))); + let mut top = rect(Rect::new(60.0, 60.0, 160.0, 160.0), (0.0, 0.0, 1.0)); + top.appearance.fill_mut().unwrap().overprint = true; + let mut below = rect(Rect::new(20.0, 20.0, 120.0, 120.0), (1.0, 0.0, 0.0)); + (below.id, top.id) = (d.alloc_id(), d.alloc_id()); + let mut g = Node::group(NodeId(0), vec![std::sync::Arc::new(below), std::sync::Arc::new(top)]); + g.isolate = isolate; + add(&mut d, g); + assert_eq!(text(&d).matches("/Itrue").count(), usize::from(isolate), "isolate {isolate}"); + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap_or_else(|e| panic!("isolate {isolate}: {e}")); + let mut groups = vec![]; + back.walk(|n| { + if matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. }) { + groups.push(n.isolate); + } + }); + assert_eq!(groups, if isolate { vec![true] } else { vec![] }, "isolate {isolate}"); + assert_eq!(text(&back).matches("/Itrue").count(), usize::from(isolate), "isolate {isolate}"); } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4c8b2d43f8..0e1a7fbe3d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1704,6 +1704,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports. PDF export writes a knockout group as one PDF knockout group (`/K true`, with `/I true` when the group isolates; ISO 32000-1, 11.4.6.3), so a knockout group of any size opens again in VectorCraft as a knockout group, and the form nesting of the file does not grow with the number of elements. When an element's opacity or mask defines its knockout shape, or it paints translucently, each element is drawn through one soft mask of the overlapping elements above it instead; past 65,536 objects in a group's masks (or 2,048 elements) the group is a PDF knockout group, with a warning that its translucent elements knock out fully. SVG export draws each element of a knockout group through one mask of the overlapping elements above it, and the masks refer to one copy of each element's shape, so the elements stay siblings and the file nests no deeper with more elements; past 65,536 shapes in a group's masks (or 2,048 elements) the group is written as a plain group, with a warning. Evidence: `pdf/src/tests/blend.rs`, `svg/tests/it/blend_export.rs`, `pdf/tests/gradient_forms.rs` (a gradient in a knockout group reads back in place). A knockout group in Normal mode at full opacity that does not isolate, with nothing blending inside, is written without an isolated group around its elements, clipped or not, so the art below the group is its initial backdrop in the PDF as on the canvas (ISO 32000-1, 11.4.5): neither its `/K` group nor a form around a clipped group carries `/I true`, and an overprinting element in it overprints the art below the group. Evidence: `pdf/src/tests/blend.rs` (`a_knockout_group_without_blending_inside_is_isolated_only_when_it_isolates`, `an_overprinting_element_of_a_knockout_group_that_does_not_isolate_overprints_the_art_below`). + PDF import keeps a group's isolation when something inside it overprints as well as when something blends, so an isolated group or knockout group whose objects only overprint opens as an isolated group and is written with `/I true` again, and its overprinting objects still cover the inks of the art below the group. Evidence: `pdf/src/tests/blend.rs` (`an_isolated_group_with_an_overprinting_object_reads_back_isolated`, `an_overprinting_element_of_a_knockout_group_that_does_not_isolate_overprints_the_art_below`). - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; eleven generated style libraries (including Game HUD, Status Effects, Neon & Glow, Painted and Sketched, whose seven styles each repaint a solid fill with one watercolor, dry brush, pastel, charcoal or ink effect, and Textures) save and load as `.vcstyles`. The built-in Textures graphic style library (`graphicStyle.addFromLibrary {library: "textures"}`) has eight styles that paint their fills and strokes with the texture effects: Fine Grain (Grain), Canvas Weave and Burlap Sack (Texturizer), Cracked Glaze (Craquelure), Mosaic Floor (Mosaic Tiles), Quilted Squares (Patchwork), Leaded Glass (Stained Glass) and Damp Paper (Water Paper). The help page names new graphic styles for textures ([What's new](https://helpx.adobe.com/illustrator/desktop/new-features/whats-new.html)) without naming them, so the styles, their names and their settings are VectorCraft's own (`tests::stylelib::textures_styles_apply_their_texture_effects_and_keep_their_params_through_save_and_open`). In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one.