summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-02-14 15:12:28 +0100
committerGitHub <noreply@github.com>2024-02-14 14:12:28 +0000
commitb89348b92a9c1ae67a4bb69a6db69d14f881384d (patch)
treec48f067d9dce5eae08e95f48b47222791b79f442
parentfcf64d0ee0c2c48580f5d3576f8d53df4922c5e9 (diff)
Read EXIF data and apply image rotation (#3413)
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml1
-rw-r--r--assets/files/f2t.jpgbin0 -> 441 bytes
-rw-r--r--crates/typst/Cargo.toml1
-rw-r--r--crates/typst/src/visualize/image/raster.rs38
-rw-r--r--tests/ref/bugs/870-image-rotation.pngbin0 -> 296 bytes
-rw-r--r--tests/typ/bugs/870-image-rotation.typ6
7 files changed, 60 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 13a11278..0099a699 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1176,6 +1176,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1376,6 +1385,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 6333cbb0..5624eba8 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 00000000..15065b08
--- /dev/null
+++ b/assets/files/f2t.jpg
Binary files differ
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index 650f53c3..0f33dbac 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 98ba8fc0..3c9afe43 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<image::ImageFormat> for RasterFormat {
}
}
+/// Get rotation from EXIF metadata.
+fn exif_rotation(data: &[u8]) -> Option<u32> {
+ 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 00000000..83d9267d
--- /dev/null
+++ b/tests/ref/bugs/870-image-rotation.png
Binary files differ
diff --git a/tests/typ/bugs/870-image-rotation.typ b/tests/typ/bugs/870-image-rotation.typ
new file mode 100644
index 00000000..56c3da1d
--- /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)