From b89348b92a9c1ae67a4bb69a6db69d14f881384d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 14 Feb 2024 15:12:28 +0100 Subject: [PATCH] Read EXIF data and apply image rotation (#3413) --- Cargo.lock | 16 +++++++++ Cargo.toml | 1 + assets/files/f2t.jpg | Bin 0 -> 441 bytes crates/typst/Cargo.toml | 1 + crates/typst/src/visualize/image/raster.rs | 38 +++++++++++++++++++-- tests/ref/bugs/870-image-rotation.png | Bin 0 -> 296 bytes tests/typ/bugs/870-image-rotation.typ | 6 ++++ 7 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 assets/files/f2t.jpg create mode 100644 tests/ref/bugs/870-image-rotation.png create mode 100644 tests/typ/bugs/870-image-rotation.typ diff --git a/Cargo.lock b/Cargo.lock index 13a112786..0099a6993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,6 +1175,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1375,6 +1384,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "native-tls" version = "0.2.11" @@ -2556,6 +2571,7 @@ dependencies = [ "icu_segmenter", "image", "indexmap 2.1.0", + "kamadak-exif", "kurbo", "lipsum", "log", diff --git a/Cargo.toml b/Cargo.toml index 6333cbb06..5624eba8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ if_chain = "1" image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } include_dir = "0.7" indexmap = { version = "2", features = ["serde"] } +kamadak-exif = "0.5" kurbo = "0.9" # in sync with usvg libfuzzer-sys = "0.4" lipsum = "0.9" diff --git a/assets/files/f2t.jpg b/assets/files/f2t.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15065b08c32b7f2cb2d5ea86095fb91236ff8cb4 GIT binary patch literal 441 zcmex=7PJ2Ll5GBZCklGmvCpU;<(g z_)`~N?KV)OiV#X zL0L^*Q%h4)Ue`cZ!$4I-Q{(>u25t_900sj_MnQ)EM;PRRHn9So0dffxFfuU%En(;2 z?FU=d^$QZ#gA6AnydS1J@SYMi)`L)mHLLD8U# zA5@H!ikdjN#Ka{erBv0_H8izMOwG(KEUlbfT;1F~JiUTLLc_u%BBPR1Qq$5iGP89XZ3R<7E#dCS&q+js2Tb?ESsqsNY) zIC<*QzEE%Kv{80Hh3-XaE2J literal 0 HcmV?d00001 diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index 650f53c38..0f33dbac2 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -36,6 +36,7 @@ icu_provider_blob = { workspace = true } icu_segmenter = { workspace = true } image = { workspace = true } indexmap = { workspace = true } +kamadak-exif = { workspace = true } kurbo = { workspace = true } lipsum = { workspace = true } log = { workspace = true } diff --git a/crates/typst/src/visualize/image/raster.rs b/crates/typst/src/visualize/image/raster.rs index 98ba8fc05..3c9afe43f 100644 --- a/crates/typst/src/visualize/image/raster.rs +++ b/crates/typst/src/visualize/image/raster.rs @@ -7,7 +7,7 @@ use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; use image::io::Limits; -use image::{guess_format, ImageDecoder, ImageResult}; +use image::{guess_format, DynamicImage, ImageDecoder, ImageResult}; use crate::diag::{bail, StrResult}; use crate::foundations::{Bytes, Cast}; @@ -39,13 +39,17 @@ impl RasterImage { } let cursor = io::Cursor::new(&data); - let (dynamic, icc) = match format { + let (mut dynamic, icc) = match format { RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)), RasterFormat::Png => decode_with(PngDecoder::new(cursor)), RasterFormat::Gif => decode_with(GifDecoder::new(cursor)), } .map_err(format_image_error)?; + if let Some(rotation) = exif_rotation(&data) { + apply_rotation(&mut dynamic, rotation); + } + Ok(Self(Arc::new(Repr { data, format, dynamic, icc }))) } @@ -129,6 +133,36 @@ impl TryFrom for RasterFormat { } } +/// Get rotation from EXIF metadata. +fn exif_rotation(data: &[u8]) -> Option { + let reader = exif::Reader::new(); + let mut cursor = std::io::Cursor::new(data); + let exif = reader.read_from_container(&mut cursor).ok()?; + let orient = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?; + orient.value.get_uint(0) +} + +/// Apply an EXIF rotation to a dynamic image. +fn apply_rotation(image: &mut DynamicImage, rotation: u32) { + use image::imageops as ops; + match rotation { + 2 => ops::flip_horizontal_in_place(image), + 3 => ops::rotate180_in_place(image), + 4 => ops::flip_vertical_in_place(image), + 5 => { + ops::flip_horizontal_in_place(image); + *image = image.rotate270(); + } + 6 => *image = image.rotate90(), + 7 => { + ops::flip_horizontal_in_place(image); + *image = image.rotate90(); + } + 8 => *image = image.rotate270(), + _ => {} + } +} + /// Format the user-facing raster graphic decoding error message. fn format_image_error(error: image::ImageError) -> EcoString { match error { diff --git a/tests/ref/bugs/870-image-rotation.png b/tests/ref/bugs/870-image-rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..83d9267d1c0440adb1a7db87b9b80db8acdc6083 GIT binary patch literal 296 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZV>y5XL)ujLSRnP@)5S5Q;?~i@2sAt_ zew5PCGeM1;h4WbhQ>U2Kw?zr8-<(c7U{+KK4%o49*ZcQ(>a1>lS9#s4;yKA=n~oR@ zhk$}Z0|O%yGUHyqfPNDLqf=?iv(>e$)SQxq9x2yte|lOf4<6d3m2Wfq_=w)$Hkg^zxrxJTNavqQ+sH~ zd5?4Vn;KcZ%5CZ?`N?~}oZ~ykhp_+85=K;az+AStc=I%%dykc|e%^b%$!YKW4p0zy My85}Sb4q9e0EMA%e*gdg literal 0 HcmV?d00001 diff --git a/tests/typ/bugs/870-image-rotation.typ b/tests/typ/bugs/870-image-rotation.typ new file mode 100644 index 000000000..56c3da1d0 --- /dev/null +++ b/tests/typ/bugs/870-image-rotation.typ @@ -0,0 +1,6 @@ +// Ensure that EXIF rotation is applied. +// https://github.com/image-rs/image/issues/1045 + +--- +// Files is from https://magnushoff.com/articles/jpeg-orientation/ +#image("/files/f2t.jpg", width: 10pt)