Skip to main content

typst_svg/
image.rs

1use std::sync::Arc;
2
3use base64::Engine;
4use ecow::{EcoString, eco_format};
5use hayro::hayro_interpret::InterpreterSettings;
6use hayro::hayro_interpret::font::{FontData, FontQuery, StandardFont};
7use hayro_svg::{RenderCache, SvgRenderSettings};
8use image::{ImageEncoder, codecs::png::PngEncoder};
9use typst_library::foundations::{Bytes, Smart};
10use typst_library::layout::{Abs, Axes};
11use typst_library::visualize::{
12    ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat,
13};
14
15use crate::write::{SvgElem, SvgTransform, SvgWrite};
16use crate::{SVGRenderer, State};
17
18impl SVGRenderer<'_> {
19    /// Render an image element.
20    pub(super) fn render_image(
21        &mut self,
22        svg: &mut SvgElem,
23        state: &State,
24        image: &Image,
25        size: &Axes<Abs>,
26    ) {
27        let url = WebImage::new(image).to_base64_url();
28        let mut svg = svg.elem("image");
29        if !state.transform.is_identity() {
30            svg.attr("transform", SvgTransform(state.transform));
31        }
32        svg.attr("xlink:href", url.as_str());
33        svg.attr("width", size.x.to_pt());
34        svg.attr("height", size.y.to_pt());
35        svg.attr("preserveAspectRatio", "none");
36        if let Some(value) = convert_image_scaling(image.scaling()) {
37            svg.attr_with("style", |attr| {
38                attr.push_str("image-rendering: ");
39                attr.push_str(value);
40            });
41        }
42    }
43}
44
45/// Converts an image scaling to a CSS `image-rendering` property value.
46pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> Option<&'static str> {
47    match scaling {
48        Smart::Auto => None,
49        Smart::Custom(ImageScaling::Smooth) => {
50            // This is still experimental and not implemented in all major browsers.
51            // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
52            Some("smooth")
53        }
54        Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
55    }
56}
57
58/// An image that is prepared for use in the web platform (SVG or HTML).
59#[derive(Debug, Clone, Hash)]
60pub struct WebImage {
61    pub format: WebImageFormat,
62    pub data: Bytes,
63}
64
65/// An image format supported on the web.
66#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
67#[non_exhaustive]
68pub enum WebImageFormat {
69    Png,
70    Jpg,
71    Gif,
72    Webp,
73    Svg,
74}
75
76impl WebImageFormat {
77    /// The mime type for this format.
78    pub fn mime(&self) -> &'static str {
79        match self {
80            Self::Png => "image/png",
81            Self::Jpg => "image/jpeg",
82            Self::Gif => "image/gif",
83            Self::Webp => "image/webp",
84            Self::Svg => "image/svg+xml",
85        }
86    }
87
88    /// The canonical extension used for this format.
89    pub fn extension(&self) -> &'static str {
90        match self {
91            Self::Png => "png",
92            Self::Jpg => "jpg",
93            Self::Gif => "gif",
94            Self::Webp => "webp",
95            Self::Svg => "svg",
96        }
97    }
98}
99
100impl WebImage {
101    /// Prepare an image for embedding in SVG or HTML.
102    #[comemo::memoize]
103    pub fn new(image: &Image) -> WebImage {
104        let (format, data) = match image.kind() {
105            ImageKind::Raster(raster) => match raster.format() {
106                RasterFormat::Exchange(format) => (
107                    match format {
108                        ExchangeFormat::Png => WebImageFormat::Png,
109                        ExchangeFormat::Jpg => WebImageFormat::Jpg,
110                        ExchangeFormat::Gif => WebImageFormat::Gif,
111                        ExchangeFormat::Webp => WebImageFormat::Webp,
112                    },
113                    raster.data().clone(),
114                ),
115                RasterFormat::Pixel(_) => (WebImageFormat::Png, {
116                    let mut buf = vec![];
117                    let mut encoder = PngEncoder::new(&mut buf);
118                    if let Some(icc_profile) = raster.icc() {
119                        encoder.set_icc_profile(icc_profile.to_vec()).ok();
120                    }
121                    raster.dynamic().write_with_encoder(encoder).unwrap();
122                    Bytes::new(buf)
123                }),
124            },
125            ImageKind::Svg(svg) => (WebImageFormat::Svg, svg.data().clone()),
126            ImageKind::Pdf(pdf) => {
127                (WebImageFormat::Svg, Bytes::from_string(pdf_to_svg(pdf)))
128            }
129        };
130        Self { format, data }
131    }
132
133    /// Turns the image into a base64 URL.
134    ///
135    /// The format of the URL is `data:{mime};base64,`.
136    #[comemo::memoize]
137    pub fn to_base64_url(&self) -> EcoString {
138        let mut url = eco_format!("data:{};base64,", self.format.mime());
139        let data = base64::engine::general_purpose::STANDARD.encode(&self.data);
140        url.push_str(&data);
141        url
142    }
143}
144
145// Keep this in sync with `typst-png`!
146fn pdf_to_svg(pdf: &PdfImage) -> String {
147    let select_standard_font = move |font: StandardFont| -> Option<(FontData, u32)> {
148        let bytes = match font {
149            StandardFont::Helvetica => typst_assets::pdf::SANS,
150            StandardFont::HelveticaBold => typst_assets::pdf::SANS_BOLD,
151            StandardFont::HelveticaOblique => typst_assets::pdf::SANS_ITALIC,
152            StandardFont::HelveticaBoldOblique => typst_assets::pdf::SANS_BOLD_ITALIC,
153            StandardFont::Courier => typst_assets::pdf::FIXED,
154            StandardFont::CourierBold => typst_assets::pdf::FIXED_BOLD,
155            StandardFont::CourierOblique => typst_assets::pdf::FIXED_ITALIC,
156            StandardFont::CourierBoldOblique => typst_assets::pdf::FIXED_BOLD_ITALIC,
157            StandardFont::TimesRoman => typst_assets::pdf::SERIF,
158            StandardFont::TimesBold => typst_assets::pdf::SERIF_BOLD,
159            StandardFont::TimesItalic => typst_assets::pdf::SERIF_ITALIC,
160            StandardFont::TimesBoldItalic => typst_assets::pdf::SERIF_BOLD_ITALIC,
161            StandardFont::ZapfDingBats => typst_assets::pdf::DING_BATS,
162            StandardFont::Symbol => typst_assets::pdf::SYMBOL,
163        };
164        Some((Arc::new(bytes), 0))
165    };
166
167    let interpreter_settings = InterpreterSettings {
168        font_resolver: Arc::new(move |query| match query {
169            FontQuery::Standard(s) => select_standard_font(*s),
170            FontQuery::Fallback(f) => select_standard_font(f.pick_standard_font()),
171        }),
172        cmap_resolver: Arc::new(|_| None),
173        warning_sink: Arc::new(|_| {}),
174        render_annotations: false,
175    };
176
177    let cache = RenderCache::new();
178    hayro_svg::convert(
179        pdf.page(),
180        &cache,
181        &interpreter_settings,
182        &SvgRenderSettings { bg_color: [0, 0, 0, 0] },
183    )
184}