🔧 Support independent image bounds on wasm export (#11590)

This commit is contained in:
Elena Torró 2026-09-09 18:27:22 +02:00 committed by GitHub
parent 6586631293
commit 61caefbcf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 34 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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();
}

View File

@ -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

View File

@ -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(())
}

View File

@ -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();