Skip to content

Commit

Permalink
Apply orientation transformation based on EXIF data (getzola#1912)
Browse files Browse the repository at this point in the history
  • Loading branch information
bemyak authored Jul 7, 2022
1 parent cec65d0 commit 065e8e6
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 1 deletion.
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.

0 comments on commit 065e8e6

Please sign in to comment.