use std::sync::Arc;
use base64::Engine;
use ecow::{EcoString, eco_format};
use hayro::hayro_interpret::InterpreterSettings;
use hayro::hayro_interpret::font::{FontData, FontQuery, StandardFont};
use hayro_svg::{RenderCache, SvgRenderSettings};
use image::{ImageEncoder, codecs::png::PngEncoder};
use typst_library::foundations::{Bytes, Smart};
use typst_library::layout::{Abs, Axes};
use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat,
};
use crate::write::{SvgElem, SvgTransform, SvgWrite};
use crate::{SVGRenderer, State};
impl SVGRenderer<'_> {
pub(super) fn render_image(
&mut self,
svg: &mut SvgElem,
state: &State,
image: &Image,
size: &Axes<Abs>,
) {
let url = WebImage::new(image).to_base64_url();
let mut svg = svg.elem("image");
if !state.transform.is_identity() {
svg.attr("transform", SvgTransform(state.transform));
}
svg.attr("xlink:href", url.as_str());
svg.attr("width", size.x.to_pt());
svg.attr("height", size.y.to_pt());
svg.attr("preserveAspectRatio", "none");
if let Some(value) = convert_image_scaling(image.scaling()) {
svg.attr_with("style", |attr| {
attr.push_str("image-rendering: ");
attr.push_str(value);
});
}
}
}
pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> Option<&'static str> {
match scaling {
Smart::Auto => None,
Smart::Custom(ImageScaling::Smooth) => {
Some("smooth")
}
Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
}
}
#[derive(Debug, Clone, Hash)]
pub struct WebImage {
pub format: WebImageFormat,
pub data: Bytes,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum WebImageFormat {
Png,
Jpg,
Gif,
Webp,
Svg,
}
impl WebImageFormat {
pub fn mime(&self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpg => "image/jpeg",
Self::Gif => "image/gif",
Self::Webp => "image/webp",
Self::Svg => "image/svg+xml",
}
}
pub fn extension(&self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpg => "jpg",
Self::Gif => "gif",
Self::Webp => "webp",
Self::Svg => "svg",
}
}
}
impl WebImage {
#[comemo::memoize]
pub fn new(image: &Image) -> WebImage {
let (format, data) = match image.kind() {
ImageKind::Raster(raster) => match raster.format() {
RasterFormat::Exchange(format) => (
match format {
ExchangeFormat::Png => WebImageFormat::Png,
ExchangeFormat::Jpg => WebImageFormat::Jpg,
ExchangeFormat::Gif => WebImageFormat::Gif,
ExchangeFormat::Webp => WebImageFormat::Webp,
},
raster.data().clone(),
),
RasterFormat::Pixel(_) => (WebImageFormat::Png, {
let mut 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();
Bytes::new(buf)
}),
},
ImageKind::Svg(svg) => (WebImageFormat::Svg, svg.data().clone()),
ImageKind::Pdf(pdf) => {
(WebImageFormat::Svg, Bytes::from_string(pdf_to_svg(pdf)))
}
};
Self { format, data }
}
#[comemo::memoize]
pub fn to_base64_url(&self) -> EcoString {
let mut url = eco_format!("data:{};base64,", self.format.mime());
let data = base64::engine::general_purpose::STANDARD.encode(&self.data);
url.push_str(&data);
url
}
}
fn pdf_to_svg(pdf: &PdfImage) -> String {
let select_standard_font = move |font: StandardFont| -> Option<(FontData, u32)> {
let bytes = match font {
StandardFont::Helvetica => typst_assets::pdf::SANS,
StandardFont::HelveticaBold => typst_assets::pdf::SANS_BOLD,
StandardFont::HelveticaOblique => typst_assets::pdf::SANS_ITALIC,
StandardFont::HelveticaBoldOblique => typst_assets::pdf::SANS_BOLD_ITALIC,
StandardFont::Courier => typst_assets::pdf::FIXED,
StandardFont::CourierBold => typst_assets::pdf::FIXED_BOLD,
StandardFont::CourierOblique => typst_assets::pdf::FIXED_ITALIC,
StandardFont::CourierBoldOblique => typst_assets::pdf::FIXED_BOLD_ITALIC,
StandardFont::TimesRoman => typst_assets::pdf::SERIF,
StandardFont::TimesBold => typst_assets::pdf::SERIF_BOLD,
StandardFont::TimesItalic => typst_assets::pdf::SERIF_ITALIC,
StandardFont::TimesBoldItalic => typst_assets::pdf::SERIF_BOLD_ITALIC,
StandardFont::ZapfDingBats => typst_assets::pdf::DING_BATS,
StandardFont::Symbol => typst_assets::pdf::SYMBOL,
};
Some((Arc::new(bytes), 0))
};
let interpreter_settings = InterpreterSettings {
font_resolver: Arc::new(move |query| match query {
FontQuery::Standard(s) => select_standard_font(*s),
FontQuery::Fallback(f) => select_standard_font(f.pick_standard_font()),
}),
cmap_resolver: Arc::new(|_| None),
warning_sink: Arc::new(|_| {}),
render_annotations: false,
};
let cache = RenderCache::new();
hayro_svg::convert(
pdf.page(),
&cache,
&interpreter_settings,
&SvgRenderSettings { bg_color: [0, 0, 0, 0] },
)
}