🐛 Exclude inner shadows from WebGL export bounds (#12427)

Inner shadows were joined into extrect via the drop-shadow filter, so a
Y offset grew the PNG/SVG export surface. Only drop
shadows expand bounds, matching classic export. Closes #12386.
This commit is contained in:
Alejandro Alonso 2026-10-19 17:23:26 +02:00 committed by GitHub
parent fd03ce2a63
commit 29cd50621d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 15 deletions

View File

@ -3,6 +3,6 @@ source: src/render/svg/tests.rs
expression: svg
---
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="123.78461" height="103.78461" viewBox="0 0 123.78461 103.78461"><defs><filter id="fx0" filterUnits="userSpaceOnUse" x="0" y="0" width="123.78461" height="103.78461" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset in="hardAlpha" dx="2" dy="3" result="off"/><feGaussianBlur in="off" stdDeviation="3.9641016" result="blurred"/><feComposite in="blurred" in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" result="shadow"/><feColorMatrix in="shadow" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7058824 0" result="colored"/><feBlend mode="normal" in="colored" in2="shape" result="inner0"/></filter></defs><g filter="url(#fx0)">
<rect fill="#0080FF" transform="translate(9.8923 8.8923)" width="100" height="80"/>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80"><defs><filter id="fx0" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="80" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset in="hardAlpha" dx="2" dy="3" result="off"/><feGaussianBlur in="off" stdDeviation="3.9641016" result="blurred"/><feComposite in="blurred" in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" result="shadow"/><feColorMatrix in="shadow" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7058824 0" result="colored"/><feBlend mode="normal" in="colored" in2="shape" result="inner0"/></filter></defs><g filter="url(#fx0)">
<rect fill="#0080FF" width="100" height="80"/>
</g></svg>

View File

@ -1005,16 +1005,14 @@ impl Shape {
let max_stroke = Stroke::max_bounds_width(self.strokes.iter(), self.is_open());
let mut rect = bounds.to_rect();
for shadow in self.shadows_visible() {
if !shadow.hidden() {
if let Some(filter) = shadow.get_drop_shadow_filter() {
let mut source = rect;
if mitered && shadow.spread > 0.0 {
let tip = (MITER_LIMIT - 1.0) * (max_stroke + shadow.spread);
source.outset((tip, tip));
}
rect.join(filter.compute_fast_bounds(source));
for shadow in self.drop_shadows_visible() {
if let Some(filter) = shadow.get_drop_shadow_filter() {
let mut source = rect;
if mitered && shadow.spread > 0.0 {
let tip = (MITER_LIMIT - 1.0) * (max_stroke + shadow.spread);
source.outset((tip, tip));
}
rect.join(filter.compute_fast_bounds(source));
}
}
Bounds::from_rect(&rect)
@ -1514,10 +1512,6 @@ impl Shape {
.filter(|shadow| shadow.style() == ShadowStyle::Inner && !shadow.hidden())
}
pub fn shadows_visible(&self) -> impl DoubleEndedIterator<Item = &Shadow> {
self.shadows.iter().rev().filter(|shadow| !shadow.hidden())
}
pub fn to_path_transform(&self) -> Option<Matrix> {
match self.shape_type {
Type::Path(_) | Type::Bool(_) => {
@ -2571,6 +2565,35 @@ mod tests {
assert!(extrect.bottom > 50.0 + 20.0);
}
/// Inner shadows are drawn inside the shape; they must not inflate export
/// size (WebGL PNG surface uses `extrect`). Regression for #12386.
#[test]
fn leaf_extrect_ignores_inner_shadow_offset() {
let mut pool = ShapesPool::new();
pool.initialize(1);
let shape_id = Uuid::new_v4();
{
let shape = pool.add_shape(shape_id);
shape.set_shape_type(Type::Rect(Rect::default()));
shape.set_selrect(0.0, 0.0, 200.0, 200.0);
shape.add_shadow(inner_shadow(0.0, (0.0, 1.0)));
}
let shape = pool.get(&shape_id).expect("shape should exist");
let baseline = {
let mut no_shadow = shape.clone();
no_shadow.clear_shadows();
no_shadow.calculate_extrect(&pool, 1.0)
};
let extrect = shape.calculate_extrect(&pool, 1.0);
assert_eq!(extrect.height(), baseline.height());
assert_eq!(extrect.width(), baseline.width());
assert_eq!(extrect.height(), 200.0);
assert_eq!(extrect.width(), 200.0);
}
#[test]
fn test_apply_transform() {
let mut shape = Shape::new(Uuid::new_v4());

View File

@ -87,6 +87,10 @@ impl Shadow {
}
pub fn get_drop_shadow_filter(&self) -> Option<ImageFilter> {
if self.style == ShadowStyle::Inner {
return None;
}
let sigma = radius_to_sigma(self.blur);
let mut filter = image_filters::drop_shadow_only(
(self.offset.0, self.offset.1),