Skip to main content

refilelabs_image/metadata/
extract.rs

1use std::{
2    collections::HashMap,
3    io::{BufReader, Cursor},
4};
5
6use exif::Exif;
7use image::{codecs, GenericImageView, ImageDecoder, ImageFormat};
8use resvg::usvg::Options;
9
10use crate::{
11    error::WasmImageError,
12    load::{load_raw_image, RawSourceImage},
13    source_type::SourceType,
14};
15
16#[cfg(feature = "wasm")]
17use {js_sys::Uint8Array, wasm_bindgen::prelude::*};
18
19#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
20#[cfg_attr(feature = "wasm", tsify(from_wasm_abi, into_wasm_abi))]
21#[derive(serde::Deserialize, serde::Serialize, Default)]
22pub struct Metadata {
23    pub width: u32,
24    pub height: u32,
25    pub other: Option<HashMap<String, String>>,
26    pub gps: Option<HashMap<String, String>>,
27    pub errors: Option<Vec<String>>,
28}
29
30pub(super) fn exif_to_hashmaps(
31    exif: &Exif,
32) -> (HashMap<String, String>, Option<HashMap<String, String>>) {
33    let mut other = HashMap::new();
34    let mut gps = HashMap::new();
35
36    for field in exif.fields() {
37        let key = field.tag.description().unwrap_or_default().to_string();
38        let value = field.value.display_as(field.tag).to_string();
39        if field.tag.0 == exif::Context::Gps {
40            gps.insert(key, value);
41        } else {
42            other.insert(key, value);
43        }
44    }
45
46    (other, if gps.is_empty() { None } else { Some(gps) })
47}
48
49fn get_decoder<'a>(
50    format: ImageFormat,
51    img: &'a [u8],
52) -> Result<Box<dyn ImageDecoder + 'a>, WasmImageError> {
53    let decoder: Box<dyn ImageDecoder> = match format {
54        ImageFormat::Bmp => Box::new(
55            codecs::bmp::BmpDecoder::new(Cursor::new(img))
56                .map_err(|e| WasmImageError::DecoderError("BMP".to_string(), e.to_string()))?,
57        ),
58        ImageFormat::Gif => Box::new(
59            codecs::gif::GifDecoder::new(Cursor::new(img))
60                .map_err(|e| WasmImageError::DecoderError("GIF".to_string(), e.to_string()))?,
61        ),
62        ImageFormat::Ico => Box::new(
63            codecs::ico::IcoDecoder::new(Cursor::new(img))
64                .map_err(|e| WasmImageError::DecoderError("ICO".to_string(), e.to_string()))?,
65        ),
66        ImageFormat::Jpeg => Box::new(
67            codecs::jpeg::JpegDecoder::new(Cursor::new(img))
68                .map_err(|e| WasmImageError::DecoderError("JPEG".to_string(), e.to_string()))?,
69        ),
70        ImageFormat::OpenExr => Box::new(
71            codecs::openexr::OpenExrDecoder::new(Cursor::new(img))
72                .map_err(|e| WasmImageError::DecoderError("OpenEXR".to_string(), e.to_string()))?,
73        ),
74        ImageFormat::Png => Box::new(
75            codecs::png::PngDecoder::new(Cursor::new(img))
76                .map_err(|e| WasmImageError::DecoderError("PNG".to_string(), e.to_string()))?,
77        ),
78        ImageFormat::Tiff => Box::new(
79            codecs::tiff::TiffDecoder::new(Cursor::new(img))
80                .map_err(|e| WasmImageError::DecoderError("TIFF".to_string(), e.to_string()))?,
81        ),
82        ImageFormat::WebP => Box::new(
83            codecs::webp::WebPDecoder::new(Cursor::new(img))
84                .map_err(|e| WasmImageError::DecoderError("WebP".to_string(), e.to_string()))?,
85        ),
86        ImageFormat::Dds => Box::new(
87            codecs::dds::DdsDecoder::new(Cursor::new(img))
88                .map_err(|e| WasmImageError::DecoderError("DDS".to_string(), e.to_string()))?,
89        ),
90        ImageFormat::Farbfeld => Box::new(
91            codecs::farbfeld::FarbfeldDecoder::new(Cursor::new(img))
92                .map_err(|e| WasmImageError::DecoderError("Farbfeld".to_string(), e.to_string()))?,
93        ),
94        ImageFormat::Hdr => Box::new(
95            codecs::hdr::HdrDecoder::new(Cursor::new(img))
96                .map_err(|e| WasmImageError::DecoderError("HDR".to_string(), e.to_string()))?,
97        ),
98        ImageFormat::Qoi => Box::new(
99            codecs::qoi::QoiDecoder::new(Cursor::new(img))
100                .map_err(|e| WasmImageError::DecoderError("QOI".to_string(), e.to_string()))?,
101        ),
102        ImageFormat::Tga => Box::new(
103            codecs::tga::TgaDecoder::new(Cursor::new(img))
104                .map_err(|e| WasmImageError::DecoderError("TGA".to_string(), e.to_string()))?,
105        ),
106        _ => {
107            return Err(WasmImageError::DecoderError(
108                "Unknown".to_string(),
109                format!("Unknown format: {format:?}"),
110            ))
111        }
112    };
113
114    Ok(decoder)
115}
116
117fn get_exif_errors(error: exif::Error) -> Result<Vec<String>, WasmImageError> {
118    let mut errors = Vec::new();
119    error
120        .distill_partial_result(|exif_errors| {
121            errors = exif_errors
122                .iter()
123                .map(std::string::ToString::to_string)
124                .collect();
125        })
126        .map_err(WasmImageError::ExifError)?;
127    Ok(errors)
128}
129
130impl TryFrom<RawSourceImage<'_>> for Metadata {
131    type Error = WasmImageError;
132
133    fn try_from(img: RawSourceImage) -> Result<Self, Self::Error> {
134        match img {
135            RawSourceImage::Raster(img, format) => {
136                let decoder = get_decoder(format, img).ok();
137
138                let metadata = if let Some(mut decoder) = decoder {
139                    let (width, height) = decoder.dimensions();
140
141                    let mut reader = exif::Reader::new();
142                    reader.continue_on_error(true);
143
144                    let exif = match format {
145                        // Also HEIF, HEIC, not supported by image-rs though
146                        ImageFormat::Tiff
147                        | ImageFormat::Jpeg
148                        | ImageFormat::Avif
149                        | ImageFormat::Png
150                        | ImageFormat::WebP => {
151                            let data_reader = Cursor::new(img);
152                            let mut data_reader = BufReader::new(data_reader);
153                            reader.read_from_container(&mut data_reader).map_err(|e| {
154                                get_exif_errors(e).unwrap_or_else(|e| vec![e.to_string()])
155                            })
156                        }
157                        _ => {
158                            if let Ok(Some(exif)) = decoder.exif_metadata() {
159                                reader.read_raw(exif).map_err(|e| {
160                                    get_exif_errors(e).unwrap_or_else(|e| vec![e.to_string()])
161                                })
162                            } else {
163                                Err(Vec::new())
164                            }
165                        }
166                    };
167
168                    let (other, gps, errors) = match exif {
169                        Ok(exif) => {
170                            let (other, gps) = exif_to_hashmaps(&exif);
171                            (Some(other), gps, None)
172                        }
173                        Err(errors) => (None, None, Some(errors)),
174                    };
175
176                    Self {
177                        width,
178                        height,
179                        other,
180                        gps,
181                        errors,
182                    }
183                } else {
184                    let img = image::load_from_memory_with_format(img, format)
185                        .map_err(WasmImageError::LibError)?;
186                    let (width, height) = img.dimensions();
187                    Self {
188                        width,
189                        height,
190                        ..Default::default()
191                    }
192                };
193
194                Ok(metadata)
195            }
196            RawSourceImage::Svg(svg) => {
197                let tree = resvg::usvg::Tree::from_data(svg, &Options::default())?;
198                let size = tree.size();
199                let (width, height) = (size.width() as u32, size.height() as u32);
200                Ok(Self {
201                    width,
202                    height,
203                    ..Default::default()
204                })
205            }
206        }
207    }
208}
209
210#[cfg(feature = "wasm")]
211#[wasm_bindgen(js_name = loadMetadata)]
212pub fn load_metadata(
213    file: &Uint8Array,
214    src_type: &str,
215    cb: &js_sys::Function,
216) -> Result<Metadata, JsValue> {
217    let src_type = SourceType::from_mime_type(src_type);
218
219    crate::progress::report(cb, 10.0, "Starting metadata extraction");
220    let file = file.to_vec();
221    crate::progress::report(cb, 35.0, "Loading image");
222
223    let img = load_raw_image(&file, src_type.as_ref())
224        .map_err(|e| JsValue::from_str(e.to_string().as_str()))?;
225
226    crate::progress::report(cb, 65.0, "Extracting metadata");
227
228    let metadata =
229        Metadata::try_from(img).map_err(|e| JsValue::from_str(e.to_string().as_str()))?;
230
231    crate::progress::report(cb, 100.0, "Metadata extraction complete");
232
233    Ok(metadata)
234}
235
236#[cfg(not(feature = "wasm"))]
237pub fn load_metadata(file: &[u8], src_type: &str) -> Result<Metadata, WasmImageError> {
238    let src_type = SourceType::from_mime_type(src_type);
239    let img = load_raw_image(file, src_type.as_ref())?;
240    Metadata::try_from(img)
241}
242
243#[cfg(test)]
244mod tests {
245    use image::{codecs, ImageDecoder};
246    use std::collections::HashMap;
247    use std::io::{BufReader, Cursor};
248
249    #[test]
250    fn test_load_metadata() {
251        let file = include_bytes!("../../assets/test.jpeg");
252        let mut decoder =
253            codecs::jpeg::JpegDecoder::new(Cursor::new(file)).expect("Failed to create decoder");
254
255        let exif = decoder
256            .exif_metadata()
257            .expect("Failed to extract EXIF data")
258            .expect("No EXIF data found");
259
260        let reader = exif::Reader::new();
261        let exif = reader.read_raw(exif).expect("Failed to read EXIF data");
262
263        let mut hashmap = HashMap::new();
264        for field in exif.fields() {
265            let field_name = field.tag.description();
266            let field_value = field.value.display_as(field.tag);
267            hashmap.insert(
268                field_name.unwrap_or_default().to_string(),
269                field_value.to_string(),
270            );
271        }
272        println!("{hashmap:?}");
273    }
274
275    #[test]
276    fn test_load_webp_metadata() {
277        let file = include_bytes!("../../assets/exif.webp");
278
279        let mut reader = exif::Reader::new();
280        reader.continue_on_error(true);
281
282        let exif = reader
283            .read_from_container(&mut BufReader::new(Cursor::new(file)))
284            .expect("Failed to read EXIF data");
285
286        for field in exif.fields() {
287            println!("{field:?}");
288        }
289    }
290}