Martin Slachta 397f4871ff SVG Export: Removed groups around every single element to reduce size.
Every element like path had group around it that defined it's transform.
These changes move the transformations to the element itself. This
reduces the overall size of the exported SVG.

Added new SVG path builder using relative coordinates. The previous with
global coordinates is still used for glyph paths. Using relative
coordinates allows to transform the entire element without changing the entire path.
2025-05-05 18:28:09 +02:00

76 lines
2.8 KiB
Rust

use base64::Engine;
use ecow::{eco_format, EcoString};
use image::{codecs::png::PngEncoder, ImageEncoder};
use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Axes, Transform};
use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
};
use crate::SVGRenderer;
impl SVGRenderer {
/// Render an image element.
pub(super) fn render_image(
&mut self,
transform: &Transform,
image: &Image,
size: &Axes<Abs>,
) {
let url = convert_image_to_base64_url(image);
self.xml.start_element("image");
self.xml.write_attribute("xlink:href", &url);
self.xml.write_attribute("x", &transform.tx.to_pt());
self.xml.write_attribute("y", &transform.ty.to_pt());
self.xml.write_attribute("width", &size.x.to_pt());
self.xml.write_attribute("height", &size.y.to_pt());
self.xml.write_attribute("preserveAspectRatio", "none");
match image.scaling() {
Smart::Auto => {}
Smart::Custom(ImageScaling::Smooth) => {
// This is still experimental and not implemented in all major browsers.
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
self.xml.write_attribute("style", "image-rendering: smooth")
}
Smart::Custom(ImageScaling::Pixelated) => {
self.xml.write_attribute("style", "image-rendering: pixelated")
}
}
self.xml.end_element();
}
}
/// Encode an image into a data URL. The format of the URL is
/// `data:image/{format};base64,`.
#[comemo::memoize]
pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
let mut buf;
let (format, data): (&str, &[u8]) = match image.kind() {
ImageKind::Raster(raster) => match raster.format() {
RasterFormat::Exchange(format) => (
match format {
ExchangeFormat::Png => "png",
ExchangeFormat::Jpg => "jpeg",
ExchangeFormat::Gif => "gif",
},
raster.data(),
),
RasterFormat::Pixel(_) => ("png", {
buf = vec![];
let mut encoder = PngEncoder::new(&mut buf);
if let Some(icc_profile) = raster.icc() {
encoder.set_icc_profile(icc_profile.to_vec()).ok();
}
raster.dynamic().write_with_encoder(encoder).unwrap();
buf.as_slice()
}),
},
ImageKind::Svg(svg) => ("svg+xml", svg.data()),
};
let mut url = eco_format!("data:image/{format};base64,");
let data = base64::engine::general_purpose::STANDARD.encode(data);
url.push_str(&data);
url
}