From 065e8e64e51d435e10238d86ebf95f9567100282 Mon Sep 17 00:00:00 2001 From: bemyak Date: Fri, 8 Jul 2022 00:19:13 +0300 Subject: [PATCH] Apply orientation transformation based on EXIF data (#1912) --- Cargo.lock | 16 ++++ components/imageproc/Cargo.toml | 1 + components/imageproc/src/lib.rs | 27 +++++++ components/imageproc/tests/resize_image.rs | 74 +++++++++++++++++- .../imageproc/tests/test_imgs/exif_0.jpg | Bin 0 -> 661 bytes .../imageproc/tests/test_imgs/exif_1.jpg | Bin 0 -> 761 bytes .../imageproc/tests/test_imgs/exif_2.jpg | Bin 0 -> 762 bytes .../imageproc/tests/test_imgs/exif_3.jpg | Bin 0 -> 755 bytes .../imageproc/tests/test_imgs/exif_4.jpg | Bin 0 -> 758 bytes .../imageproc/tests/test_imgs/exif_5.jpg | Bin 0 -> 761 bytes .../imageproc/tests/test_imgs/exif_6.jpg | Bin 0 -> 763 bytes .../imageproc/tests/test_imgs/exif_7.jpg | Bin 0 -> 757 bytes .../imageproc/tests/test_imgs/exif_8.jpg | Bin 0 -> 759 bytes 13 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 components/imageproc/tests/test_imgs/exif_0.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_1.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_2.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_3.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_4.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_5.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_6.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_7.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_8.jpg diff --git a/Cargo.lock b/Cargo.lock index dae52756..58c7fa46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1203,6 +1203,7 @@ version = "0.1.0" dependencies = [ "config", "errors", + "kamadak-exif", "libs", "serde", "tempfile", @@ -1340,6 +1341,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70494964492bf8e491eb3951c5d70c9627eb7100ede6cc56d748b9a3f302cfb6" +dependencies = [ + "mutate_once", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1843,6 +1853,12 @@ dependencies = [ "similar", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "nanorand" version = "0.7.0" diff --git a/components/imageproc/Cargo.toml b/components/imageproc/Cargo.toml index 29c810d8..7f05f975 100644 --- a/components/imageproc/Cargo.toml +++ b/components/imageproc/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } +kamadak-exif = "0.5.4" errors = { path = "../errors" } utils = { path = "../utils" } diff --git a/components/imageproc/src/lib.rs b/components/imageproc/src/lib.rs index 179f9b25..09bf21f5 100644 --- a/components/imageproc/src/lib.rs +++ b/components/imageproc/src/lib.rs @@ -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::*; @@ -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 { @@ -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 { + 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 diff --git a/components/imageproc/tests/resize_image.rs b/components/imageproc/tests/resize_image.rs index 3f5b7f85..507de5e2 100644 --- a/components/imageproc/tests/resize_image.rs +++ b/components/imageproc/tests/resize_image.rs @@ -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#" @@ -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 diff --git a/components/imageproc/tests/test_imgs/exif_0.jpg b/components/imageproc/tests/test_imgs/exif_0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..36beb6863cdd569f6057de07e5c25f7872f42cb3 GIT binary patch literal 661 zcmex=nEb00R>vGcywlGb<|#3s7|}P@aKB zkX1<0(2-3zFp*uUP{gQl;zAB(r;P_igD!qhF-|IK;^Yz&myncFRa4i{)G{$OGqmaka3YSZQ|TeofBv2)jkyBj`3o2bj7%&n%q;96e=#zZ12MB83#+0bn~-B5dt#xm zl2Idvh||P{8xL|S8wY(5O)9#`C8lEXQ1v6oYha%d=dmWTdCS(KLwo-5zW&dU+5B$}|964)KiKV8@&CUG0P`x(H2?qr literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_1.jpg b/components/imageproc/tests/test_imgs/exif_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0bb69bf4da79db08c90b08a5bbf3197d5ac705f GIT binary patch literal 761 zcmex=kR1VRQz zDF#+Bn}NZLQ5wz;V$^`DVParl5=N*8ss$+rnhe$rrvKk&a0c55vXxu_GK5^caDD#| zFbHxm2rvjRGYT>=2{JMZGX6ipAP;maD?|<0^&q!1F|)9;v2$>8aRU`>6<}auWM*b! zVP<7zVFAk40_7Q41X+a?4ISBp0~6Vm3Pp?>CobercG`GQH0a_772~9$CQdFfaS2H& zRW)@DO)V2sGjj_|D`yv1H+K(Dui%i-u<(e;sN|H?wDgS3tm2Z=vhs?`s^*r~w)T$B zu1S-pOr17;#>`oZ7B5-4Z25|nt2S-kvUS_`9Xod&I(+2lvEwIBp1O4T%GGPvZ`{1~ z@X_NZPoF)1@$%KjPoKYh{r3IG&tD*aF)}d2y$ugFVBa`1POW{AbwT{3~Gnr#t@{4(<8J`}#jaX7j%_{NDxE|6sRY I#sB{%0Ge#oZ~y=R literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_2.jpg b/components/imageproc/tests/test_imgs/exif_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6f3ef37f4631841eb7a752e48379a1bd5d7f80a GIT binary patch literal 762 zcmex=kRWMBdk zQVgtMHUonfqcofy#HfK#!z2u4GXhnD0mx*aeg+8r|2Bg&*hY}84D%dK(z{JSR%*4XX z%F4n5R9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+kVDyN<3Z7&iyu^slZu)+xx~aJB&Af< z)HO7@(s#)lOnKGcaiiG7B=;GyFQV|7p#ChOXkD>(_r(_|I_H;XgxU*MEjE{vYi2hxhG&|KMM^ H{r{T)SPIpY literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_3.jpg b/components/imageproc/tests/test_imgs/exif_3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5012999fa57e703b17650f70928986405f5c8fa GIT binary patch literal 755 zcmex=kR1d)tV z46I-_1A`Z%G@Kp8r~y^O#K6EL3}rI{Re}M?WT1Wq2>t&ygEQDhkgenbkRjyih3or& zfI*OhL4ZMknNg5|Nsy6Qkn#T!26>=ESs`km?q^_RVrF4wW9Q)H;sz?%D!{d z!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~nmD<{#3dx9 zRMpfqG__1j&CD$Mvj*Ffy^QFtf0O{Kd#r4)VAl z3#+0bn~-B5dt#xml2Idvh||P{8xL|S8wY(5O)9#`C8lEXQ1v6oYha%d=dmWTd(~GO@jos8ePaEe$n!t_?PvY^&k)4__X_|2n*gSE B(yIUf literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_4.jpg b/components/imageproc/tests/test_imgs/exif_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..807020eb05fdf608db82e51b80f7cc2db7c3d29b GIT binary patch literal 758 zcmex=kRWMBaj zQVgtMHUonfqcofy#HayP!^FVABn)LU0#$+m$Yh{?1_=HCHiI+RMv$%K0+1o(>V@n3 ze}F-dgF%2nfSFN{fk}{&S&;Gn5e9jnLs=nepzdd2WMXDvWn<^y(iUwW$pkka<)WpdpCN3c< zrK+Z`p{ZqJYG!U>Y31zV>gMj@=@lFj8WtWA8I_!pnwFlCnN?g;T2@|BS=HRq+ScCD z*)?hMl&RCE&zL!D(c&dbmn~nha@D5ITefc7zGLUELx+zXJ$C%W$y1juU%7hi`i+~n z9zJ^fj z7{Xt-7kRWMBmn zQXmq@W?=AQl!mi|7&V}3m>3wCgrRJZDlh<<4AKLk|KDbC2HOa-m0SQagj~IFeg6+I z2y!q8FbFU+3NkPWGBOJ?{y)MX4|FIiL=Dva42(?7EUawo9GqO-Km}U`7?>EDnVDFa zSy@?FfU0YO@(e73tU`*0j%>n#iR?;+B1Vl97jh^&Z9FI%bn%0VaZ*teCzqJGgrtbvx}>nyN9P&a7buactm7Wa!P7idPZheaY<=ec|~Pab4zPmdq-#2 zq{&mJPMbbs=B!1Fmn>bje8tLDn>KIRx^4T8ox2VlK63Qf@e?OcUAlbb>b2`PZr*zM z=<$=M&z`?{`Re1R&tJZN`~KtSFOa_&8JOW-heiylzkq?j$i%|J%)$=x7b8VP*tI9kRWMBgl zQVgtMHUonfqcofy#HayP!^FVABn)LU0#$+m$Yh{?1_=HCHiI+RMv$%K0+1o(>V@n3 ze}F-dgF%2nfSFN{fk}{&S&;Gn5e9jnLs=nepzdd2WMXDvWn<^y?FU=d^$QZ#gA6AnydS1J@SYMi)`L)mHLLD8U#A5@H!ikdjN#Ka{e zrBv0_H8izMOwG(KEUlbfT;1F~JiUTLLc_u%BBPR1Qq$5iGP89XZ3R<7E#dCS&q+js2Tb?ESsqsNY)IC<*QkRWMBsp zQVgtMHUonfqcofy#HayP!^FVABn)LU0#$+m$Yh{?1_=HCHiI+RMv$%K0+1o(>V@n3 ze}F-dgF%2nfSFN{fk}{&S&;Gn5e9jnLs=nepzdd2WMXDvWn<^y?FU=d^$QZ#gA6AnydS1J@SYMi)`L)mHLLD8U#A5@H!ikdjN#Ka{e zrBv0_H8izMOwG(KEUlbfT;1F~JiUTLLc_u%BBPR1Qq$5iGP89XZ3R<7E#dCS&q+js2Tb?ESsqsNY)IC<*QkR1hN?z zr5ISjYz77|Mrk-Zh*1NohKYfJNf^pz1gZoBkjX&(3=sPNZ3btsjUZde1t3Gn)eG16 z{{Vv^2ZI2E05hW?1Ct;lvmoRDBMkCDhq6M{K;6&4$i&RT%Er#Y$;Ay+uvLJ8iIJI^ ziG`V!m4yYUx)vzUz#_;hq-f~KCLEZ^u2d*u)Hrb=hqBYggQ7tfKd2Zd6*X~kiHS={ zN~x-;YiMejn3|beSXw!|xVpJ}czOkggocGjL`Eg2q^6~3WM&nYl$MoOR8}>&w6?W( zbaqXeJZ0*%=`&`|TC{k{(q+q6tX#Ee^OmjKw(r=v>(JpNM~@vpaq`rq%U7;myME*5 zt%r{uKY9A>`HPpYK7RWA>z(JGL?fo zF37^FXviky7|5PjD6C}E$RXl1apA^;oXW;QA4HRiE^>*fm^@Vd2=W@(XT*7|i7cPN zJ%;etEe0NDMquPI3o_U<{Ih)h?;rou;;)bPe_C(