svg export: embed each image's bytes once per file (#2250)

This commit is contained in:
Dylan M. Taylor
2026-11-11 14:49:20 -05:00
committed by GitHub
parent 64e91207f1
commit 9032dc3e67
3 changed files with 37 additions and 13 deletions
+10
View File
@@ -57,3 +57,13 @@ fn a_transparent_images_inner_shadow_is_an_embedded_render() {
let svg = export(&d, n);
assert!(svg.contains("<pattern") && svg.contains("<mask"), "an opaque picture stays vector, shaded from its box: {svg}");
}
#[test]
fn paints_of_one_image_embed_its_bytes_once() {
let (mut d, n) = scene(false, EffectKind::DropShadow(shadow()));
let node = d.get_mut(n).unwrap();
node.fills = vec![Paint::image("svg-image-shadow-opaque", ImageScaleMode::Fill); 3];
let svg = export(&d, n);
assert_eq!(svg.matches("<image id=").count(), 1, "{svg}");
assert_eq!(svg.matches("<pattern").count(), 3);
}
+26 -13
View File
@@ -93,6 +93,8 @@ struct W<'a> {
used_ids: std::collections::BTreeSet<String>,
/// The next `_k` suffix to try per base id, so repeated names don't rescan from `_2`.
next_suffix: std::collections::HashMap<String, usize>,
/// The `<image>` written to `defs` per image key, so paints of one image share its bytes.
images: std::collections::HashMap<String, String>,
}
/// ` clip-path="url(#…)"` for a clip path id (else nothing).
@@ -269,17 +271,29 @@ impl W<'_> {
PaintKind::Image(img) => {
let data = self.doc.images.get(&img.image)?;
let pid = self.id("pattern");
let iid = self.id("image");
// Formats SVG viewers don't show (TIFF, BMP) go in as PNG.
let png;
let (mime, bytes) = match sniff_mime(&data.bytes) {
"application/octet-stream" => {
png = figcraft_render::images::decode(&data.bytes).and_then(|p| figcraft_render::encode_png(&p).ok())?;
("image/png", png.as_slice())
let iid = match self.images.get(&img.image) {
Some(iid) => iid.clone(),
None => {
let iid = self.id("image");
// Formats SVG viewers don't show (TIFF, BMP) go in as PNG.
let png;
let (mime, bytes) = match sniff_mime(&data.bytes) {
"application/octet-stream" => {
png = figcraft_render::images::decode(&data.bytes).and_then(|p| figcraft_render::encode_png(&p).ok())?;
("image/png", png.as_slice())
}
m => (m, data.bytes.as_slice()),
};
let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
let _ = write!(
self.defs,
"<image id=\"{iid}\" width=\"{}\" height=\"{}\" href=\"data:{mime};base64,{b64}\"/>",
data.width, data.height
);
self.images.insert(img.image.clone(), iid.clone());
iid
}
m => (m, data.bytes.as_slice()),
};
let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
let t = figcraft_render::paint::image_transform(img, f64::from(data.width), f64::from(data.height), w, h);
let m = Affine::new([f64::from(t.sx), f64::from(t.ky), f64::from(t.kx), f64::from(t.sy), f64::from(t.tx), f64::from(t.ty)]);
let (pw, ph) = if img.scale_mode == figcraft_color::ImageScaleMode::Tile {
@@ -289,12 +303,10 @@ impl W<'_> {
};
let _ = write!(
self.defs,
"<pattern id=\"{pid}\" patternUnits=\"userSpaceOnUse\" width=\"{}\" height=\"{}\" patternTransform=\"{}\"><image id=\"{iid}\" width=\"{}\" height=\"{}\" href=\"data:{mime};base64,{b64}\"/></pattern>",
"<pattern id=\"{pid}\" patternUnits=\"userSpaceOnUse\" width=\"{}\" height=\"{}\" patternTransform=\"{}\"><use href=\"#{iid}\"/></pattern>",
num(pw),
num(ph),
matrix(m),
data.width,
data.height
matrix(m)
);
let op = if p.opacity < 0.999 { format!(" fill-opacity=\"{}\"", num(f64::from(p.opacity))) } else { String::new() };
Some(format!("fill=\"url(#{pid})\"{op}{blend}"))
@@ -1148,6 +1160,7 @@ pub fn try_export_svg(doc: &Document, ids: &[NodeId], opts: &SvgOptions) -> Resu
clip: None,
used_ids: Default::default(),
next_suffix: Default::default(),
images: Default::default(),
};
for id in ids {
let parent_xf = doc.node(*id).and_then(|n| n.parent).map(|p| doc.world_transform(p)).unwrap_or(Affine::IDENTITY);
+1
View File
@@ -294,6 +294,7 @@ fn unique_id_is_linear_for_repeated_names() {
clip: None,
used_ids: Default::default(),
next_suffix: Default::default(),
images: Default::default(),
};
assert_eq!(w.unique_id("a"), "a");
assert_eq!(w.unique_id("a_3"), "a_3");