refilelabs_image/metadata/
extract.rs1use 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 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}