diff --git a/crates/doc/src/node/ops.rs b/crates/doc/src/node/ops.rs index fcf2c9429..edab5f73c 100644 --- a/crates/doc/src/node/ops.rs +++ b/crates/doc/src/node/ops.rs @@ -686,6 +686,12 @@ impl Node { /// stroke opacity below 100%, a blend mode, an opacity mask, effects, transparent gradient /// stops or a colorized image. Hidden objects show none. pub fn shows_transparency(&self) -> bool { + self.shows_transparency_with(&|_| true) + } + + /// [`Self::shows_transparency`], counting a colorized image by itself only when `colorize` + /// accepts it, for files that write some colorized images in their colorize paint (EPS). + pub fn shows_transparency_with(&self, colorize: &dyn Fn(&Node) -> bool) -> bool { use vectorcraft_color::Paint; let see_through = |p: &Paint| matches!(p, Paint::Gradient(g) if g.gradient.stops.iter().any(|s| s.opacity < 1.0)); let effects = |fx: &[crate::Effect]| fx.iter().any(|e| e.visible); @@ -693,14 +699,14 @@ impl Node { && (self.opacity < 1.0 || self.blend != BlendMode::Normal || self.extras().mask.as_ref().is_some_and(|m| !m.disabled) - || (matches!(self.kind, NodeKind::Image(_)) && self.extras().colorize.is_some()) + || (matches!(self.kind, NodeKind::Image(_)) && self.extras().colorize.is_some() && colorize(self)) || effects(&self.appearance.effects) || self .appearance .items .iter() .any(|i| i.visible() && (i.opacity() < 1.0 || i.blend() != BlendMode::Normal || see_through(i.paint()) || effects(i.effects()))) - || self.children().is_some_and(|ch| ch.iter().any(|c| c.shows_transparency()))) + || self.children().is_some_and(|ch| ch.iter().any(|c| c.shows_transparency_with(colorize)))) } } diff --git a/crates/engine/src/cmd/expand.rs b/crates/engine/src/cmd/expand.rs index e536d0374..cf61a305b 100644 --- a/crates/engine/src/cmd/expand.rs +++ b/crates/engine/src/cmd/expand.rs @@ -447,27 +447,32 @@ fn expand_pattern_fills(s: &mut Session) -> Result<()> { /// Expand the pattern fills under `root` that paint a transparent or colorized image into the tiles they paint /// ([`expand_patterns_where`]), so each tile image becomes an object of its own; the other /// pattern fills stay. A tile image counts when its key is in `see_through` (it has see-through -/// pixels), when it has a colorize paint, or when it, a group +/// pixels), when it has a colorize paint that `colorize` counts, or when it, a group /// in the tile art, the fill, the shape or a group the shape is in has opacity or a blending /// mode. Returns how many shapes changed. -pub(crate) fn expand_transparent_image_patterns(d: &mut Document, root: NodeId, see_through: &BTreeSet) -> Result { +pub(crate) fn expand_transparent_image_patterns( + d: &mut Document, + root: NodeId, + see_through: &BTreeSet, + colorize: &dyn Fn(&Document, &Node) -> bool, +) -> Result { let (_, n) = replace_leaves(d, root, |d, l| { // Most leaves have no pattern fill: skip the ancestry walk for them. if !l.appearance.items.iter().any(|i| pattern_fill(d, i).is_some()) { return None; } - expand_patterns_where(d, l, |d, i| transparent_pattern_fill(d, l, i, see_through)) + expand_patterns_where(d, l, |d, i| transparent_pattern_fill(d, l, i, see_through, colorize)) })?; Ok(n) } /// Whether a path or compound path of `d` has a pattern fill that -/// [`expand_transparent_image_patterns`] would expand with `see_through`. -pub(crate) fn has_transparent_image_patterns(d: &Document, see_through: &BTreeSet) -> bool { +/// [`expand_transparent_image_patterns`] would expand with `see_through` and `colorize`. +pub(crate) fn has_transparent_image_patterns(d: &Document, see_through: &BTreeSet, colorize: &dyn Fn(&Document, &Node) -> bool) -> bool { let mut found = false; d.walk(|n| { if !found && matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) { - found = n.appearance.items.iter().any(|i| transparent_pattern_fill(d, n, i, see_through)); + found = n.appearance.items.iter().any(|i| transparent_pattern_fill(d, n, i, see_through, colorize)); } }); found @@ -475,11 +480,17 @@ pub(crate) fn has_transparent_image_patterns(d: &Document, see_through: &BTreeSe /// Whether `i`, an item of leaf `l`, is a pattern fill whose tile art holds a transparent image /// ([`transparent_image`]), counting the fill's and the shape's opacity and blending mode. -fn transparent_pattern_fill(d: &Document, l: &Node, i: &AppearanceItem, see_through: &BTreeSet) -> bool { +fn transparent_pattern_fill( + d: &Document, + l: &Node, + i: &AppearanceItem, + see_through: &BTreeSet, + colorize: &dyn Fn(&Document, &Node) -> bool, +) -> bool { let Some((f, def, _)) = pattern_fill(d, i) else { return false }; let shape = d.ancestry(l.id).unwrap_or_default().iter().filter_map(|a| d.node(*a)).any(blended); let on = shape || f.opacity < 1.0 || f.blend != BlendMode::Normal; - def.art.iter().any(|a| transparent_image(a, see_through, on)) + def.art.iter().any(|a| transparent_image(d, a, see_through, colorize, on)) } /// Whether `n` has opacity or a blending mode of its own. @@ -487,13 +498,14 @@ fn blended(n: &Node) -> bool { n.opacity < 1.0 || n.blend != BlendMode::Normal } -/// Whether `n` is or holds an image with see-through pixels (key in `keys`), a colorize paint, -/// opacity or a blending mode of its own or of a group in `n`, or anywhere when `on`. -fn transparent_image(n: &Node, keys: &BTreeSet, on: bool) -> bool { +/// Whether `n` (art of `d`) is or holds an image with see-through pixels (key in `keys`), a +/// colorize paint that `colorize` counts, opacity or a blending mode of its own or of a group in +/// `n`, or anywhere when `on`. +fn transparent_image(d: &Document, n: &Node, keys: &BTreeSet, colorize: &dyn Fn(&Document, &Node) -> bool, on: bool) -> bool { let on = on || blended(n); match &n.kind { - NodeKind::Image(im) => on || keys.contains(&im.key) || n.extras().colorize.is_some(), - _ => n.children().is_some_and(|ch| ch.iter().any(|c| transparent_image(c, keys, on))), + NodeKind::Image(im) => on || keys.contains(&im.key) || (n.extras().colorize.is_some() && colorize(d, n)), + _ => n.children().is_some_and(|ch| ch.iter().any(|c| transparent_image(d, c, keys, colorize, on))), } } diff --git a/crates/engine/src/cmd/fileio/eps.rs b/crates/engine/src/cmd/fileio/eps.rs index a999b31ce..2c8ae4a84 100644 --- a/crates/engine/src/cmd/fileio/eps.rs +++ b/crates/engine/src/cmd/fileio/eps.rs @@ -301,7 +301,7 @@ fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result { let regions: Vec = pages.iter().map(|(_, region, _)| *region).collect(); let expanded = expand_transparent_tile_images(doc, &set.flatten)?; let source = expanded.as_ref().unwrap_or(doc); - let transparent = source.layers.iter().any(|l| l.shows_transparency()); + let transparent = source.layers.iter().any(|l| set.flatten.shows_transparency(source, l)); let flat = flatten_document(source, &set.flatten)?; let drawn = flat.as_ref().unwrap_or(source); let mut enc = Encoded { warnings, joiner: Some("_"), ..Encoded::default() }; diff --git a/crates/engine/src/cmd/fileio/legacy_ai.rs b/crates/engine/src/cmd/fileio/legacy_ai.rs index 0e53a5a18..7cbbda2fe 100644 --- a/crates/engine/src/cmd/fileio/legacy_ai.rs +++ b/crates/engine/src/cmd/fileio/legacy_ai.rs @@ -167,7 +167,7 @@ fn expand_image_patterns(d: &mut Document) -> Result<()> { let keys = super::super::flatten::see_through_images(d); let layers: Vec = d.layers.iter().map(|l| l.id).collect(); for id in layers { - super::super::expand::expand_transparent_image_patterns(d, id, &keys)?; + super::super::expand::expand_transparent_image_patterns(d, id, &keys, &|_, _| true)?; } Ok(()) } diff --git a/crates/engine/src/cmd/flatten/apply.rs b/crates/engine/src/cmd/flatten/apply.rs index 463323029..7fbe44a0d 100644 --- a/crates/engine/src/cmd/flatten/apply.rs +++ b/crates/engine/src/cmd/flatten/apply.rs @@ -74,7 +74,7 @@ pub(super) fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render: let mut sc = Scratch { brushes: vectorcraft_brush::library(&baked), doc: baked.clone() }; let plain: Vec = art.iter().map(|n| sc.plain(n)).collect(); let reaches: Vec> = plain.iter().map(reach).collect(); - let see_through: Vec = plain.iter().map(|n| n.shows_transparency() || o.draws_see_through_image(n)).collect(); + let see_through: Vec = plain.iter().map(|n| o.shows_transparency(&baked, n) || o.draws_see_through_image(n)).collect(); let cmyk = src.color_mode == ColorMode::Cmyk; let mut out = Plan { flat: vec![], kept: vec![], transparent: roots.iter().zip(&see_through).filter(|(_, t)| **t).map(|(id, _)| *id).collect() }; // Baking above stopped early when cancelled; each group is a place to stop (a plan changes nothing). @@ -298,28 +298,29 @@ pub(crate) fn expand_transparent_tile_images(doc: &Document, o: &FlattenOptions) return Ok(None); } let keys = o.no_transparency.clone().unwrap_or_default(); - if !crate::cmd::expand::has_transparent_image_patterns(doc, &keys) { + let colorize = |d: &Document, n: &Node| o.counts_colorize(d, n); + if !crate::cmd::expand::has_transparent_image_patterns(doc, &keys, &colorize) { return Ok(None); } let mut d = doc.clone(); let layers: Vec = d.layers.iter().map(|l| l.id).collect(); let mut changed = 0; for id in layers { - changed += crate::cmd::expand::expand_transparent_image_patterns(&mut d, id, &keys)?; + changed += crate::cmd::expand::expand_transparent_image_patterns(&mut d, id, &keys, &colorize)?; } Ok((changed > 0).then_some(d)) } /// `doc` with everything it draws flattened with `o`, as formats without transparency (EPS) /// write it: every visible object of the visible layers, locked ones too (a clipping layer's -/// clipping path stays). Pattern tile images with opacity, blending or colorize are expanded; -/// see-through tile pixels are expanded when `o.no_transparency` counts them. `None` when nothing -/// changes. +/// clipping path stays). Pattern tile images with opacity, blending or a colorize paint that +/// `o` counts ([`FlattenOptions::counts_colorize`]) are expanded; see-through tile pixels are +/// expanded when `o.no_transparency` counts them. `None` when nothing changes. pub(crate) fn flatten_document(doc: &Document, o: &FlattenOptions) -> Result> { let expanded = expand_transparent_tile_images(doc, o)?; let doc = expanded.as_ref().unwrap_or(doc); let outlining = o.text_to_outlines || o.strokes_to_outlines || !o.preserve_overprints; - if !outlining && !doc.layers.iter().any(|l| l.shows_transparency() || o.draws_see_through_image(l)) { + if !outlining && !doc.layers.iter().any(|l| o.shows_transparency(doc, l) || o.draws_see_through_image(l)) { return Ok(expanded); } let mut roots = vec![]; diff --git a/crates/engine/src/cmd/flatten/options.rs b/crates/engine/src/cmd/flatten/options.rs index 57331660a..d4f18f385 100644 --- a/crates/engine/src/cmd/flatten/options.rs +++ b/crates/engine/src/cmd/flatten/options.rs @@ -2,7 +2,7 @@ use super::analysis::draws_image; use super::apply::{apply, kept_work, plan}; use super::presets::{FlattenerPreset, saved_preset}; use super::report::target_roots; -use super::{BTreeSet, Deserialize, EngineError, Node, Result, Serialize, Session, Value, bad, json, str_param}; +use super::{BTreeSet, Deserialize, Document, EngineError, Node, NodeKind, Paint, Result, Serialize, Session, Value, bad, json, str_param}; /// Object → Flatten Transparency settings: a preset's, adjusted by the command's params. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -34,6 +34,10 @@ pub struct FlattenOptions { /// amounts). `None` for ordinary flattening. Not a preset option. #[serde(skip)] pub no_transparency: Option>, + /// The file writes a colorized image in its colorize paint (EPS), so a colorize paint by + /// itself doesn't count as transparency. Not a preset option. + #[serde(skip)] + pub keeps_colorize: bool, } impl Default for FlattenOptions { @@ -81,6 +85,7 @@ impl FlattenOptions { preserve_alpha: false, preserve_overprints: true, no_transparency: None, + keeps_colorize: false, } } @@ -138,6 +143,21 @@ impl FlattenOptions { Ok(o) } + /// Whether `n`, art of `d`, shows transparency these options flatten + /// ([`Node::shows_transparency`], colorized images as [`Self::counts_colorize`] says). + pub(crate) fn shows_transparency(&self, d: &Document, n: &Node) -> bool { + n.shows_transparency_with(&|c| self.counts_colorize(d, c)) + } + + /// Whether colorized image `n`, art of `d`, counts as transparency by itself. With + /// [`Self::keeps_colorize`], an image colorized with a solid paint whose pixels are grayscale + /// or 1-bit doesn't: the EPS writer draws it in that paint. + pub(crate) fn counts_colorize(&self, d: &Document, n: &Node) -> bool { + let NodeKind::Image(im) = &n.kind else { return true }; + let solid = matches!(n.extras().colorize.as_deref(), Some(Paint::Solid { .. })); + !self.keeps_colorize || !solid || d.image_blob(im).is_some_and(|b| b.gray_source().is_none()) + } + /// `n` shows an image whose see-through pixels count as transparency. pub(super) fn draws_see_through_image(&self, n: &Node) -> bool { self.no_transparency.as_ref().is_some_and(|keys| draws_image(n, keys)) diff --git a/crates/engine/src/tests/legacy_ai.rs b/crates/engine/src/tests/legacy_ai.rs index 793fe19d2..0a3f9af6f 100644 --- a/crates/engine/src/tests/legacy_ai.rs +++ b/crates/engine/src/tests/legacy_ai.rs @@ -546,7 +546,7 @@ fn a_colorized_grayscale_image_pattern_fill_is_expanded() { let keys = crate::cmd::flatten::see_through_images(d); assert!(keys.is_empty()); let layer = d.layers[0].id; - assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys).unwrap(), 1); + assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys, &|_, _| true).unwrap(), 1); let mut patterns = 0; let mut tile_images = 0; d.walk(|n| { @@ -688,7 +688,7 @@ fn only_the_transparent_pattern_fill_of_a_leaf_is_expanded() { let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); let keys = crate::cmd::flatten::see_through_images(d); let layer = d.layers[0].id; - assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys).unwrap(), 1); + assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys, &|_, _| true).unwrap(), 1); let mut patterns = vec![]; let mut images = 0; d.walk(|n| { @@ -745,11 +745,10 @@ fn eps_level3_writes_see_through_pattern_tile_images() { } #[test] -fn eps_flattens_colorized_pattern_tile_images_and_warns() { +fn eps_writes_colorized_pattern_tile_images_in_their_colors() { let mut s = colorized_pattern_over_blue(); let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)]; let r = s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": true, "level": 3})).unwrap(); - assert!(warnings(&r).iter().any(|w| w.contains("transparency is flattened")), "{:?}", warnings(&r)); s.execute("document.open", &json!({"name": "p.eps", "dataBase64": r["dataBase64"], "editingData": false})).unwrap(); let after = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)]; for (a, b) in before.iter().zip(after) { @@ -758,14 +757,31 @@ fn eps_flattens_colorized_pattern_tile_images_and_warns() { } #[test] -fn eps_warns_for_colorized_image_nested_in_pattern_tile_group() { +fn flattening_that_keeps_colorize_leaves_colorized_pattern_tile_images_alone() { + let s = colorized_pattern_over_blue(); + let d = &s.doc().unwrap().doc; + assert!(crate::cmd::flatten::flatten_document(d, &crate::cmd::FlattenOptions::default()).unwrap().is_some()); + let o = crate::cmd::FlattenOptions { keeps_colorize: true, ..Default::default() }; + assert!(crate::cmd::flatten::flatten_document(d, &o).unwrap().is_none(), "the pattern isn't expanded for a colorize paint alone"); +} + +#[test] +fn eps_flattens_colorized_image_in_a_see_through_pattern_tile_group_and_warns() { let mut s = colorized_pattern_over_blue(); let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); let id = d.alloc_id(); let art = std::mem::take(&mut d.patterns[0].art); - d.patterns[0].art.push(std::sync::Arc::new(vectorcraft_doc::Node::group(id, art))); + let mut group = vectorcraft_doc::Node::group(id, art); + group.opacity = 0.5; + d.patterns[0].art.push(std::sync::Arc::new(group)); + let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)]; let r = s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": true, "level": 3})).unwrap(); assert!(warnings(&r).iter().any(|w| w.contains("transparency is flattened")), "{:?}", warnings(&r)); + s.execute("document.open", &json!({"name": "p.eps", "dataBase64": r["dataBase64"], "editingData": false})).unwrap(); + let after = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)]; + for (a, b) in before.iter().zip(after) { + assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}"); + } } #[test]