mirror of
https://github.com/penpot/penpot.git
synced 2026-10-28 21:40:34 +00:00
🐛 Draw masked group inner shadows in PDF and multi-element export (#12468)
* 🐛 Fix render_to_pdf call in the ToUnicode test The test passed a scale argument that render_to_pdf does not take, so the render-wasm test build failed to compile. * 🐛 Draw masked group inner shadows in PDF and multi-element export The CPU renderer behind PDF and multi-element raster export drew a masked group's drop shadow but never its inner shadow, so the masked content came out flat. Wrap the masked content in a layer that draws the inner shadows over it, as the GPU single export does. * 🐛 Export every visible inner shadow of a masked group The workspace skips shadows too small to see at the current zoom, to render faster. Export has no such need, so draw every visible inner shadow instead of filtering by scale.
This commit is contained in:
parent
41d95abc5c
commit
ef68304cb8
@ -781,7 +781,7 @@ mod tests {
|
||||
.add(family, LIGATURE_FONT, false, false)
|
||||
.expect("ligature font");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let pdf = super::super::render_to_pdf(&mut resources, &id, &pool, 1.0).expect("pdf");
|
||||
let pdf = super::super::render_to_pdf(&mut resources, &id, &pool).expect("pdf");
|
||||
|
||||
let trailer = parse_xref(&pdf).expect("xref").trailer;
|
||||
assert!(
|
||||
|
||||
@ -3795,3 +3795,73 @@ fn raster_export_of_child_paints_inherited_group_fill() {
|
||||
assert_eq!(inherited_png, raster_png(&own, uid(2)));
|
||||
assert_ne!(inherited_png, raster_png(&bare, uid(2)));
|
||||
}
|
||||
|
||||
/// Reads one pixel of an encoded raster export as unpremultiplied RGBA.
|
||||
fn raster_pixel(png: &[u8], (x, y): (i32, i32)) -> skia::Color {
|
||||
let image = skia::Image::from_encoded(skia::Data::new_copy(png)).expect("decode png");
|
||||
let info = skia::ImageInfo::new(
|
||||
(1, 1),
|
||||
skia::ColorType::RGBA8888,
|
||||
skia::AlphaType::Unpremul,
|
||||
None,
|
||||
);
|
||||
let mut px = [0u8; 4];
|
||||
assert!(image.read_pixels(
|
||||
&info,
|
||||
&mut px,
|
||||
4,
|
||||
(x, y),
|
||||
skia::image::CachingHint::Disallow
|
||||
));
|
||||
skia::Color::from_argb(px[3], px[0], px[1], px[2])
|
||||
}
|
||||
|
||||
/// #12334: headless raster/PDF must draw a masked group's inner shadow
|
||||
/// inside the mask outline, as the GPU single export does.
|
||||
#[test]
|
||||
fn raster_export_of_masked_group_draws_inner_shadow() {
|
||||
let mut pool = ShapesPool::new();
|
||||
let group_id = uid(1);
|
||||
let mask_id = uid(2);
|
||||
let content_id = uid(3);
|
||||
let blue = skia::Color::from_rgb(0, 63, 255);
|
||||
|
||||
add_masked_group(
|
||||
&mut pool,
|
||||
group_id,
|
||||
Uuid::nil(),
|
||||
(0.0, 0.0, 100.0, 100.0),
|
||||
&[mask_id, content_id],
|
||||
);
|
||||
add_solid_circle(
|
||||
&mut pool,
|
||||
mask_id,
|
||||
group_id,
|
||||
(0.0, 0.0, 100.0, 100.0),
|
||||
skia::Color::BLACK,
|
||||
);
|
||||
add_solid_rect(
|
||||
&mut pool,
|
||||
content_id,
|
||||
group_id,
|
||||
(0.0, 0.0, 100.0, 100.0),
|
||||
blue,
|
||||
);
|
||||
pool.get_mut(&group_id).unwrap().add_shadow(Shadow::new(
|
||||
skia::Color::BLACK,
|
||||
0.0,
|
||||
0.0,
|
||||
(20.0, 0.0),
|
||||
ShadowStyle::Inner,
|
||||
false,
|
||||
));
|
||||
|
||||
let (png, width, height) = raster_png_size(&pool, group_id);
|
||||
assert_eq!((width, height), (100, 100));
|
||||
|
||||
// Left edge: the shifted silhouette leaves a band the shadow fills.
|
||||
assert_eq!(raster_pixel(&png, (8, 50)), skia::Color::BLACK);
|
||||
assert_eq!(raster_pixel(&png, (50, 50)), blue);
|
||||
// Outside the circle the mask still clears everything.
|
||||
assert_eq!(raster_pixel(&png, (3, 3)).a(), 0);
|
||||
}
|
||||
|
||||
@ -821,7 +821,22 @@ fn render_group(
|
||||
let child_opts = opts.for_children_of(element);
|
||||
|
||||
if masked {
|
||||
// Inner shadows filter the masked pixels, so they follow the mask's
|
||||
// outline. The canvas matrix already carries `scale`.
|
||||
let inner_shadows = element.masked_group_inner_shadow_filter();
|
||||
if let Some(filter) = &inner_shadows {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_image_filter(filter.clone());
|
||||
canvas.save_layer(
|
||||
&skia::canvas::SaveLayerRec::default()
|
||||
.bounds(&element.extrect(tree, scale))
|
||||
.paint(&paint),
|
||||
);
|
||||
}
|
||||
render_masked_children(shared, canvas, element, &children, tree, scale, &child_opts)?;
|
||||
if inner_shadows.is_some() {
|
||||
canvas.restore(); // inner shadow layer
|
||||
}
|
||||
} else {
|
||||
for child_id in &children {
|
||||
render_tree_inner(shared, canvas, child_id, tree, scale, &child_opts)?;
|
||||
|
||||
@ -1998,6 +1998,25 @@ impl Shape {
|
||||
})
|
||||
}
|
||||
|
||||
/// Source graphic with every visible inner shadow of the masked group over
|
||||
/// it, for the CPU export renderer; `None` without inner shadows.
|
||||
pub fn masked_group_inner_shadow_filter(&self) -> Option<skia::ImageFilter> {
|
||||
if !self.is_masked_group() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut layers: Vec<Option<skia::ImageFilter>> = vec![None];
|
||||
for shadow in self.inner_shadows_visible() {
|
||||
layers.push(shadow.get_inner_shadow_filter());
|
||||
}
|
||||
|
||||
if layers.len() > 1 {
|
||||
skia::image_filters::merge(layers, None)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Image filter for the `save_layer` that wraps a masked group, i.e. for
|
||||
/// the result *after* the mask has been composited into it.
|
||||
///
|
||||
@ -2390,6 +2409,28 @@ mod tests {
|
||||
math::Rect::from_ltrb(0.0, 0.0, 100.0, 100.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masked_group_inner_shadow_filter_needs_an_inner_shadow() {
|
||||
let drop_only = masked_group_with(vec![drop_shadow(4.0, 0.0, (10.0, 10.0))], None);
|
||||
assert!(drop_only.masked_group_inner_shadow_filter().is_none());
|
||||
|
||||
let inner = masked_group_with(vec![inner_shadow(4.0, (10.0, 10.0))], None);
|
||||
assert!(inner.masked_group_inner_shadow_filter().is_some());
|
||||
|
||||
let mut unmasked = inner.clone();
|
||||
unmasked.set_masked(false);
|
||||
assert!(unmasked.masked_group_inner_shadow_filter().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masked_group_inner_shadow_filter_keeps_imperceptible_shadows() {
|
||||
// Too small for the workspace filter at 1x, but export draws it.
|
||||
let shape = masked_group_with(vec![inner_shadow(1.0, (0.0, 0.0))], None);
|
||||
|
||||
assert!(shape.masked_group_layer_filter(1.0, false, false).is_none());
|
||||
assert!(shape.masked_group_inner_shadow_filter().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masked_group_layer_filter_is_none_without_effects() {
|
||||
let shape = masked_group_with(vec![], None);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user