use std::{
collections::HashMap,
io::{BufReader, Cursor},
};
use exif::Exif;
use image::{codecs, GenericImageView, ImageDecoder, ImageFormat};
use resvg::usvg::Options;
use crate::{
error::WasmImageError,
load::{load_raw_image, RawSourceImage},
source_type::SourceType,
};
#[cfg(feature = "wasm")]
use {js_sys::Uint8Array, wasm_bindgen::prelude::*};
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi, into_wasm_abi))]
#[derive(serde::Deserialize, serde::Serialize, Default)]
pub struct Metadata {
pub width: u32,
pub height: u32,
pub other: Option<HashMap<String, String>>,
pub gps: Option<HashMap<String, String>>,
pub errors: Option<Vec<String>>,
}
pub(super) fn exif_to_hashmaps(
exif: &Exif,
) -> (HashMap<String, String>, Option<HashMap<String, String>>) {
let mut other = HashMap::new();
let mut gps = HashMap::new();
for field in exif.fields() {
let key = field.tag.description().unwrap_or_default().to_string();
let value = field.value.display_as(field.tag).to_string();
if field.tag.0 == exif::Context::Gps {
gps.insert(key, value);
} else {
other.insert(key, value);
}
}
(other, if gps.is_empty() { None } else { Some(gps) })
}
fn get_decoder<'a>(
format: ImageFormat,
img: &'a [u8],
) -> Result<Box<dyn ImageDecoder + 'a>, WasmImageError> {
let decoder: Box<dyn ImageDecoder> = match format {
ImageFormat::Bmp => Box::new(
codecs::bmp::BmpDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("BMP".to_string(), e.to_string()))?,
),
ImageFormat::Gif => Box::new(
codecs::gif::GifDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("GIF".to_string(), e.to_string()))?,
),
ImageFormat::Ico => Box::new(
codecs::ico::IcoDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("ICO".to_string(), e.to_string()))?,
),
ImageFormat::Jpeg => Box::new(
codecs::jpeg::JpegDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("JPEG".to_string(), e.to_string()))?,
),
ImageFormat::OpenExr => Box::new(
codecs::openexr::OpenExrDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("OpenEXR".to_string(), e.to_string()))?,
),
ImageFormat::Png => Box::new(
codecs::png::PngDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("PNG".to_string(), e.to_string()))?,
),
ImageFormat::Tiff => Box::new(
codecs::tiff::TiffDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("TIFF".to_string(), e.to_string()))?,
),
ImageFormat::WebP => Box::new(
codecs::webp::WebPDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("WebP".to_string(), e.to_string()))?,
),
ImageFormat::Dds => Box::new(
codecs::dds::DdsDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("DDS".to_string(), e.to_string()))?,
),
ImageFormat::Farbfeld => Box::new(
codecs::farbfeld::FarbfeldDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("Farbfeld".to_string(), e.to_string()))?,
),
ImageFormat::Hdr => Box::new(
codecs::hdr::HdrDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("HDR".to_string(), e.to_string()))?,
),
ImageFormat::Qoi => Box::new(
codecs::qoi::QoiDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("QOI".to_string(), e.to_string()))?,
),
ImageFormat::Tga => Box::new(
codecs::tga::TgaDecoder::new(Cursor::new(img))
.map_err(|e| WasmImageError::DecoderError("TGA".to_string(), e.to_string()))?,
),
_ => {
return Err(WasmImageError::DecoderError(
"Unknown".to_string(),
format!("Unknown format: {format:?}"),
))
}
};
Ok(decoder)
}
fn get_exif_errors(error: exif::Error) -> Result<Vec<String>, WasmImageError> {
let mut errors = Vec::new();
error
.distill_partial_result(|exif_errors| {
errors = exif_errors
.iter()
.map(std::string::ToString::to_string)
.collect();
})
.map_err(WasmImageError::ExifError)?;
Ok(errors)
}
impl TryFrom<RawSourceImage<'_>> for Metadata {
type Error = WasmImageError;
fn try_from(img: RawSourceImage) -> Result<Self, Self::Error> {
match img {
RawSourceImage::Raster(img, format) => {
let decoder = get_decoder(format, img).ok();
let metadata = if let Some(mut decoder) = decoder {
let (width, height) = decoder.dimensions();
let mut reader = exif::Reader::new();
reader.continue_on_error(true);
let exif = match format {
ImageFormat::Tiff
| ImageFormat::Jpeg
| ImageFormat::Avif
| ImageFormat::Png
| ImageFormat::WebP => {
let data_reader = Cursor::new(img);
let mut data_reader = BufReader::new(data_reader);
reader.read_from_container(&mut data_reader).map_err(|e| {
get_exif_errors(e).unwrap_or_else(|e| vec![e.to_string()])
})
}
_ => {
if let Ok(Some(exif)) = decoder.exif_metadata() {
reader.read_raw(exif).map_err(|e| {
get_exif_errors(e).unwrap_or_else(|e| vec![e.to_string()])
})
} else {
Err(Vec::new())
}
}
};
let (other, gps, errors) = match exif {
Ok(exif) => {
let (other, gps) = exif_to_hashmaps(&exif);
(Some(other), gps, None)
}
Err(errors) => (None, None, Some(errors)),
};
Self {
width,
height,
other,
gps,
errors,
}
} else {
let img = image::load_from_memory_with_format(img, format)
.map_err(WasmImageError::LibError)?;
let (width, height) = img.dimensions();
Self {
width,
height,
..Default::default()
}
};
Ok(metadata)
}
RawSourceImage::Svg(svg) => {
let tree = resvg::usvg::Tree::from_data(svg, &Options::default())?;
let size = tree.size();
let (width, height) = (size.width() as u32, size.height() as u32);
Ok(Self {
width,
height,
..Default::default()
})
}
}
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = loadMetadata)]
pub fn load_metadata(
file: &Uint8Array,
src_type: &str,
cb: &js_sys::Function,
) -> Result<Metadata, JsValue> {
let src_type = SourceType::from_mime_type(src_type);
crate::progress::report(cb, 10.0, "Starting metadata extraction");
let file = file.to_vec();
crate::progress::report(cb, 35.0, "Loading image");
let img = load_raw_image(&file, src_type.as_ref())
.map_err(|e| JsValue::from_str(e.to_string().as_str()))?;
crate::progress::report(cb, 65.0, "Extracting metadata");
let metadata =
Metadata::try_from(img).map_err(|e| JsValue::from_str(e.to_string().as_str()))?;
crate::progress::report(cb, 100.0, "Metadata extraction complete");
Ok(metadata)
}
#[cfg(not(feature = "wasm"))]
pub fn load_metadata(file: &[u8], src_type: &str) -> Result<Metadata, WasmImageError> {
let src_type = SourceType::from_mime_type(src_type);
let img = load_raw_image(file, src_type.as_ref())?;
Metadata::try_from(img)
}
#[cfg(test)]
mod tests {
use image::{codecs, ImageDecoder};
use std::collections::HashMap;
use std::io::{BufReader, Cursor};
#[test]
fn test_load_metadata() {
let file = include_bytes!("../../assets/test.jpeg");
let mut decoder =
codecs::jpeg::JpegDecoder::new(Cursor::new(file)).expect("Failed to create decoder");
let exif = decoder
.exif_metadata()
.expect("Failed to extract EXIF data")
.expect("No EXIF data found");
let reader = exif::Reader::new();
let exif = reader.read_raw(exif).expect("Failed to read EXIF data");
let mut hashmap = HashMap::new();
for field in exif.fields() {
let field_name = field.tag.description();
let field_value = field.value.display_as(field.tag);
hashmap.insert(
field_name.unwrap_or_default().to_string(),
field_value.to_string(),
);
}
println!("{hashmap:?}");
}
#[test]
fn test_load_webp_metadata() {
let file = include_bytes!("../../assets/exif.webp");
let mut reader = exif::Reader::new();
reader.continue_on_error(true);
let exif = reader
.read_from_container(&mut BufReader::new(Cursor::new(file)))
.expect("Failed to read EXIF data");
for field in exif.fields() {
println!("{field:?}");
}
}
}