mugltf/loader/
file_loader.rs

1//! Loader of glTF resources from file system.
2
3#![cfg(feature = "file-loader")]
4
5use super::GltfResourceLoader;
6use crate::Error;
7use alloc::{boxed::Box, string::String, vec::Vec};
8use async_trait::async_trait;
9use core::fmt::{self, Debug};
10use data_url::DataUrl;
11use image::{
12    error::{ImageFormatHint, UnsupportedError, UnsupportedErrorKind},
13    ImageError, ImageFormat, ImageResult,
14};
15use mugl::Extent2D;
16use std::{fs::File, io::Read, path::PathBuf};
17
18/// Loader of glTF resources from file system.
19#[derive(Debug)]
20pub struct GltfResourceFileLoader {
21    path: String,
22}
23
24impl Default for GltfResourceFileLoader {
25    fn default() -> Self {
26        Self { path: "./".into() }
27    }
28}
29
30#[async_trait(?Send)]
31impl GltfResourceLoader for GltfResourceFileLoader {
32    type Error = Box<Error>;
33    type ImageData = (Vec<u8>, Extent2D);
34
35    #[inline]
36    fn set_path(&mut self, path: &str) {
37        self.path = path.into();
38    }
39
40    async fn get_gltf(&self, uri: &str) -> Result<Vec<u8>, Self::Error> {
41        Ok(read_file(&self.path, uri)?)
42    }
43
44    async fn get_buffer(&self, uri: &str) -> Result<Vec<u8>, Self::Error> {
45        if let Some(data) = try_read_data_url(uri, false)? {
46            Ok(data)
47        } else {
48            Ok(read_file(&self.path, uri)?)
49        }
50    }
51
52    async fn get_image(&self, uri: &str) -> Result<Self::ImageData, Self::Error> {
53        let data = if let Some(data) = try_read_data_url(uri, true)? {
54            data
55        } else {
56            read_file(&self.path, uri)?
57        };
58        let dynimage = image::load_from_memory(data.as_slice())?;
59        let size = Extent2D(dynimage.width(), dynimage.height());
60        Ok((dynimage.into_bytes(), size))
61    }
62
63    async fn decode_image(
64        &self,
65        img: &[u8],
66        mime_type: &str,
67    ) -> Result<Self::ImageData, Self::Error> {
68        let format = get_image_format(mime_type)?;
69        let dynimage = image::load_from_memory_with_format(img, format)?;
70        let size = Extent2D(dynimage.width(), dynimage.height());
71        Ok((dynimage.into_bytes(), size))
72    }
73}
74
75fn read_file(path: &str, file: &str) -> Result<Vec<u8>, std::io::Error> {
76    let mut file_path = PathBuf::from(path);
77    file_path.push(file);
78    let mut file = File::open(file_path)?;
79    let mut content = Vec::new();
80    file.read_to_end(&mut content)?;
81    Ok(content)
82}
83
84fn try_read_data_url(uri: &str, is_image: bool) -> Result<Option<Vec<u8>>, Box<Error>> {
85    if uri.starts_with("data:") {
86        let data_url = DataUrl::process(uri).map_err(|err| DataUrlError::InvalidDataUrl(err))?;
87        let is_supported_mime = if is_image {
88            is_supported_image_mime(&data_url.mime_type().type_, &data_url.mime_type().subtype)
89        } else {
90            is_supported_buffer_mime(&data_url.mime_type().type_, &data_url.mime_type().subtype)
91        };
92        if !is_supported_mime {
93            return Err(DataUrlError::UnsupportedMimeType(
94                data_url.mime_type().type_.clone(),
95                data_url.mime_type().subtype.clone(),
96            )
97            .into());
98        }
99
100        let (data, _) = data_url
101            .decode_to_vec()
102            .map_err(|err| DataUrlError::InvalidBase64(err))?;
103        Ok(Some(data))
104    } else {
105        Ok(None)
106    }
107}
108
109#[inline]
110fn is_supported_buffer_mime(type_: &str, subtype: &str) -> bool {
111    type_ == "application" && (subtype == "gltf-buffer" || subtype == "octet-stream")
112}
113
114#[inline]
115fn is_supported_image_mime(type_: &str, subtype: &str) -> bool {
116    type_ == "image" && (subtype == "png" || subtype == "jpeg")
117}
118
119fn get_image_format(mime_type: &str) -> ImageResult<ImageFormat> {
120    if mime_type == "image/png" {
121        Ok(ImageFormat::Png)
122    } else if mime_type == "image/jpeg" {
123        Ok(ImageFormat::Jpeg)
124    } else {
125        let format_hint = ImageFormatHint::Name(mime_type.into());
126        Err(ImageError::Unsupported(
127            UnsupportedError::from_format_and_kind(
128                format_hint.clone(),
129                UnsupportedErrorKind::Format(format_hint),
130            ),
131        ))
132    }
133}
134
135#[derive(Debug)]
136pub enum DataUrlError {
137    InvalidDataUrl(data_url::DataUrlError),
138    InvalidBase64(data_url::forgiving_base64::InvalidBase64),
139    UnsupportedMimeType(String, String),
140}
141
142impl fmt::Display for DataUrlError {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        match self {
145            DataUrlError::InvalidDataUrl(err) => err.fmt(f),
146            DataUrlError::InvalidBase64(err) => err.fmt(f),
147            DataUrlError::UnsupportedMimeType(type_, sub_type) => {
148                write!(f, "unsupported mime type: {}/{}", type_, sub_type)
149            }
150        }
151    }
152}
153
154impl std::error::Error for DataUrlError {}