asset.rs raw

//! Turning asset bytes into scene resources.
//!
//! Assets reach this point already located: the typed build embeds the bytes
//! with `include_bytes!` (so editing the file rebuilds the crate), and the
//! dev runtime reads them from disk through the same asset search path. Both
//! then land here, which is the one place that knows how to decode.
//!
//! The supported formats are exactly what the `image` dependency is compiled
//! for — PNG and JPEG, the two an interface actually ships. Widening the set
//! is a Cargo feature, not a code change.

use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;

use guiduck_scene::paint::{Blob, ImageAlphaType, ImageBrush, ImageData, ImageFormat};

thread_local! {
    static EMBEDDED: RefCell<HashMap<&'static str, ImageBrush>> = RefCell::new(HashMap::new());
}

/// Decode an asset the typed build embedded, memoized on its path.
///
/// Memoizing is sound precisely because the bytes came from `include_bytes!`:
/// they are fixed at build time, so one path always means one image, and an
/// edited file arrives as a rebuild rather than as new bytes under an old
/// path. That makes an icon inside a `for` decode once instead of once per
/// row. The dev runtime deliberately does *not* come through here — it reads
/// assets from disk, where an edit must be visible without a rebuild.
pub fn embedded_image(path: &'static str, bytes: &'static [u8]) -> Result<ImageBrush, String> {
    if let Some(brush) = EMBEDDED.with(|cache| cache.borrow().get(path).cloned()) {
        return Ok(brush);
    }
    let brush = decode_image(bytes)?;
    EMBEDDED.with(|cache| cache.borrow_mut().insert(path, brush.clone()));
    Ok(brush)
}

/// Decode encoded image bytes (PNG or JPEG) into scene image data.
///
/// The error is a human-readable sentence, because both callers report it to
/// a person: the proc-macro through a compile error, the dev runtime through
/// a warning that skips the widget.
pub fn decode_image(bytes: &[u8]) -> Result<ImageBrush, String> {
    let decoded = image::load_from_memory(bytes).map_err(|e| e.to_string())?;
    let rgba = decoded.to_rgba8();
    let (width, height) = rgba.dimensions();
    if width == 0 || height == 0 {
        return Err("the image has no pixels".to_owned());
    }
    Ok(ImageBrush::from(ImageData {
        data: Blob::new(Arc::new(rgba.into_raw())),
        format: ImageFormat::Rgba8,
        // `image` hands back straight (non-premultiplied) alpha.
        alpha_type: ImageAlphaType::Alpha,
        width,
        height,
    }))
}

#[cfg(test)]
mod tests;