Skip to main content

goud_engine/assets/loaders/texture/
loader.rs

1//! [`TextureLoader`] — decodes image bytes into [`TextureAsset`].
2
3use image::{DynamicImage, ImageError};
4
5use crate::assets::{Asset, AssetLoadError, AssetLoader, LoadContext};
6
7use super::{asset::TextureAsset, format::TextureFormat, settings::TextureSettings};
8
9/// Asset loader for texture images.
10///
11/// Uses the `image` crate to decode image files into RGBA8 pixel data.
12/// Supports PNG, JPEG, BMP, TGA, GIF, WebP, ICO, and TIFF formats.
13///
14/// # Example
15///
16/// ```no_run
17/// use goud_engine::assets::{AssetServer, loaders::texture::{TextureLoader, TextureAsset, TextureSettings}};
18///
19/// let mut server = AssetServer::new();
20/// server.register_loader(TextureLoader::default());
21///
22/// // Load with default settings
23/// let texture = server.load::<TextureAsset>("player.png");
24///
25/// // Load with custom settings
26/// let mut settings = TextureSettings::default();
27/// settings.flip_vertical = false;
28/// server.register_loader_with_settings(TextureLoader::default(), settings);
29/// ```
30#[derive(Debug, Clone, Default)]
31pub struct TextureLoader;
32
33impl TextureLoader {
34    /// Creates a new texture loader.
35    pub fn new() -> Self {
36        Self
37    }
38
39    /// Loads a texture from raw bytes with format detection.
40    pub(super) fn load_from_bytes(
41        bytes: &[u8],
42        settings: &TextureSettings,
43        format_hint: Option<TextureFormat>,
44    ) -> Result<TextureAsset, AssetLoadError> {
45        // Try to load the image
46        let img = if let Some(format) = format_hint.and_then(|f| f.to_image_format()) {
47            // Try with format hint first
48            image::load_from_memory_with_format(bytes, format)
49                .or_else(|_| image::load_from_memory(bytes))
50        } else {
51            image::load_from_memory(bytes)
52        }
53        .map_err(Self::convert_image_error)?;
54
55        // Optionally flip vertically
56        let img = if settings.flip_vertical {
57            img.flipv()
58        } else {
59            img
60        };
61
62        let rgba = img.to_rgba8();
63        let width = rgba.width();
64        let height = rgba.height();
65        let data = rgba.into_raw();
66
67        // Determine format
68        let format = format_hint
69            .unwrap_or_else(|| Self::detect_format(&img).unwrap_or(TextureFormat::Unknown));
70
71        Ok(TextureAsset {
72            data,
73            width,
74            height,
75            format,
76        })
77    }
78
79    /// Detects texture format from a [`DynamicImage`].
80    ///
81    /// The image crate does not store the original format after decoding,
82    /// so this always returns `None`. The extension-based guess is used instead.
83    fn detect_format(_img: &DynamicImage) -> Option<TextureFormat> {
84        None
85    }
86
87    /// Converts an image crate error to [`AssetLoadError`].
88    fn convert_image_error(error: ImageError) -> AssetLoadError {
89        match error {
90            ImageError::IoError(e) => AssetLoadError::io_error("", e),
91            ImageError::Decoding(e) => AssetLoadError::decode_failed(e.to_string()),
92            ImageError::Encoding(e) => AssetLoadError::decode_failed(e.to_string()),
93            ImageError::Parameter(e) => AssetLoadError::decode_failed(e.to_string()),
94            ImageError::Limits(e) => AssetLoadError::decode_failed(e.to_string()),
95            ImageError::Unsupported(e) => AssetLoadError::decode_failed(e.to_string()),
96        }
97    }
98}
99
100impl AssetLoader for TextureLoader {
101    type Asset = TextureAsset;
102    type Settings = TextureSettings;
103
104    fn extensions(&self) -> &[&str] {
105        TextureAsset::extensions()
106    }
107
108    fn load<'a>(
109        &'a self,
110        bytes: &'a [u8],
111        settings: &'a Self::Settings,
112        context: &'a mut LoadContext,
113    ) -> Result<Self::Asset, AssetLoadError> {
114        // Get format hint from file extension
115        let format_hint = context
116            .extension()
117            .map(TextureFormat::from_extension)
118            .filter(|f| *f != TextureFormat::Unknown);
119
120        Self::load_from_bytes(bytes, settings, format_hint)
121    }
122}