mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
feat: Flipped Exif orientations (#8057)
Before, sending of images flipped in Exif led to images having wrong orientation.
This commit is contained in:
36
src/blob.rs
36
src/blob.rs
@@ -10,8 +10,8 @@ use anyhow::{Context as _, Result, ensure, format_err};
|
|||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
use image::codecs::jpeg::JpegEncoder;
|
|
||||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||||
|
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use tokio::{fs, task};
|
use tokio::{fs, task};
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
@@ -362,7 +362,10 @@ impl<'a> BlobObject<'a> {
|
|||||||
return Ok(name);
|
return Ok(name);
|
||||||
}
|
}
|
||||||
let mut img = imgreader.decode().context("image decode failure")?;
|
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();
|
let mut encoded = Vec::new();
|
||||||
|
|
||||||
if *vt == Viewtype::Sticker {
|
if *vt == Viewtype::Sticker {
|
||||||
@@ -381,13 +384,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
return Ok(name);
|
return Ok(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
img.apply_orientation(orientation);
|
||||||
img = match orientation {
|
|
||||||
Some(90) => img.rotate90(),
|
|
||||||
Some(180) => img.rotate180(),
|
|
||||||
Some(270) => img.rotate270(),
|
|
||||||
_ => img,
|
|
||||||
};
|
|
||||||
|
|
||||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||||
// target_wh target-resolution for resizing the image.
|
// target_wh target-resolution for resizing the image.
|
||||||
@@ -551,18 +548,17 @@ fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
|||||||
Ok((len, exif))
|
Ok((len, exif))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
|
||||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
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
|
&& let Some(val) = orientation.value.get_uint(0)
|
||||||
// we only use rotation, in practise, flipping is not used.
|
&& let Ok(val) = TryInto::<u8>::try_into(val)
|
||||||
match orientation.value.get_uint(0) {
|
{
|
||||||
Some(3) => return 180,
|
return Orientation::from_exif(val).unwrap_or({
|
||||||
Some(6) => return 90,
|
warn!(context, "Exif orientation value ignored: {val:?}.");
|
||||||
Some(8) => return 270,
|
Orientation::NoTransforms
|
||||||
other => warn!(context, "Exif orientation value ignored: {other:?}."),
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
0
|
Orientation::NoTransforms
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All files in the blobdir.
|
/// All files in the blobdir.
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ async fn test_recode_image_2() {
|
|||||||
has_exif: true,
|
has_exif: true,
|
||||||
original_width: 2000,
|
original_width: 2000,
|
||||||
original_height: 1800,
|
original_height: 1800,
|
||||||
orientation: 270,
|
orientation: Some(Orientation::Rotate270),
|
||||||
compressed_width: 1800,
|
compressed_width: 1800,
|
||||||
compressed_height: 2000,
|
compressed_height: 2000,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -336,6 +336,28 @@ async fn test_recode_image_2() {
|
|||||||
assert_correct_rotation(&img_rotated);
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_recode_image_bad_exif() {
|
async fn test_recode_image_bad_exif() {
|
||||||
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
|
// `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) has_exif: bool,
|
||||||
pub(crate) original_width: u32,
|
pub(crate) original_width: u32,
|
||||||
pub(crate) original_height: u32,
|
pub(crate) original_height: u32,
|
||||||
pub(crate) orientation: i32,
|
pub(crate) orientation: Option<Orientation>,
|
||||||
pub(crate) res_viewtype: Option<Viewtype>,
|
pub(crate) res_viewtype: Option<Viewtype>,
|
||||||
pub(crate) compressed_width: u32,
|
pub(crate) compressed_width: u32,
|
||||||
pub(crate) compressed_height: u32,
|
pub(crate) compressed_height: u32,
|
||||||
@@ -546,7 +568,7 @@ impl SendImageCheckMediaquality<'_> {
|
|||||||
let has_exif = self.has_exif;
|
let has_exif = self.has_exif;
|
||||||
let original_width = self.original_width;
|
let original_width = self.original_width;
|
||||||
let original_height = self.original_height;
|
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 res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
|
||||||
let compressed_width = self.compressed_width;
|
let compressed_width = self.compressed_width;
|
||||||
let compressed_height = self.compressed_height;
|
let compressed_height = self.compressed_height;
|
||||||
|
|||||||
BIN
test-data/image/rectangle200x180-vflipped.jpg
Normal file
BIN
test-data/image/rectangle200x180-vflipped.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user