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;