diff --git a/src/blob.rs b/src/blob.rs index 27c44f3e3..601acd9cf 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -10,8 +10,8 @@ use anyhow::{Context as _, Result, ensure, format_err}; use base64::Engine as _; use futures::StreamExt; use image::ImageReader; -use image::codecs::jpeg::JpegEncoder; use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba}; +use image::{codecs::jpeg::JpegEncoder, metadata::Orientation}; use num_traits::FromPrimitive; use tokio::{fs, task}; use tokio_stream::wrappers::ReadDirStream; @@ -362,7 +362,10 @@ impl<'a> BlobObject<'a> { return Ok(name); } let mut img = imgreader.decode().context("image decode failure")?; - let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context)); + let orientation = exif + .as_ref() + .map(|exif| exif_orientation(exif, context)) + .unwrap_or(Orientation::NoTransforms); let mut encoded = Vec::new(); if *vt == Viewtype::Sticker { @@ -381,13 +384,7 @@ impl<'a> BlobObject<'a> { return Ok(name); } } - - img = match orientation { - Some(90) => img.rotate90(), - Some(180) => img.rotate180(), - Some(270) => img.rotate270(), - _ => img, - }; + img.apply_orientation(orientation); // max_wh is the maximum image width and height, i.e. the resolution-limit. // target_wh target-resolution for resizing the image. @@ -551,18 +548,17 @@ fn image_metadata(file: &std::fs::File) -> Result<(u64, Option)> { Ok((len, exif)) } -fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 { - if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) { - // possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html - // we only use rotation, in practise, flipping is not used. - match orientation.value.get_uint(0) { - Some(3) => return 180, - Some(6) => return 90, - Some(8) => return 270, - other => warn!(context, "Exif orientation value ignored: {other:?}."), - } +fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation { + if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) + && let Some(val) = orientation.value.get_uint(0) + && let Ok(val) = TryInto::::try_into(val) + { + return Orientation::from_exif(val).unwrap_or({ + warn!(context, "Exif orientation value ignored: {val:?}."); + Orientation::NoTransforms + }); } - 0 + Orientation::NoTransforms } /// All files in the blobdir. diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs index ed5a2b001..1e9fbc720 100644 --- a/src/blob/blob_tests.rs +++ b/src/blob/blob_tests.rs @@ -305,7 +305,7 @@ async fn test_recode_image_2() { has_exif: true, original_width: 2000, original_height: 1800, - orientation: 270, + orientation: Some(Orientation::Rotate270), compressed_width: 1800, compressed_height: 2000, ..Default::default() @@ -336,6 +336,28 @@ async fn test_recode_image_2() { assert_correct_rotation(&img_rotated); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_vflipped() { + let bytes = include_bytes!("../../test-data/image/rectangle200x180-vflipped.jpg"); + let img_rotated = SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "0", + bytes, + extension: "jpg", + has_exif: true, + original_width: 200, + original_height: 180, + orientation: Some(Orientation::FlipVertical), + compressed_width: 200, + compressed_height: 180, + ..Default::default() + } + .test() + .await + .unwrap(); + assert_correct_rotation(&img_rotated); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_recode_image_bad_exif() { // `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be @@ -530,7 +552,7 @@ struct SendImageCheckMediaquality<'a> { pub(crate) has_exif: bool, pub(crate) original_width: u32, pub(crate) original_height: u32, - pub(crate) orientation: i32, + pub(crate) orientation: Option, pub(crate) res_viewtype: Option, pub(crate) compressed_width: u32, pub(crate) compressed_height: u32, @@ -546,7 +568,7 @@ impl SendImageCheckMediaquality<'_> { let has_exif = self.has_exif; let original_width = self.original_width; let original_height = self.original_height; - let orientation = self.orientation; + let orientation = self.orientation.unwrap_or(Orientation::NoTransforms); let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image); let compressed_width = self.compressed_width; let compressed_height = self.compressed_height; diff --git a/test-data/image/rectangle200x180-vflipped.jpg b/test-data/image/rectangle200x180-vflipped.jpg new file mode 100644 index 000000000..bb069085c Binary files /dev/null and b/test-data/image/rectangle200x180-vflipped.jpg differ