mirror of
https://github.com/penpot/penpot.git
synced 2026-09-11 22:49:30 +00:00
🔧 Support independent image bounds on wasm export (#11590)
This commit is contained in:
parent
6586631293
commit
61caefbcf3
@ -3,8 +3,7 @@ use skia_safe::{self as skia, Paint, RRect};
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::error::Result;
|
||||
use crate::get_resources;
|
||||
use crate::math::Rect as MathRect;
|
||||
use crate::render::get_source_rect;
|
||||
use crate::render::{get_image_dest_rect, get_source_rect};
|
||||
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
|
||||
|
||||
// Set the clipping area to the shape outline within the container bounds
|
||||
@ -94,15 +93,7 @@ fn draw_image_fill(
|
||||
let container = &shape.selrect;
|
||||
let sampling = get_resources().sampling_options;
|
||||
|
||||
let dest_rect = match image_fill.transform() {
|
||||
Some(tf) => MathRect::from_xywh(
|
||||
container.left + tf.x * container.width(),
|
||||
container.top + tf.y * container.height(),
|
||||
tf.width * container.width(),
|
||||
tf.height * container.height(),
|
||||
),
|
||||
None => *container,
|
||||
};
|
||||
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||
let needs_clip = image_fill.transform().is_some() || !is_axis_aligned_image_rect(shape);
|
||||
|
||||
@ -189,16 +180,7 @@ fn draw_svg_image_fill(
|
||||
let fill_layer = skia::canvas::SaveLayerRec::default().paint(paint);
|
||||
canvas.save_layer(&fill_layer);
|
||||
|
||||
let dest_rect = match image_fill.transform() {
|
||||
Some(tf) => MathRect::from_xywh(
|
||||
container.left + tf.x * container.width(),
|
||||
container.top + tf.y * container.height(),
|
||||
tf.width * container.width(),
|
||||
tf.height * container.height(),
|
||||
),
|
||||
None => *container,
|
||||
};
|
||||
|
||||
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||
if src_rect.width() <= 0.0 || src_rect.height() <= 0.0 {
|
||||
canvas.restore();
|
||||
|
||||
@ -20,6 +20,18 @@ pub fn get_dest_rect(container: &MathRect, delta: f32) -> MathRect {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_image_dest_rect(container: &MathRect, image_fill: &ImageFill) -> MathRect {
|
||||
match image_fill.transform() {
|
||||
Some(tf) => MathRect::from_xywh(
|
||||
container.left + tf.x * container.width(),
|
||||
container.top + tf.y * container.height(),
|
||||
tf.width * container.width(),
|
||||
tf.height * container.height(),
|
||||
),
|
||||
None => *container,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_source_rect(size: ISize, container: &MathRect, image_fill: &ImageFill) -> MathRect {
|
||||
let image_width = size.width as f32;
|
||||
let image_height = size.height as f32;
|
||||
|
||||
@ -5,7 +5,8 @@ use crate::shapes::{Fill, ImageFill, Shape};
|
||||
use crate::state::ShapesPoolRef;
|
||||
|
||||
use super::document::SvgLayerCanvas;
|
||||
use crate::render::RenderResources;
|
||||
use crate::math::Rect as MathRect;
|
||||
use crate::render::{get_image_dest_rect, RenderResources};
|
||||
|
||||
/// Emits fills bottom -> top for SVG export.
|
||||
///
|
||||
@ -63,19 +64,20 @@ fn emit_image_fill(
|
||||
let clip_id = builder.unique("imgclip");
|
||||
builder.push_clip_path(&clip_id, shape, tree);
|
||||
let href = xml_escape_attr(url);
|
||||
emit_linked_image_element(builder, shape, image_fill, &href, &clip_id);
|
||||
let dest_rect = get_image_dest_rect(&shape.selrect(), image_fill);
|
||||
emit_linked_image_element(builder, shape, image_fill, dest_rect, &href, &clip_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits `<g clip-path>` + `<image href>` using the shape selrect and page CTM.
|
||||
/// Emits `<g clip-path>` + `<image href>` at `dest_rect`, under the page CTM.
|
||||
pub(super) fn emit_linked_image_element(
|
||||
builder: &mut SvgLayerCanvas,
|
||||
shape: &Shape,
|
||||
image_fill: &ImageFill,
|
||||
dest_rect: MathRect,
|
||||
href: &str,
|
||||
clip_id: &str,
|
||||
) {
|
||||
let selrect = shape.selrect();
|
||||
let opacity = image_fill.opacity() as f32 / 255.0;
|
||||
let preserve = if image_fill.keep_aspect_ratio() {
|
||||
"xMidYMid slice"
|
||||
@ -93,10 +95,10 @@ pub(super) fn emit_linked_image_element(
|
||||
builder.open_group(&format!("clip-path=\"url(#{clip_id})\""));
|
||||
builder.push_raw(&format!(
|
||||
r#"<image href="{href}" x="{}" y="{}" width="{}" height="{}" preserveAspectRatio="{preserve}"{opacity_attr} transform="{transform}"/>"#,
|
||||
selrect.left(),
|
||||
selrect.top(),
|
||||
selrect.width(),
|
||||
selrect.height(),
|
||||
dest_rect.left(),
|
||||
dest_rect.top(),
|
||||
dest_rect.width(),
|
||||
dest_rect.height(),
|
||||
));
|
||||
builder.close_group();
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use super::fixtures::*;
|
||||
|
||||
use crate::shapes::{BlendMode, Fill, ImageFill, SolidColor, StrokeCap, StrokeKind};
|
||||
use crate::shapes::{
|
||||
BlendMode, Fill, ImageFill, ImageFillTransform, SolidColor, StrokeCap, StrokeKind,
|
||||
};
|
||||
use crate::state::ShapesPool;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
@ -1076,6 +1078,43 @@ fn exports_image_fill_as_linked_image() {
|
||||
insta::assert_snapshot!(svg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_image_fill_bounds_transform() {
|
||||
let mut pool = ShapesPool::new();
|
||||
let id = uid(1);
|
||||
let image_id = uid(42);
|
||||
add_rect_with_fills(
|
||||
&mut pool,
|
||||
id,
|
||||
Uuid::nil(),
|
||||
(0.0, 0.0, 100.0, 80.0),
|
||||
vec![Fill::Image(ImageFill::new_with_transform(
|
||||
image_id,
|
||||
255,
|
||||
200,
|
||||
100,
|
||||
false,
|
||||
Some(ImageFillTransform {
|
||||
x: 0.25,
|
||||
y: 0.5,
|
||||
width: 0.5,
|
||||
height: 0.25,
|
||||
}),
|
||||
))],
|
||||
);
|
||||
|
||||
let svg = render_with(&pool, id, |resources| {
|
||||
resources
|
||||
.images
|
||||
.set_source_url(image_id, TEST_IMAGE_URL.to_string());
|
||||
});
|
||||
|
||||
assert!(
|
||||
svg.contains(r#"x="25" y="40" width="50" height="20""#),
|
||||
"linked image must keep the independent image bounds: {svg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_mixed_solid_and_image_fills_in_order() {
|
||||
// Image under a translucent solid; stretch (keep-aspect off); partial image
|
||||
|
||||
@ -117,6 +117,6 @@ fn emit_text_image_fill(
|
||||
builder.finish_clip_path_fragment(&clip_id, canvas);
|
||||
|
||||
let href = xml_escape_attr(url);
|
||||
emit_linked_image_element(builder, shape, image_fill, &href, &clip_id);
|
||||
emit_linked_image_element(builder, shape, image_fill, shape.selrect(), &href, &clip_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ use super::shape_renderer::ShapeRenderer;
|
||||
use super::text;
|
||||
use super::RenderResources;
|
||||
use super::RenderState;
|
||||
use super::{get_dest_rect, get_source_rect};
|
||||
use super::{get_dest_rect, get_image_dest_rect, get_source_rect};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VectorRenderer — implements ShapeRenderer for canvas-based vector export
|
||||
@ -1043,8 +1043,8 @@ fn draw_image_fill(
|
||||
let size = image.dimensions();
|
||||
let container = &shape.selrect;
|
||||
|
||||
let src_rect = get_source_rect(size, container, image_fill);
|
||||
let dest_rect = container;
|
||||
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||
|
||||
canvas.save();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user