Skip to content

Commit c27e4c5

Browse files
InsprillKeats
authored andcommitted
Adjust ImageMeta dimensions based on Exif orientation data (#2906)
* Adjust ImageMeta dimensions based on Exif data * Get image Exif data from the decoder This lets us avoid reading the file from disk twice - once to get the dimensions/contents, and once to get the image metadata. Now we create one decoder, get all the data we need from it, and turn it into a DynamicImage directly. * Get image size directly from ImageDecoder in get_rotated_size_test
1 parent 53d5636 commit c27e4c5

File tree

5 files changed

+70
-19
lines changed

5 files changed

+70
-19
lines changed

components/imageproc/src/helpers.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,8 @@ use libs::image::DynamicImage;
99

1010
/// Apply image rotation based on EXIF data
1111
/// Returns `None` if no transformation is needed
12-
pub fn fix_orientation(img: &DynamicImage, path: &Path) -> Option<DynamicImage> {
13-
let file = std::fs::File::open(path).ok()?;
14-
let mut buf_reader = std::io::BufReader::new(&file);
15-
let exif_reader = exif::Reader::new();
16-
let exif = exif_reader.read_from_container(&mut buf_reader).ok()?;
17-
let orientation =
18-
exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?.value.get_uint(0)?;
19-
match orientation {
12+
pub fn fix_orientation(img: &DynamicImage, raw_metadata: Option<Vec<u8>>) -> Option<DynamicImage> {
13+
match get_orientation(raw_metadata)? {
2014
// Values are taken from the page 30 of
2115
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
2216
// For more details check http://sylvana.net/jpegcrop/exif_orientation.html
@@ -32,6 +26,26 @@ pub fn fix_orientation(img: &DynamicImage, path: &Path) -> Option<DynamicImage>
3226
}
3327
}
3428

29+
/// Adjusts the width and height of an image based on EXIF rotation data.
30+
/// Returns `None` if no transformation is needed.
31+
pub fn get_rotated_size(w: u32, h: u32, raw_metadata: Option<Vec<u8>>) -> Option<(u32, u32)> {
32+
// See fix_orientation for the meaning of these values.
33+
match get_orientation(raw_metadata)? {
34+
5 | 6 | 7 | 8 => Some((h, w)),
35+
_ => None,
36+
}
37+
}
38+
39+
fn get_orientation(raw_metadata: Option<Vec<u8>>) -> Option<u32> {
40+
match raw_metadata {
41+
Some(metadata) => {
42+
let exif = exif::Reader::new().read_raw(metadata).ok()?;
43+
Some(exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?.value.get_uint(0)?)
44+
}
45+
None => None,
46+
}
47+
}
48+
3549
/// We only use the input_path to get the file stem.
3650
/// Hashing the resolved `input_path` would include the absolute path to the image
3751
/// with all filesystem components.

components/imageproc/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod meta;
44
mod ops;
55
mod processor;
66

7-
pub use helpers::fix_orientation;
7+
pub use helpers::{fix_orientation, get_rotated_size};
88
pub use meta::{read_image_metadata, ImageMeta, ImageMetaResponse};
99
pub use ops::{ResizeInstructions, ResizeOperation};
1010
pub use processor::{EnqueueResponse, Processor, RESIZED_SUBDIR};

components/imageproc/src/meta.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use errors::{anyhow, Context, Result};
22
use libs::avif_parse::read_avif;
3-
use libs::image::ImageReader;
3+
use libs::image::{ImageDecoder, ImageReader};
44
use libs::image::{ImageFormat, ImageResult};
55
use libs::svg_metadata::Metadata as SvgMetadata;
66
use serde::Serialize;
@@ -9,6 +9,8 @@ use std::fs::File;
99
use std::io::BufReader;
1010
use std::path::Path;
1111

12+
use crate::get_rotated_size;
13+
1214
/// Size and format read cheaply with `image`'s `Reader`.
1315
#[derive(Debug)]
1416
pub struct ImageMeta {
@@ -21,7 +23,13 @@ impl ImageMeta {
2123
pub fn read(path: &Path) -> ImageResult<Self> {
2224
let reader = ImageReader::open(path).and_then(ImageReader::with_guessed_format)?;
2325
let format = reader.format();
24-
let size = reader.into_dimensions()?;
26+
let mut decoder = reader.into_decoder()?;
27+
let mut size = decoder.dimensions();
28+
let raw_metadata = decoder.exif_metadata()?;
29+
30+
if let Some((w, h)) = get_rotated_size(size.0, size.1, raw_metadata) {
31+
size = (w, h);
32+
}
2533

2634
Ok(Self { size, format })
2735
}

components/imageproc/src/processor.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ use libs::ahash::{HashMap, HashSet};
99
use libs::image::codecs::avif::AvifEncoder;
1010
use libs::image::codecs::jpeg::JpegEncoder;
1111
use libs::image::imageops::FilterType;
12-
use libs::image::GenericImageView;
12+
use libs::image::{DynamicImage, GenericImageView, ImageDecoder, ImageReader};
1313
use libs::image::{EncodableLayout, ImageEncoder, ImageFormat};
1414
use libs::rayon::prelude::*;
15-
use libs::{image, webp};
15+
use libs::webp;
1616
use serde::{Deserialize, Serialize};
1717
use utils::fs as ufs;
1818

@@ -41,8 +41,13 @@ impl ImageOp {
4141
return Ok(());
4242
}
4343

44-
let img = image::open(&self.input_path)?;
45-
let mut img = fix_orientation(&img, &self.input_path).unwrap_or(img);
44+
let reader =
45+
ImageReader::open(&self.input_path).and_then(ImageReader::with_guessed_format)?;
46+
let mut decoder = reader.into_decoder()?;
47+
let raw_metadata = decoder.exif_metadata()?;
48+
let img = DynamicImage::from_decoder(decoder)?;
49+
50+
let mut img = fix_orientation(&img, raw_metadata).unwrap_or(img);
4651

4752
let img = match self.instr.crop_instruction {
4853
Some((x, y, w, h)) => img.crop(x, y, w, h),

components/imageproc/tests/resize_image.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::env;
22
use std::path::{PathBuf, MAIN_SEPARATOR as SLASH};
33

44
use config::Config;
5-
use imageproc::{fix_orientation, ImageMetaResponse, Processor, ResizeOperation};
6-
use libs::image::{self, DynamicImage, GenericImageView, Pixel};
5+
use imageproc::{fix_orientation, get_rotated_size, ImageMetaResponse, Processor, ResizeOperation};
6+
use libs::image::{self, DynamicImage, GenericImageView, ImageDecoder, ImageReader, Pixel};
77
use libs::once_cell::sync::Lazy;
88

99
/// Assert that `address` matches `prefix` + RESIZED_FILENAME regex + "." + `extension`,
@@ -513,12 +513,36 @@ fn read_image_metadata_avif() {
513513
);
514514
}
515515

516+
#[test]
517+
fn get_rotated_size_test() {
518+
fn is_landscape(img_name: &str) -> bool {
519+
let path = TEST_IMGS.join(img_name);
520+
let mut decoder = ImageReader::open(path).unwrap().into_decoder().unwrap();
521+
let (mut w, mut h) = decoder.dimensions();
522+
w = w + 1; // Test images are square, add an offset so we can tell if the dimensions actually changed.
523+
let metadata = decoder.exif_metadata().unwrap();
524+
(w, h) = get_rotated_size(w, h, metadata).unwrap_or((w, h));
525+
w > h
526+
}
527+
assert!(is_landscape("exif_0.jpg"));
528+
assert!(is_landscape("exif_1.jpg"));
529+
assert!(is_landscape("exif_2.jpg"));
530+
assert!(is_landscape("exif_3.jpg"));
531+
assert!(is_landscape("exif_4.jpg"));
532+
assert!(!is_landscape("exif_5.jpg"));
533+
assert!(!is_landscape("exif_6.jpg"));
534+
assert!(!is_landscape("exif_7.jpg"));
535+
assert!(!is_landscape("exif_8.jpg"));
536+
}
537+
516538
#[test]
517539
fn fix_orientation_test() {
518540
fn load_img_and_fix_orientation(img_name: &str) -> DynamicImage {
519541
let path = TEST_IMGS.join(img_name);
520-
let img = image::open(&path).unwrap();
521-
fix_orientation(&img, &path).unwrap_or(img)
542+
let mut decoder = ImageReader::open(path).unwrap().into_decoder().unwrap();
543+
let metadata = decoder.exif_metadata().unwrap();
544+
let img = DynamicImage::from_decoder(decoder).unwrap();
545+
fix_orientation(&img, metadata).unwrap_or(img)
522546
}
523547

524548
let img = image::open(TEST_IMGS.join("exif_1.jpg")).unwrap();

0 commit comments

Comments
 (0)