mirror of
https://github.com/penpot/penpot.git
synced 2026-10-28 21:40:34 +00:00
🐛 Include board drop shadow in multi-element raster export (#12440)
Headless raster/PDF used the board selrect so overflowing children stayed cropped, but that also clipped the board's own drop shadow. Use export_root_bounds: own strokes/shadows/blur without joining children. Closes #12388
This commit is contained in:
parent
c99980327f
commit
655300e4d1
@ -24,15 +24,9 @@ pub fn render_to_pdf(
|
||||
let shape = tree
|
||||
.get(id)
|
||||
.ok_or_else(|| crate::error::Error::CriticalError("Shape not found for PDF".to_string()))?;
|
||||
// Boards export at their own bounds (like the classic exporter, which
|
||||
// screenshots exactly the board box): content overflowing a board lands
|
||||
// outside the page/surface and is cropped. Other shapes keep the extended
|
||||
// rect so their own shadows/blur are included.
|
||||
let bounds = if matches!(shape.shape_type, crate::shapes::Type::Frame(_)) {
|
||||
shape.selrect()
|
||||
} else {
|
||||
shape.extrect(tree, scale)
|
||||
};
|
||||
// Boards crop overflowing children (classic board box) but keep their own
|
||||
// strokes/shadows/blur. Other shapes use full extrect.
|
||||
let bounds = shape.export_root_bounds(tree, scale);
|
||||
|
||||
let page_w = bounds.width() * scale;
|
||||
let page_h = bounds.height() * scale;
|
||||
|
||||
@ -79,15 +79,9 @@ pub fn render_to_raster(
|
||||
let Some(shape) = tree.get(id) else {
|
||||
return Ok((Vec::new(), 0, 0));
|
||||
};
|
||||
// Boards export at their own bounds (like the classic exporter, which
|
||||
// screenshots exactly the board box): content overflowing a board lands
|
||||
// outside the page/surface and is cropped. Other shapes keep the extended
|
||||
// rect so their own shadows/blur are included.
|
||||
let bounds = if matches!(shape.shape_type, crate::shapes::Type::Frame(_)) {
|
||||
shape.selrect()
|
||||
} else {
|
||||
shape.extrect(tree, scale)
|
||||
};
|
||||
// Boards crop overflowing children (classic board box) but keep their own
|
||||
// strokes/shadows/blur. Other shapes use full extrect.
|
||||
let bounds = shape.export_root_bounds(tree, scale);
|
||||
|
||||
let width = (bounds.width() * scale).ceil() as i32;
|
||||
let height = (bounds.height() * scale).ceil() as i32;
|
||||
|
||||
@ -3639,17 +3639,98 @@ fn group_with_empty_path(group_fills: Vec<Fill>) -> ShapesPool {
|
||||
}
|
||||
|
||||
fn raster_png(pool: &ShapesPool, id: Uuid) -> Vec<u8> {
|
||||
let (bytes, _, _) = raster_png_size(pool, id);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn raster_png_size(pool: &ShapesPool, id: Uuid) -> (Vec<u8>, i32, i32) {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let (bytes, _, _) = crate::render::raster::render_to_raster(
|
||||
crate::render::raster::render_to_raster(
|
||||
&mut resources,
|
||||
&id,
|
||||
pool,
|
||||
1.0,
|
||||
crate::render::raster::RasterFormat::Png,
|
||||
)
|
||||
.expect("raster export");
|
||||
bytes
|
||||
.expect("raster export")
|
||||
}
|
||||
|
||||
/// #12388: multi-element (headless) raster must include a board's own drop
|
||||
/// shadow, matching single-export sizing (spread 4 → +4px each side).
|
||||
#[test]
|
||||
fn raster_export_of_board_includes_own_drop_shadow_spread() {
|
||||
let mut pool = ShapesPool::new();
|
||||
let frame_id = uid(1);
|
||||
add_frame(
|
||||
&mut pool,
|
||||
frame_id,
|
||||
Uuid::nil(),
|
||||
(0.0, 0.0, 200.0, 120.0),
|
||||
skia::Color::from_rgb(240, 240, 240),
|
||||
true,
|
||||
);
|
||||
{
|
||||
let frame = pool.get_mut(&frame_id).unwrap();
|
||||
frame.add_shadow(Shadow::new(
|
||||
skia::Color::from_rgb(0, 200, 0),
|
||||
0.0,
|
||||
4.0,
|
||||
(0.0, 0.0),
|
||||
ShadowStyle::Drop,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
let (_, width, height) = raster_png_size(&pool, frame_id);
|
||||
assert_eq!(
|
||||
(width, height),
|
||||
(208, 128),
|
||||
"board raster must grow by drop-shadow spread, not stay at selrect"
|
||||
);
|
||||
}
|
||||
|
||||
/// Overflowing children stay cropped; only the board's own effects expand
|
||||
/// the headless raster page (classic exporter board-box rule).
|
||||
#[test]
|
||||
fn raster_export_of_board_crops_overflowing_child_but_keeps_own_shadow() {
|
||||
let mut pool = ShapesPool::new();
|
||||
let frame_id = uid(1);
|
||||
let child = uid(2);
|
||||
add_frame(
|
||||
&mut pool,
|
||||
frame_id,
|
||||
Uuid::nil(),
|
||||
(0.0, 0.0, 200.0, 120.0),
|
||||
skia::Color::from_rgb(240, 240, 240),
|
||||
true,
|
||||
);
|
||||
{
|
||||
let frame = pool.get_mut(&frame_id).unwrap();
|
||||
frame.add_shadow(Shadow::new(
|
||||
skia::Color::from_rgb(0, 200, 0),
|
||||
0.0,
|
||||
4.0,
|
||||
(0.0, 0.0),
|
||||
ShadowStyle::Drop,
|
||||
false,
|
||||
));
|
||||
frame.add_child(child);
|
||||
}
|
||||
add_solid_rect(
|
||||
&mut pool,
|
||||
child,
|
||||
frame_id,
|
||||
(150.0, 80.0, 280.0, 200.0),
|
||||
skia::Color::from_rgb(255, 0, 0),
|
||||
);
|
||||
|
||||
let (_, width, height) = raster_png_size(&pool, frame_id);
|
||||
assert_eq!(
|
||||
(width, height),
|
||||
(208, 128),
|
||||
"overflowing child must not grow board raster; own shadow still must"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -1148,10 +1148,28 @@ impl Shape {
|
||||
extrect
|
||||
}
|
||||
|
||||
fn own_extrect_bounds(&self) -> Bounds {
|
||||
pub(crate) fn own_extrect_bounds(&self) -> Bounds {
|
||||
self.expand_own_bounds(self.own_base_bounds(), true)
|
||||
}
|
||||
|
||||
/// Bounds for a headless export rooted at this shape.
|
||||
///
|
||||
/// Frames keep the classic board-box crop of overflowing children, but
|
||||
/// still expand by their own strokes / drop shadows / layer blur so those
|
||||
/// effects are not clipped (parity with single-export `extrect` for the
|
||||
/// board itself). Other shapes use full [`Self::extrect`].
|
||||
pub fn export_root_bounds(&self, tree: ShapesPoolRef, scale: f32) -> math::Rect {
|
||||
if matches!(self.shape_type, Type::Frame(_)) {
|
||||
let mut bounds = self.own_extrect_bounds();
|
||||
if !self.transform.is_identity() {
|
||||
bounds.transform_mut(&self.centered_transform());
|
||||
}
|
||||
bounds.to_rect()
|
||||
} else {
|
||||
self.extrect(tree, scale)
|
||||
}
|
||||
}
|
||||
|
||||
/// The shape's own geometry bounds, before stroke/shadow/blur margins.
|
||||
fn own_base_bounds(&self) -> Bounds {
|
||||
let shape = self;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user