Skip to content

Commit

Permalink
Add ImageDecoder::orientation method (#2328)
Browse files Browse the repository at this point in the history
  • Loading branch information
fintelia authored Oct 6, 2024
1 parent 1bf7543 commit 58922fb
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 6 deletions.
35 changes: 33 additions & 2 deletions src/codecs/jpeg/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::error::{
DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind,
};
use crate::image::{ImageDecoder, ImageFormat};
use crate::Limits;
use crate::{Limits, Orientation};

type ZuneColorSpace = zune_core::colorspace::ColorSpace;

Expand All @@ -17,6 +17,7 @@ pub struct JpegDecoder<R> {
width: u16,
height: u16,
limits: Limits,
orientation: Option<Orientation>,
// For API compatibility with the previous jpeg_decoder wrapper.
// Can be removed later, which would be an API break.
phantom: PhantomData<R>,
Expand Down Expand Up @@ -49,6 +50,7 @@ impl<R: BufRead + Seek> JpegDecoder<R> {
width,
height,
limits,
orientation: None,
phantom: PhantomData,
})
}
Expand All @@ -72,7 +74,23 @@ impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {
fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
let mut decoder = zune_jpeg::JpegDecoder::new(&self.input);
decoder.decode_headers().map_err(ImageError::from_jpeg)?;
Ok(decoder.exif().cloned())
let exif = decoder.exif().cloned();

self.orientation = Some(
exif.as_ref()
.and_then(|exif| Orientation::from_exif_chunk(exif))
.unwrap_or(Orientation::NoTransforms),
);

Ok(exif)
}

fn orientation(&mut self) -> ImageResult<Orientation> {
// `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet.
if self.orientation.is_none() {
let _ = self.exif_metadata()?;
}
Ok(self.orientation.unwrap())
}

fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
Expand Down Expand Up @@ -169,3 +187,16 @@ impl ImageError {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::{fs, io::Cursor};

#[test]
fn test_exif_orientation() {
let data = fs::read("tests/images/jpg/portrait_2.jpg").unwrap();
let mut decoder = JpegDecoder::new(Cursor::new(data)).unwrap();
assert_eq!(decoder.orientation().unwrap(), Orientation::FlipHorizontal);
}
}
13 changes: 13 additions & 0 deletions src/codecs/tiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::error::{
ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind,
};
use crate::image::{ImageDecoder, ImageEncoder, ImageFormat};
use crate::Orientation;

/// Decoder for TIFF images.
pub struct TiffDecoder<R>
Expand Down Expand Up @@ -207,6 +208,18 @@ impl<R: BufRead + Seek> ImageDecoder for TiffDecoder<R> {
}
}

fn orientation(&mut self) -> ImageResult<Orientation> {
if let Some(decoder) = &mut self.inner {
Ok(decoder
.find_tag(tiff::tags::Tag::Orientation)
.map_err(ImageError::from_tiff_decode)?
.and_then(|v| Orientation::from_exif(v.into_u16().ok()?.min(255) as u8))
.unwrap_or(Orientation::NoTransforms))
} else {
Ok(Orientation::NoTransforms)
}
}

fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> {
limits.check_support(&crate::LimitSupport::default())?;

Expand Down
27 changes: 24 additions & 3 deletions src/codecs/webp/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ use std::io::{BufRead, Read, Seek};
use crate::buffer::ConvertBuffer;
use crate::error::{DecodingError, ImageError, ImageResult};
use crate::image::{ImageDecoder, ImageFormat};
use crate::{AnimationDecoder, ColorType, Delay, Frame, Frames, RgbImage, Rgba, RgbaImage};
use crate::{
AnimationDecoder, ColorType, Delay, Frame, Frames, Orientation, RgbImage, Rgba, RgbaImage,
};

/// WebP Image format decoder.
///
/// Supports both lossless and lossy WebP images.
pub struct WebPDecoder<R> {
inner: image_webp::WebPDecoder<R>,
orientation: Option<Orientation>,
}

impl<R: BufRead + Seek> WebPDecoder<R> {
/// Create a new `WebPDecoder` from the Reader `r`.
pub fn new(r: R) -> ImageResult<Self> {
Ok(Self {
inner: image_webp::WebPDecoder::new(r).map_err(ImageError::from_webp_decode)?,
orientation: None,
})
}

Expand Down Expand Up @@ -65,9 +69,26 @@ impl<R: BufRead + Seek> ImageDecoder for WebPDecoder<R> {
}

fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
self.inner
let exif = self
.inner
.exif_metadata()
.map_err(ImageError::from_webp_decode)
.map_err(ImageError::from_webp_decode)?;

self.orientation = Some(
exif.as_ref()
.and_then(|exif| Orientation::from_exif_chunk(exif))
.unwrap_or(Orientation::NoTransforms),
);

Ok(exif)
}

fn orientation(&mut self) -> ImageResult<Orientation> {
// `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet.
if self.orientation.is_none() {
let _ = self.exif_metadata()?;
}
Ok(self.orientation.unwrap())
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::error::{
};
use crate::math::Rect;
use crate::traits::Pixel;
use crate::ImageBuffer;
use crate::{ImageBuffer, Orientation};

use crate::animation::Frames;

Expand Down Expand Up @@ -636,6 +636,17 @@ pub trait ImageDecoder {
Ok(None)
}

/// Returns the orientation of the image.
///
/// This is usually obtained from the Exif metadata, if present. Formats that don't support
/// indicating orientation in their image metadata will return `Ok(Orientation::NoTransforms)`.
fn orientation(&mut self) -> ImageResult<Orientation> {
Ok(self
.exif_metadata()?
.and_then(|chunk| Orientation::from_exif_chunk(&chunk))
.unwrap_or(Orientation::NoTransforms))
}

/// Returns the total number of bytes in the decoded image.
///
/// This is the size of the buffer that must be passed to `read_image` or
Expand Down
46 changes: 46 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use std::io::{Cursor, Read};

use byteorder_lite::{BigEndian, LittleEndian, ReadBytesExt};

/// Describes the transformations to be applied to the image.
/// Compatible with [Exif orientation](https://web.archive.org/web/20200412005226/https://www.impulseadventure.com/photo/exif-orientation.html).
///
Expand Down Expand Up @@ -53,4 +57,46 @@ impl Orientation {
Self::Rotate270 => 8,
}
}

pub(crate) fn from_exif_chunk(chunk: &[u8]) -> Option<Self> {
let mut reader = Cursor::new(chunk);

let mut magic = [0; 4];
reader.read_exact(&mut magic).ok()?;

match magic {
[0x49, 0x49, 42, 0] => {
let ifd_offset = reader.read_u32::<LittleEndian>().ok()?;
reader.set_position(ifd_offset as u64);
let entries = reader.read_u16::<LittleEndian>().ok()?;
for _ in 0..entries {
let tag = reader.read_u16::<LittleEndian>().ok()?;
let format = reader.read_u16::<LittleEndian>().ok()?;
let count = reader.read_u32::<LittleEndian>().ok()?;
let value = reader.read_u16::<LittleEndian>().ok()?;
let _padding = reader.read_u16::<LittleEndian>().ok()?;
if tag == 0x112 && format == 3 && count == 1 {
return Self::from_exif(value.min(255) as u8);
}
}
}
[0x4d, 0x4d, 0, 42] => {
let ifd_offset = reader.read_u32::<BigEndian>().ok()?;
reader.set_position(ifd_offset as u64);
let entries = reader.read_u16::<BigEndian>().ok()?;
for _ in 0..entries {
let tag = reader.read_u16::<BigEndian>().ok()?;
let format = reader.read_u16::<BigEndian>().ok()?;
let count = reader.read_u32::<BigEndian>().ok()?;
let value = reader.read_u16::<BigEndian>().ok()?;
let _padding = reader.read_u16::<BigEndian>().ok()?;
if tag == 0x112 && format == 3 && count == 1 {
return Self::from_exif(value.min(255) as u8);
}
}
}
_ => {}
}
None
}
}
Binary file added tests/images/jpg/portrait_2.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 58922fb

Please sign in to comment.