use chrono::{DateTime, Utc}; use color_eyre::{ Result, eyre::{Context, eyre}, }; use exif::{Exif, In, Tag, Value}; use image::DynamicImage; use std::{fs, path::Path}; pub struct ImageProcessor { exif: Option, image: DynamicImage, } impl ImageProcessor { pub fn new(path: &Path) -> Result { let media_bytes = fs::read(path)?; let mut exif_buf_reader = std::io::Cursor::new(media_bytes); let exif_reader = exif::Reader::new(); let exif = exif_reader.read_from_container(&mut exif_buf_reader).ok(); let image = image::open(path).wrap_err("Failed to open image")?; Ok(Self { exif, image }) } pub fn get_timestamp(&self) -> Result> { let exif = self.exif.as_ref().ok_or_else(|| eyre!("no exif data"))?; let timestamp = exif_get_str(exif, Tag::DateTimeOriginal) .or_else(|_| exif_get_str(exif, Tag::DateTime)) .wrap_err("Failed to get DateTime/DateTimeOriginal tag from exif data")?; let timestamp_tz = exif_get_str(exif, Tag::OffsetTimeOriginal); let mut timestamp_modified = timestamp.to_string().replace('-', ":"); timestamp_modified.push(' '); match ×tamp_tz { Ok(tz) => timestamp_modified.push_str(tz.as_ref()), Err(_) => timestamp_modified.push_str("+02:00"), // TODO: Make configurable } let timestamp = DateTime::parse_from_str(×tamp_modified, "%Y:%m:%d %H:%M:%S %z") .wrap_err(format!( "Failed to parse timestamp:{timestamp:?} tz:{timestamp_tz:?}" ))? .with_timezone(&Utc); Ok(timestamp) } pub fn get_thumbnail(&self, size: u32) -> Result { let mut image = self.image.thumbnail(size, size); if let Some(exif) = &self.exif { image = match exif_get_u32(exif, Tag::Orientation) { // http://sylvana.net/jpegcrop/exif_orientation.html // Ok(1) => image, Ok(2) => image.flipv(), Ok(3) => image.rotate180(), Ok(4) => image.rotate180().flipv(), Ok(5) => image.rotate90().flipv(), Ok(6) => image.rotate90(), Ok(7) => image.rotate270().flipv(), Ok(8) => image.rotate270(), _ => image, }; } Ok(crop(image)) } } fn exif_get_u32(exif: &exif::Exif, tag: Tag) -> Result { #[allow(clippy::option_if_let_else)] match exif.get_field(tag, In::PRIMARY) { Some(field) => match field.value.get_uint(0) { Some(v) => Ok(v), _ => Err(eyre!("Failed getting number")), }, None => Err(eyre!("Value not found")), } } fn exif_get_str(exif: &exif::Exif, tag: Tag) -> Result<&str> { match exif.get_field(tag, In::PRIMARY) { Some(field) => match &field.value { Value::Ascii(bytes) => { let bytes = bytes.first().ok_or_else(|| eyre!("Something is wrong"))?; Ok(std::str::from_utf8(bytes)?) } _ => Err(eyre!("Not Ascii value")), }, None => Err(eyre!("Value not found")), } } #[must_use] pub fn crop(mut image: DynamicImage) -> DynamicImage { let image_height = image.height(); let image_width = image.width(); if image_height > image_width { image.crop( 0, (image_height - image_width) / 2, image_width, image_width, ) } else { image.crop( (image_width - image_height) / 2, 0, image_height, image_height, ) } }