From cd315908aae3aa7b13f89225cda1af0d69f842a7 Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Sat, 7 Nov 2026 03:07:28 +0600 Subject: [PATCH] Preflight: a frame with states is looked at as PDF export draws it (review) Flex Layout's convert turns a multi-state group into a frame that keeps its states. PDF export draws every object pasted into a frame, but the white overprint check walked only the active state, so a white overprint in another state printed unreported. Which children export draws is now one function, drawn_children, used by both. --- .../src/cmd/preflight/white_overprint.rs | 4 ++-- .../cmd/preflight/white_overprint/tests.rs | 19 +++++++++++++++++++ crates/pdf/src/export.rs | 4 +++- crates/pdf/src/export/pipeline.rs | 8 ++++++++ crates/pdf/src/export/walk.rs | 6 +++--- crates/pdf/src/lib.rs | 4 ++-- 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/preflight/white_overprint.rs b/crates/engine/src/cmd/preflight/white_overprint.rs index 3dd784bdd..5df0d1fa2 100644 --- a/crates/engine/src/cmd/preflight/white_overprint.rs +++ b/crates/engine/src/cmd/preflight/white_overprint.rs @@ -15,7 +15,7 @@ use designcraft_color::{Color, ColorType, SwatchValue, swatch}; use designcraft_doc::cjk_settings::AdornmentOverprint; use designcraft_doc::{AnchoredObject, CharProps, Content, Document, Item, ItemId, SpreadRef, Story, StrokeType}; use designcraft_geom::Affine; -use designcraft_pdf::{item_exported, layer_exported, shown_parent_items}; +use designcraft_pdf::{drawn_children, item_exported, layer_exported, shown_parent_items}; use super::{Issue, page_of}; use crate::Session; @@ -126,7 +126,7 @@ pub(super) fn printed(s: &Session, d: &Document) -> (Vec, BTreeMap (page, on), }; issues.extend(item_issue(d, it, page)); - let children: Vec<_> = it.shown_children().map(|c| (Node::Item(c.clone()), inside, composed)).collect(); + let children: Vec<_> = drawn_children(it).map(|c| (Node::Item(c.clone()), inside, composed)).collect(); if let Content::Text(tf) = &it.content { frames.entry(it.id).or_insert(page); let cs = s.cache.get(d, tf.story, composed); diff --git a/crates/engine/src/cmd/preflight/white_overprint/tests.rs b/crates/engine/src/cmd/preflight/white_overprint/tests.rs index e09f85287..2e8e8360d 100644 --- a/crates/engine/src/cmd/preflight/white_overprint/tests.rs +++ b/crates/engine/src/cmd/preflight/white_overprint/tests.rs @@ -235,3 +235,22 @@ fn what_pdf_export_leaves_out_is_not_reported() { s.execute("object.hide", &json!({"ids": [group]})).unwrap(); assert_eq!(white_overprints(&mut s), [(PAPER_KENTEN.to_string(), r["id"].as_u64())]); } + +/// A multi-state object prints its active state only, but a frame made from one (Flex Layout's +/// convert keeps the states) prints every object in it, as PDF export draws a frame's pasted-in +/// objects: the inactive state's white overprint was left out (noted in review). +#[test] +fn a_frame_with_states_is_looked_at_as_pdf_export_draws_it() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let a = rect(&mut s, [72.0, 72.0, 200.0, 200.0]); + paper_overprint(&mut s, a); + let b = rect(&mut s, [300.0, 72.0, 400.0, 200.0]); + let ms = s.execute("states.create", &json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap(); + let at = s.doc().unwrap().doc.item(designcraft_doc::ItemId(ms)).unwrap().children().iter().position(|c| c.id.0 == a).unwrap(); + s.execute("states.show", &json!({"id": ms, "index": 1 - at})).unwrap(); + assert!(white_overprints(&mut s).is_empty(), "an inactive state doesn't print"); + s.execute("flex.convert", &json!({"ids": [ms]})).unwrap(); + assert!(!s.doc().unwrap().doc.item(designcraft_doc::ItemId(ms)).unwrap().states.is_empty(), "the frame keeps its states"); + assert_eq!(white_overprints(&mut s), [(PAPER_FILL.to_string(), Some(a))]); +} diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index aa834eadb..e4fa19e5c 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -46,7 +46,9 @@ pub(crate) use images::SRGB_ICC; use images::{CmykImage, GreyImage, ImageContentCache, RasterMode, SrgbImage, embedded_icc, is_grey, is_grey_icc}; pub(crate) use metadata::civil; pub(crate) use pipeline::{MARK_LEN, Sheet, exported_items, mark_start}; -pub use pipeline::{export_pdf, export_pdf_with_report, item_exported, layer_exported, print_color, sheet_spreads, shown_parent_items}; +pub use pipeline::{ + drawn_children, export_pdf, export_pdf_with_report, item_exported, layer_exported, print_color, sheet_spreads, shown_parent_items, +}; #[cfg(test)] impl std::hash::Hasher for ConstantImageHasher { fn finish(&self) -> u64 { diff --git a/crates/pdf/src/export/pipeline.rs b/crates/pdf/src/export/pipeline.rs index 415e2db4a..85d39c2e9 100644 --- a/crates/pdf/src/export/pipeline.rs +++ b/crates/pdf/src/export/pipeline.rs @@ -30,6 +30,14 @@ pub fn item_exported(it: &Item) -> bool { !it.hidden && !it.nonprinting } +/// The items PDF export draws inside `it`: every item pasted into a frame (a frame keeps the +/// states of a multi-state group converted to it, and draws them all), or a group's shown +/// children (only the active state of a multi-state object). +pub fn drawn_children(it: &Item) -> impl Iterator> { + let nested = it.has_nested_items(); + it.children().iter().filter(move |_| nested).chain(it.shown_children().filter(move |_| !nested)) +} + /// The parent pages' items that page `pi` of spread `si` shows, on exported layers, each with the /// offset that moves it onto the page: the parent's bases first, then the parent. None when the /// page hides parent items; the items the page overrides are left out (their overrides are the diff --git a/crates/pdf/src/export/walk.rs b/crates/pdf/src/export/walk.rs index 6eda58334..b34b82232 100644 --- a/crates/pdf/src/export/walk.rs +++ b/crates/pdf/src/export/walk.rs @@ -1,6 +1,6 @@ use super::common::{TagEntry, blend, norm, rects_overlap, solid_fill, tf, to_path}; use super::graphics::draw_coverage; -use super::pipeline::{item_exported, layer_exported, shown_parent_items}; +use super::pipeline::{drawn_children, item_exported, layer_exported, shown_parent_items}; use super::*; impl Exporter<'_> { @@ -249,7 +249,7 @@ impl Exporter<'_> { if let Some(c) = &clip { s.push_clip_path(c, &FillRule::NonZero); } - for child in it.children() { + for child in drawn_children(it) { self.item(s, child, xf, page); } if clip.is_some() { @@ -269,7 +269,7 @@ impl Exporter<'_> { fn draw_group_children(&mut self, s: &mut Surface, it: &Item, xf: Affine, page: Option) { let pushes = Self::push_group(s, it.opacity, it.blend) + Self::push_isolation(s, it); - for child in it.shown_children() { + for child in drawn_children(it) { self.item(s, child, xf, page); } for _ in 0..pushes { diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 363509c33..b0ceaefff 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -49,8 +49,8 @@ mod pdfx; mod pdfx1a; mod transitions; pub use export::{ - BookletKind, BookletOptions, booklet_pairs, export_booklet, export_pdf, export_pdf_with_report, item_exported, layer_exported, merge_pdfs, - print_color, sheet_spreads, shown_parent_items, + BookletKind, BookletOptions, booklet_pairs, drawn_children, export_booklet, export_pdf, export_pdf_with_report, item_exported, layer_exported, + merge_pdfs, print_color, sheet_spreads, shown_parent_items, }; pub use pdfx::{check_pdfx4, has_rgb_groups, make_pdfx4, pdfx_version}; pub use pdfx1a::check_pdfx1a;