kroki_rs/utils/
image_converter.rs

1use anyhow::{Context, Result};
2use image::{codecs::webp::WebPEncoder, ImageEncoder};
3use resvg::{tiny_skia, usvg};
4use std::io::Cursor;
5
6/// Represents WebP quality configuration.
7#[derive(Debug, Clone, Copy)]
8pub enum WebpQuality {
9    /// Lossless encoding — larger file, perfect quality.
10    Lossless,
11    /// Lossy encoding — quality 0 (worst) to 100 (best).
12    Lossy(u8),
13}
14
15/// Creates a WebP encoder with the specified quality setting.
16fn create_webp_encoder(
17    output: &mut Cursor<Vec<u8>>,
18    quality: WebpQuality,
19) -> WebPEncoder<&mut Cursor<Vec<u8>>> {
20    match quality {
21        WebpQuality::Lossless => WebPEncoder::new_lossless(output),
22        WebpQuality::Lossy(_q) => {
23            tracing::warn!("Lossy WebP requested but not yet supported (image crate limitation). Falling back to lossless.");
24            WebPEncoder::new_lossless(output)
25        }
26    }
27}
28
29/// Converts SVG bytes to WebP format.
30pub async fn svg_to_webp(
31    svg_bytes: &[u8],
32    quality: WebpQuality,
33    fonts: &[String],
34    cache_dir: Option<&std::path::Path>,
35) -> Result<Vec<u8>> {
36    let mut opt = usvg::Options::default();
37    opt.fontdb_mut().load_system_fonts();
38
39    if !fonts.is_empty() {
40        let font_mgr = crate::utils::font_manager::FontManager::new(cache_dir)?;
41        let downloaded_dir = font_mgr.prepare_fonts(fonts).await?;
42        opt.fontdb_mut().load_fonts_dir(&downloaded_dir);
43    }
44
45    let tree = usvg::Tree::from_data(svg_bytes, &opt)
46        .context("Failed to parse SVG for WebP conversion")?;
47
48    let size = tree.size();
49    let mut pixmap = tiny_skia::Pixmap::new(size.width() as u32, size.height() as u32)
50        .context("Failed to allocate Pixmap for WebP conversion")?;
51
52    resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
53
54    let mut output = Cursor::new(Vec::new());
55
56    let (width, height) = (pixmap.width(), pixmap.height());
57    let data = pixmap.data(); // RGBA bytes
58
59    let encoder = create_webp_encoder(&mut output, quality);
60    encoder
61        .write_image(data, width, height, image::ExtendedColorType::Rgba8)
62        .context("Failed to encode WebP")?;
63
64    Ok(output.into_inner())
65}
66
67/// Converts PNG bytes to WebP format.
68pub async fn png_to_webp(png_bytes: &[u8], quality: WebpQuality) -> Result<Vec<u8>> {
69    let img = image::load_from_memory_with_format(png_bytes, image::ImageFormat::Png)
70        .context("Failed to decode PNG for WebP conversion")?;
71
72    let mut output = Cursor::new(Vec::new());
73
74    let encoder = create_webp_encoder(&mut output, quality);
75    encoder
76        .write_image(
77            img.as_bytes(),
78            img.width(),
79            img.height(),
80            img.color().into(),
81        )
82        .context("Failed to encode WebP from PNG")?;
83
84    Ok(output.into_inner())
85}