diff --git a/crates/effects/src/raster.rs b/crates/effects/src/raster.rs index 7d647ff8..3da990a5 100644 --- a/crates/effects/src/raster.rs +++ b/crates/effects/src/raster.rs @@ -52,6 +52,13 @@ impl RasterFx { pub fn is_below(&self) -> bool { matches!(self, RasterFx::DropShadow { .. } | RasterFx::OuterGlow { .. }) } + /// The blend mode it paints with (shadows and glows). + pub fn mode(&self) -> Option { + match self { + RasterFx::DropShadow { mode, .. } | RasterFx::OuterGlow { mode, .. } | RasterFx::InnerGlow { mode, .. } => Some(*mode), + _ => None, + } + } /// How far this effect reaches beyond `bounds`, what it paints around (Radial Blur reaches /// further around larger objects). pub fn outset(&self, bounds: Rect) -> f64 { diff --git a/crates/engine/src/cmd/rasterfx.rs b/crates/engine/src/cmd/rasterfx.rs index 60e213ee..77b97e31 100644 --- a/crates/engine/src/cmd/rasterfx.rs +++ b/crates/engine/src/cmd/rasterfx.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use serde_json::{Map, Value, json}; -use vectorcraft_color::Paint; +use vectorcraft_color::{BlendMode, Paint}; use vectorcraft_doc::rastersettings::MAX_ADD_AROUND; use vectorcraft_doc::{ Appearance, AppearanceItem, ColorMode, Document, Effect, ImageBlob, ImageObject, Node, NodeId, NodeKind, RasterColorModel, StrokeLayer, @@ -217,18 +217,55 @@ pub(crate) fn raster_fx(n: &Node) -> Vec { /// Remove the raster effects of `n` (its own and its fills' and strokes') and, with `members`, of /// everything inside it. pub(crate) fn strip_raster(n: &mut Node, members: bool) { - let keep = |e: &Effect| !effects::is_raster(&e.id); + retain_effects(n, members, &|e| !effects::is_raster(&e.id)); +} + +/// Keep the effects of `n` (its own and its fills' and strokes') that `keep` accepts and, with +/// `members`, of everything inside it. +fn retain_effects(n: &mut Node, members: bool, keep: &dyn Fn(&Effect) -> bool) { n.appearance.effects.retain(keep); for it in &mut n.appearance.items { it.effects_mut().retain(keep); } if members { for c in n.children_mut().into_iter().flatten() { - strip_raster(Arc::make_mut(c), true); + retain_effects(Arc::make_mut(c), true, keep); } } } +/// The blend mode raster effect `e` paints with (`None`: not a raster effect, or one without). +fn effect_mode(e: &Effect) -> Option { + effects::raster_effects(std::slice::from_ref(e)).first().and_then(RasterFx::mode) +} + +/// The images of the shadows and outer glows of `n`, one per blend mode they use (in the order +/// the modes first come), each composited with its mode: rendered alone, over nothing, they can't +/// blend with the art below the object, so the image does. +fn below_images(d: &mut Document, n: &Node) -> Option> { + let mut modes: Vec = vec![]; + for mode in raster_fx(n).iter().filter_map(RasterFx::mode) { + if !modes.contains(&mode) { + modes.push(mode); + } + } + if modes.len() < 2 { + let mut image = raster_image(d, n, true, false)?; + image.blend = modes.first().copied().unwrap_or_default(); + return Some(vec![image]); + } + modes + .into_iter() + .map(|mode| { + let mut only = n.clone(); + retain_effects(&mut only, false, &|e| effect_mode(e).is_none_or(|m| m == mode)); + let mut image = raster_image(d, &only, true, false)?; + image.blend = mode; + Some(image) + }) + .collect() +} + /// The raster effects of `n` (its own, its fills' and strokes') as an embedded image of `d` /// rendered at the document's raster effects resolution, without `n`'s transparency (that stays /// on the object). With `below` (they all paint below it: shadows and outer glows) the image holds @@ -405,15 +442,18 @@ fn walk(out: &mut Document, n: &Node, which: Which) -> Option { return Some(walk(out, &g, which).unwrap_or(g)); } // An image that can't be made leaves the object as it is (the writer reports its effects). - let image = raster_image(out, n, below, false)?; let mut v = n.clone(); strip_raster(&mut v, false); let mut m = if below { + let images = below_images(out, n)?; let mut m = walk(out, &v, which).unwrap_or(v); - put_below(out, &mut m, image); + // Each goes in first: the last one put is the lowest. + for image in images.into_iter().rev() { + put_below(out, &mut m, image); + } m } else { - replace_with_image(v, image) + replace_with_image(v, raster_image(out, n, false, false)?) }; walk_mask(out, &mut m, which); Some(m) diff --git a/crates/engine/src/tests_pdfraster.rs b/crates/engine/src/tests_pdfraster.rs index a6a63000..7f497df8 100644 --- a/crates/engine/src/tests_pdfraster.rs +++ b/crates/engine/src/tests_pdfraster.rs @@ -150,3 +150,35 @@ fn live_objects_and_single_fills_and_strokes_keep_their_raster_effects() { assert_eq!(pieces, 2, "item {item}: one piece per fill and stroke"); } } + +#[test] +fn shadows_and_glows_keep_their_blend_modes() { + // #827: a red Screen glow over grey can't darken it, as Normal would. + let glow = Effect { + id: "stylize.outerGlow".into(), + params: json!({"mode": "screen", "color": "#ff0000", "opacity": 100.0, "blur": 20.0}), + visible: true, + }; + let mut d = Document::new(200.0, 200.0); + let back = rect(&mut d, Rect::new(0.0, 0.0, 200.0, 200.0), Color::rgb(0.5, 0.5, 0.5)); + add(&mut d, back); + let dot = rect(&mut d, Rect::new(70.0, 70.0, 130.0, 130.0), Color::rgb(0.12, 0.12, 0.12)); + add(&mut d, with(dot, glow.clone())); + assert_written("screen glow", &d); + let r = crate::cmd::rasterfx::export_pdf_with_report(&d, &Default::default()).unwrap(); + let back = render(&vectorcraft_pdf::import(&r.bytes).unwrap()); + for x in [133, 138, 145] { + let i = (100 * back.width as usize + x) * 4; + let (red, green, blue) = (back.pixels[i], back.pixels[i + 1], back.pixels[i + 2]); + assert!(red > green && green >= 124 && blue >= 124, "x {x}: {:?}", (red, green, blue)); + } + + // A Multiply shadow and the Screen glow: an image each, composited with its mode. + let mut d = Document::new(200.0, 200.0); + let dot = rect(&mut d, Rect::new(70.0, 70.0, 130.0, 130.0), Color::rgb(0.12, 0.12, 0.12)); + add(&mut d, with(with(dot, shadow()), glow)); + let flat = crate::flatten_raster_effects(&d).unwrap(); + let modes: Vec<_> = + flat.layers[0].children().unwrap()[0].children().unwrap().iter().filter(|c| matches!(c.kind, NodeKind::Image(_))).map(|c| c.blend).collect(); + assert_eq!(modes, [vectorcraft_color::BlendMode::Multiply, vectorcraft_color::BlendMode::Screen]); +}