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 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
45pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> Option<&'static str> {
47 match scaling {
48 Smart::Auto => None,
49 Smart::Custom(ImageScaling::Smooth) => {
50 Some("smooth")
53 }
54 Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
55 }
56}
57
58#[derive(Debug, Clone, Hash)]
60pub struct WebImage {
61 pub format: WebImageFormat,
62 pub data: Bytes,
63}
64
65#[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 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 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 #[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 #[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
145fn 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}