//! 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> = 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 { 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 { 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;