Skip to main content

r/interpreter/graphics/
raster.rs

1//! SVG-to-raster conversion using resvg + tiny-skia.
2//!
3//! Converts an SVG string into a `tiny_skia::Pixmap` which can then be
4//! encoded to PNG (via tiny-skia's built-in encoder), JPEG, or BMP
5//! (via the `image` crate).
6
7use crate::interpreter::value::{RError, RErrorKind};
8
9/// Rasterize an SVG string into a pixel buffer.
10///
11/// Returns a `tiny_skia::Pixmap` of the given dimensions with the SVG
12/// rendered onto a white background.
13pub fn svg_to_raster(
14    svg_str: &str,
15    width_px: u32,
16    height_px: u32,
17) -> Result<tiny_skia::Pixmap, RError> {
18    // Parse SVG into usvg tree
19    let tree = usvg::Tree::from_str(svg_str, &usvg::Options::default()).map_err(|e| {
20        RError::new(
21            RErrorKind::Other,
22            format!("failed to parse SVG for rasterization: {e}"),
23        )
24    })?;
25
26    // Create pixel buffer
27    let mut pixmap = tiny_skia::Pixmap::new(width_px, height_px).ok_or_else(|| {
28        RError::new(
29            RErrorKind::Other,
30            format!("failed to create {width_px}x{height_px} pixel buffer"),
31        )
32    })?;
33
34    // Fill with white background
35    pixmap.fill(tiny_skia::Color::WHITE);
36
37    // Compute transform to fit SVG viewbox into pixel dimensions
38    let svg_size = tree.size();
39    let sx = width_px as f32 / svg_size.width();
40    let sy = height_px as f32 / svg_size.height();
41    let transform = tiny_skia::Transform::from_scale(sx, sy);
42
43    // Render
44    resvg::render(&tree, transform, &mut pixmap.as_mut());
45
46    Ok(pixmap)
47}
48
49/// Encode a pixmap as JPEG bytes.
50pub fn pixmap_to_jpeg(pixmap: &tiny_skia::Pixmap, quality: u8) -> Result<Vec<u8>, RError> {
51    let width = pixmap.width();
52    let height = pixmap.height();
53
54    // Convert RGBA premultiplied -> RGB for JPEG (JPEG doesn't support alpha)
55    let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
56    for pixel in pixmap.pixels() {
57        // tiny-skia stores premultiplied RGBA; demultiply
58        let a = pixel.alpha() as f32 / 255.0;
59        if a > 0.0 {
60            rgb_data.push((pixel.red() as f32 / a).min(255.0) as u8);
61            rgb_data.push((pixel.green() as f32 / a).min(255.0) as u8);
62            rgb_data.push((pixel.blue() as f32 / a).min(255.0) as u8);
63        } else {
64            // Transparent → white background
65            rgb_data.push(255);
66            rgb_data.push(255);
67            rgb_data.push(255);
68        }
69    }
70
71    let img: image::ImageBuffer<image::Rgb<u8>, _> =
72        image::ImageBuffer::from_raw(width, height, rgb_data).ok_or_else(|| {
73            RError::new(
74                RErrorKind::Other,
75                "failed to create image buffer".to_string(),
76            )
77        })?;
78
79    let mut buf = Vec::new();
80    let mut cursor = std::io::Cursor::new(&mut buf);
81    let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, quality);
82    image::ImageEncoder::write_image(
83        encoder,
84        img.as_raw(),
85        width,
86        height,
87        image::ExtendedColorType::Rgb8,
88    )
89    .map_err(|e| RError::new(RErrorKind::Other, format!("JPEG encoding failed: {e}")))?;
90
91    Ok(buf)
92}
93
94/// Encode a pixmap as BMP bytes.
95pub fn pixmap_to_bmp(pixmap: &tiny_skia::Pixmap) -> Result<Vec<u8>, RError> {
96    let width = pixmap.width();
97    let height = pixmap.height();
98
99    // BMP supports RGBA, but we'll use RGB for compatibility
100    let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
101    for pixel in pixmap.pixels() {
102        let a = pixel.alpha() as f32 / 255.0;
103        if a > 0.0 {
104            rgb_data.push((pixel.red() as f32 / a).min(255.0) as u8);
105            rgb_data.push((pixel.green() as f32 / a).min(255.0) as u8);
106            rgb_data.push((pixel.blue() as f32 / a).min(255.0) as u8);
107        } else {
108            rgb_data.push(255);
109            rgb_data.push(255);
110            rgb_data.push(255);
111        }
112    }
113
114    let img: image::ImageBuffer<image::Rgb<u8>, _> =
115        image::ImageBuffer::from_raw(width, height, rgb_data).ok_or_else(|| {
116            RError::new(
117                RErrorKind::Other,
118                "failed to create image buffer".to_string(),
119            )
120        })?;
121
122    let mut buf = Vec::new();
123    let mut cursor = std::io::Cursor::new(&mut buf);
124    let encoder = image::codecs::bmp::BmpEncoder::new(&mut cursor);
125    image::ImageEncoder::write_image(
126        encoder,
127        img.as_raw(),
128        width,
129        height,
130        image::ExtendedColorType::Rgb8,
131    )
132    .map_err(|e| RError::new(RErrorKind::Other, format!("BMP encoding failed: {e}")))?;
133
134    Ok(buf)
135}