Merge pull request #5859 from mockydevs/fix/inner-glow-image-pixels

Effects: an image's Inner Glow follows its visible pixels, not its frame (#5308)
This commit is contained in:
BFlat
2026-11-06 08:43:58 +08:00
committed by GitHub
3 changed files with 80 additions and 2 deletions
+38 -1
View File
@@ -697,11 +697,48 @@ impl Renderer {
} else {
paint(self, ctx, f);
}
// Above the content: inner glows, clipped to its outline.
// Above the content: inner glows, clipped to its outline. An image's glow follows its
// visible pixels, not its frame (#5308).
let Some((g, rule)) = content.outline else { return };
let pixels = matches!(content.node.kind, NodeKind::Image(_));
for fx in rfx {
let RasterFx::InnerGlow { mode, opacity, blur, color, center } = fx else { continue };
let filter = shadow_filter(0.0, 0.0, *blur, pcolor(f.ink, color));
if pixels {
self.with_filters(ctx, f, g.bounding_box(), *blur, Some((*mode, *opacity)), |r, c, fr, comp| {
if let Some((m, o)) = comp {
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(blend_mode(m)), Some(o), None, None);
}
c.set_transform(fr.view);
c.set_fill_rule(fill_rule(rule));
c.push_clip_layer(g);
c.push_layer(None, None, None, None, Some(filter));
if !*center {
// Everything around the visible pixels, so the glow bleeds in from their edges.
let pad = blur * 2.0 + 4.0 * fr.px;
c.set_transform(fr.view);
c.set_paint(peniko::Color::BLACK);
c.fill_rect(&g.bounding_box().inflate(pad, pad));
c.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut)), None, None, None);
}
// The glow is cast by the pixels' alpha alone (the filter takes no color from them).
paint(r, c, fr);
if !*center {
c.pop_layer();
}
c.pop_layer();
// Only where the image shows, as much as it shows.
c.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestIn)), None, None, None);
paint(r, c, fr);
c.pop_layer();
c.pop_layer();
if comp.is_some() {
c.pop_layer();
}
});
continue;
}
self.with_filters(ctx, f, g.bounding_box(), *blur, Some((*mode, *opacity)), |_, c, fr, comp| {
// The blend layer goes outside the clip: a clip layer is isolated, so a blend
// inside it would mix with nothing instead of the content below.
+41
View File
@@ -1076,3 +1076,44 @@ fn photoshop_filters_on_content_larger_than_the_budget() {
assert!(lum(img.pixel(190, 100)) >= WHITE - 3 * 25, "{id}: outside {:?}", img.pixel(190, 100));
}
}
/// #5308: an image's Inner Glow follows its visible pixels, not its frame. A 40 × 40 image whose
/// left half is transparent and right half opaque black, on a black page, with a white glow from
/// the edges: the transparent half stays black, and the glow lights the opaque half's left edge
/// (inside the frame), on the single-threaded and the multithreaded renderer alike.
#[test]
fn an_image_inner_glow_follows_its_visible_pixels() {
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
let rgba: Vec<u8> = (0..40 * 40).flat_map(|i| if i % 40 < 20 { [0, 0, 0, 0] } else { [0, 0, 0, 255] }).collect();
let png = crate::encode::png::encode(&rgba, 40, 40, &Default::default()).unwrap();
let mut d = Document::new(100.0, 100.0);
d.images.insert("half".into(), ImageBlob::new("image/png", png));
let im = ImageObject {
key: "half".into(),
width: 40,
height: 40,
xf: Affine::translate((30.0, 30.0)),
link: None,
placement: Default::default(),
pixels: None,
};
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
n.appearance.effects = vec![Effect {
id: "stylize.innerGlow".into(),
params: json!({"color": "#ffffff", "mode": "normal", "opacity": 100, "blur": 4}),
visible: true,
}];
d.insert(Some(d.layers[0].id), 0, n).unwrap();
let opts = RenderOptions { background: Some([0, 0, 0, 255]), ..Default::default() };
for threads in [0, 3] {
let mut r = Renderer::new();
r.threads = threads;
let img = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
for (x, y) in [(31, 50), (40, 31), (45, 50)] {
assert!(lum(img.pixel(x, y)) < 10, "threads {threads}: no glow on the transparent pixels at ({x}, {y}): {:?}", img.pixel(x, y));
}
assert!(lum(img.pixel(51, 50)) > 60, "threads {threads}: the glow lights the visible pixels' edge: {:?}", img.pixel(51, 50));
assert!(lum(img.pixel(68, 50)) > 60, "threads {threads}: and the frame's edge where the pixels reach it: {:?}", img.pixel(68, 50));
assert!(lum(img.pixel(60, 50)) < lum(img.pixel(51, 50)), "threads {threads}: fading inward");
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Properties: Quick Actions has Repeat Options for one selected repeat, #5181; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
## Feature submission evidence