Merge pull request #884 from storytold/fix/827-raster-effect-blend

Fix #827: shadows and glows keep their blend modes in PDF
This commit is contained in:
BFlat 2026-10-10 10:46:45 +07:00 committed by GitHub
commit 1b398978ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 6 deletions

View File

@ -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<BlendMode> {
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 {

View File

@ -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<RasterFx> {
/// 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<BlendMode> {
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<Vec<Node>> {
let mut modes: Vec<BlendMode> = 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<Node> {
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)

View File

@ -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]);
}