mkapk-res 0.1.1

Cross-platform audio plugin build and packaging system (Standalone/VST3/AU/AAX) with a shared retained-mode GUI framework
use crate::Resource;

pub struct SvgImage {
    tree: usvg::Tree,
    cache: Option<tiny_skia::Pixmap>,
}

impl Resource for SvgImage {
    fn decode(bytes: &[u8]) -> Option<Self>
    where
        Self: Sized,
    {
        let text = std::str::from_utf8(bytes).ok()?;
        let tree = usvg::Tree::from_str(text, &usvg::Options::default()).ok()?;
        Some(Self { tree, cache: None })
    }
}

impl SvgImage {
    pub fn tree(&self) -> &usvg::Tree {
        &self.tree
    }

    pub fn width(&self) -> u32 {
        self.tree.size().to_int_size().width()
    }

    pub fn height(&self) -> u32 {
        self.tree.size().to_int_size().height()
    }

    pub fn render(&mut self, size: (u32, u32)) -> Option<&[u8]> {
        let (width, height) = size;
        if width == 0 || height == 0 {
            return None;
        }

        let needs_render = self
            .cache
            .as_ref()
            .is_none_or(|pixmap| pixmap.width() != width || pixmap.height() != height);

        if needs_render {
            let mut pixmap = tiny_skia::Pixmap::new(width, height)?;
            resvg::render(
                &self.tree,
                tiny_skia::Transform::default(),
                &mut pixmap.as_mut(),
            );
            self.cache = Some(pixmap);
        }

        Some(self.cache.as_ref().unwrap().data())
    }

    pub fn render_rgba(&mut self, size: (u32, u32)) -> Option<(u32, u32, Vec<u8>)> {
        let (width, height) = size;
        self.render(size)?;
        let pixmap = self.cache.as_ref().unwrap();
        Some((width, height, pixmap.data().to_vec()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const RED_SQUARE: &[u8] = b"<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='10' height='10' fill='red'/></svg>";

    fn decode_sample() -> SvgImage {
        SvgImage::decode(RED_SQUARE).unwrap()
    }

    #[test]
    fn decode_intrinsic_size() {
        let svg = decode_sample();
        assert_eq!(svg.width(), 10);
        assert_eq!(svg.height(), 10);
    }

    #[test]
    fn render_produces_rgba_bytes() {
        let mut svg = decode_sample();
        let bytes = svg.render((20, 20)).unwrap();
        assert_eq!(bytes.len(), 20 * 20 * 4);
        assert!(bytes[3] > 0);
    }

    #[test]
    fn render_caches_pixmap() {
        let mut svg = decode_sample();
        let first = svg.render((20, 20)).unwrap().as_ptr();
        let second = svg.render((20, 20)).unwrap().as_ptr();
        assert_eq!(first, second);
    }
}