mirror of
https://github.com/penpot/penpot.git
synced 2026-09-13 15:38:38 +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 super::{filters, RenderState, SurfaceId};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::get_resources;
|
use crate::get_resources;
|
||||||
use crate::math::Rect as MathRect;
|
use crate::render::{get_image_dest_rect, get_source_rect};
|
||||||
use crate::render::get_source_rect;
|
|
||||||
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
|
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
|
||||||
|
|
||||||
// Set the clipping area to the shape outline within the container bounds
|
// 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 container = &shape.selrect;
|
||||||
let sampling = get_resources().sampling_options;
|
let sampling = get_resources().sampling_options;
|
||||||
|
|
||||||
let dest_rect = match image_fill.transform() {
|
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||||
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 src_rect = get_source_rect(size, &dest_rect, 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);
|
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);
|
let fill_layer = skia::canvas::SaveLayerRec::default().paint(paint);
|
||||||
canvas.save_layer(&fill_layer);
|
canvas.save_layer(&fill_layer);
|
||||||
|
|
||||||
let dest_rect = match image_fill.transform() {
|
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||||
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 src_rect = get_source_rect(size, &dest_rect, image_fill);
|
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||||
if src_rect.width() <= 0.0 || src_rect.height() <= 0.0 {
|
if src_rect.width() <= 0.0 || src_rect.height() <= 0.0 {
|
||||||
canvas.restore();
|
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 {
|
pub fn get_source_rect(size: ISize, container: &MathRect, image_fill: &ImageFill) -> MathRect {
|
||||||
let image_width = size.width as f32;
|
let image_width = size.width as f32;
|
||||||
let image_height = size.height as f32;
|
let image_height = size.height as f32;
|
||||||
|
|||||||
@ -5,7 +5,8 @@ use crate::shapes::{Fill, ImageFill, Shape};
|
|||||||
use crate::state::ShapesPoolRef;
|
use crate::state::ShapesPoolRef;
|
||||||
|
|
||||||
use super::document::SvgLayerCanvas;
|
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.
|
/// Emits fills bottom -> top for SVG export.
|
||||||
///
|
///
|
||||||
@ -63,19 +64,20 @@ fn emit_image_fill(
|
|||||||
let clip_id = builder.unique("imgclip");
|
let clip_id = builder.unique("imgclip");
|
||||||
builder.push_clip_path(&clip_id, shape, tree);
|
builder.push_clip_path(&clip_id, shape, tree);
|
||||||
let href = xml_escape_attr(url);
|
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(())
|
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(
|
pub(super) fn emit_linked_image_element(
|
||||||
builder: &mut SvgLayerCanvas,
|
builder: &mut SvgLayerCanvas,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
image_fill: &ImageFill,
|
image_fill: &ImageFill,
|
||||||
|
dest_rect: MathRect,
|
||||||
href: &str,
|
href: &str,
|
||||||
clip_id: &str,
|
clip_id: &str,
|
||||||
) {
|
) {
|
||||||
let selrect = shape.selrect();
|
|
||||||
let opacity = image_fill.opacity() as f32 / 255.0;
|
let opacity = image_fill.opacity() as f32 / 255.0;
|
||||||
let preserve = if image_fill.keep_aspect_ratio() {
|
let preserve = if image_fill.keep_aspect_ratio() {
|
||||||
"xMidYMid slice"
|
"xMidYMid slice"
|
||||||
@ -93,10 +95,10 @@ pub(super) fn emit_linked_image_element(
|
|||||||
builder.open_group(&format!("clip-path=\"url(#{clip_id})\""));
|
builder.open_group(&format!("clip-path=\"url(#{clip_id})\""));
|
||||||
builder.push_raw(&format!(
|
builder.push_raw(&format!(
|
||||||
r#"<image href="{href}" x="{}" y="{}" width="{}" height="{}" preserveAspectRatio="{preserve}"{opacity_attr} transform="{transform}"/>"#,
|
r#"<image href="{href}" x="{}" y="{}" width="{}" height="{}" preserveAspectRatio="{preserve}"{opacity_attr} transform="{transform}"/>"#,
|
||||||
selrect.left(),
|
dest_rect.left(),
|
||||||
selrect.top(),
|
dest_rect.top(),
|
||||||
selrect.width(),
|
dest_rect.width(),
|
||||||
selrect.height(),
|
dest_rect.height(),
|
||||||
));
|
));
|
||||||
builder.close_group();
|
builder.close_group();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use super::fixtures::*;
|
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::state::ShapesPool;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
|
|
||||||
@ -1076,6 +1078,43 @@ fn exports_image_fill_as_linked_image() {
|
|||||||
insta::assert_snapshot!(svg);
|
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]
|
#[test]
|
||||||
fn exports_mixed_solid_and_image_fills_in_order() {
|
fn exports_mixed_solid_and_image_fills_in_order() {
|
||||||
// Image under a translucent solid; stretch (keep-aspect off); partial image
|
// 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);
|
builder.finish_clip_path_fragment(&clip_id, canvas);
|
||||||
|
|
||||||
let href = xml_escape_attr(url);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use super::shape_renderer::ShapeRenderer;
|
|||||||
use super::text;
|
use super::text;
|
||||||
use super::RenderResources;
|
use super::RenderResources;
|
||||||
use super::RenderState;
|
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
|
// VectorRenderer — implements ShapeRenderer for canvas-based vector export
|
||||||
@ -1043,8 +1043,8 @@ fn draw_image_fill(
|
|||||||
let size = image.dimensions();
|
let size = image.dimensions();
|
||||||
let container = &shape.selrect;
|
let container = &shape.selrect;
|
||||||
|
|
||||||
let src_rect = get_source_rect(size, container, image_fill);
|
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||||
let dest_rect = container;
|
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||||
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user