Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply orientation transformation based on EXIF data #1912

Merged
merged 2 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions components/imageproc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
kamadak-exif = "0.5.4"

errors = { path = "../errors" }
utils = { path = "../utils" }
Expand Down
27 changes: 27 additions & 0 deletions components/imageproc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use image::error::ImageResult;
use image::io::Reader as ImgReader;
use image::{imageops::FilterType, EncodableLayout};
use image::{ImageFormat, ImageOutputFormat};
use libs::image::DynamicImage;
use libs::{image, once_cell, rayon, regex, svg_metadata, webp};
use once_cell::sync::Lazy;
use rayon::prelude::*;
Expand Down Expand Up @@ -319,6 +320,8 @@ impl ImageOp {
None => img,
};

let img = fix_orientation(&img, &self.input_path).unwrap_or(img);

let mut f = File::create(target_path)?;

match self.format {
Expand All @@ -343,6 +346,30 @@ impl ImageOp {
}
}

/// Apply image rotation based on EXIF data
/// Returns `None` if no transformation is needed
pub fn fix_orientation(img: &DynamicImage, path: &Path) -> Option<DynamicImage> {
let file = std::fs::File::open(path).ok()?;
let mut buf_reader = std::io::BufReader::new(&file);
let exif_reader = exif::Reader::new();
let exif = exif_reader.read_from_container(&mut buf_reader).ok()?;
let orientation = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?.value.get_uint(0)?;
match orientation {
// Values are taken from the page 30 of
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
// For more details check http://sylvana.net/jpegcrop/exif_orientation.html
1 => None,
2 => Some(img.fliph()),
3 => Some(img.rotate180()),
4 => Some(img.flipv()),
5 => Some(img.fliph().rotate270()),
6 => Some(img.rotate90()),
7 => Some(img.fliph().rotate90()),
8 => Some(img.rotate270()),
_ => None,
}
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct EnqueueResponse {
/// The final URL for that asset
Expand Down
74 changes: 73 additions & 1 deletion components/imageproc/tests/resize_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::env;
use std::path::{PathBuf, MAIN_SEPARATOR as SLASH};

use config::Config;
use imageproc::{assert_processed_path_matches, ImageMetaResponse, Processor};
use imageproc::{assert_processed_path_matches, fix_orientation, ImageMetaResponse, Processor};
use libs::image::{self, DynamicImage, GenericImageView, Pixel};
use libs::once_cell::sync::Lazy;

static CONFIG: &str = r#"
Expand Down Expand Up @@ -153,4 +154,75 @@ fn read_image_metadata_webp() {
);
}

#[test]
fn fix_orientation_test() {
fn load_img_and_fix_orientation(img_name: &str) -> DynamicImage {
let path = TEST_IMGS.join(img_name);
let img = image::open(&path).unwrap();
fix_orientation(&img, &path).unwrap_or(img)
}

let img = image::open(TEST_IMGS.join("exif_1.jpg")).unwrap();
assert!(check_img(img));
assert!(check_img(load_img_and_fix_orientation("exif_0.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_1.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_2.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_3.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_4.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_5.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_6.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_7.jpg")));
assert!(check_img(load_img_and_fix_orientation("exif_8.jpg")));
}

#[test]
fn resize_image_applies_exif_rotation() {
// No exif metadata
assert!(resize_and_check("exif_0.jpg"));
// 1: Horizontal (normal)
assert!(resize_and_check("exif_1.jpg"));
// 2: Mirror horizontal
assert!(resize_and_check("exif_2.jpg"));
// 3: Rotate 180
assert!(resize_and_check("exif_3.jpg"));
// 4: Mirror vertical
assert!(resize_and_check("exif_4.jpg"));
// 5: Mirror horizontal and rotate 270 CW
assert!(resize_and_check("exif_5.jpg"));
// 6: Rotate 90 CW
assert!(resize_and_check("exif_6.jpg"));
// 7: Mirror horizontal and rotate 90 CW
assert!(resize_and_check("exif_7.jpg"));
// 8: Rotate 270 CW
assert!(resize_and_check("exif_8.jpg"));
}

fn resize_and_check(source_img: &str) -> bool {
let source_path = TEST_IMGS.join(source_img);
let tmpdir = tempfile::tempdir().unwrap().into_path();
let config = Config::parse(CONFIG).unwrap();
let mut proc = Processor::new(tmpdir.clone(), &config);

let resp = proc
.enqueue(source_img.into(), source_path, "scale", Some(16), Some(16), "jpg", None)
.unwrap();

proc.do_process().unwrap();
let processed_path = PathBuf::from(&resp.static_path);
let img = image::open(&tmpdir.join(processed_path)).unwrap();
check_img(img)
}

// Checks that an image has the correct orientation
fn check_img(img: DynamicImage) -> bool {
// top left is red
img.get_pixel(0, 0)[0] > 250 // because of the jpeg compression some colors are a bit less than 255
// top right is green
&& img.get_pixel(15, 0)[1] > 250
// bottom left is blue
&& img.get_pixel(0, 15)[2] > 250
// bottom right is white
&& img.get_pixel(15, 15).channels() == [255, 255, 255, 255]
}

// TODO: Test that hash remains the same if physical path is changed
Binary file added components/imageproc/tests/test_imgs/exif_0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_6.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_7.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/imageproc/tests/test_imgs/exif_8.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.