use std::path::Path;
use image::ImageDecoder;
pub fn decode_oriented_file(path: &Path) -> image::ImageResult<image::DynamicImage> {
let (img, orientation) = decode_raw_with_orientation(path)?;
Ok(apply(img, orientation))
}
pub fn decode_oriented_reader<R: std::io::BufRead + std::io::Seek>(
reader: R,
) -> image::ImageResult<image::DynamicImage> {
let (img, orientation) = decode_raw_with_orientation_reader(reader)?;
Ok(apply(img, orientation))
}
pub fn decode_oriented_bytes(bytes: &[u8]) -> Option<image::DynamicImage> {
let decoder = image::ImageReader::new(std::io::Cursor::new(bytes))
.with_guessed_format()
.ok()?
.into_decoder()
.ok()?;
let (img, orientation) = decode_decoder(decoder).ok()?;
Some(apply(img, orientation))
}
pub fn decode_raw_with_orientation(
path: &Path,
) -> image::ImageResult<(image::DynamicImage, image::metadata::Orientation)> {
let reader = std::io::BufReader::new(std::fs::File::open(path)?);
decode_raw_with_orientation_reader(reader)
}
pub fn decode_raw_with_orientation_reader<R: std::io::BufRead + std::io::Seek>(
reader: R,
) -> image::ImageResult<(image::DynamicImage, image::metadata::Orientation)> {
let decoder = image::ImageReader::new(reader)
.with_guessed_format()?
.into_decoder()?;
decode_decoder(decoder)
}
fn decode_decoder<D: ImageDecoder>(
mut decoder: D,
) -> image::ImageResult<(image::DynamicImage, image::metadata::Orientation)> {
let orientation = decoder
.orientation()
.unwrap_or(image::metadata::Orientation::NoTransforms);
let img = image::DynamicImage::from_decoder(decoder)?;
Ok((img, orientation))
}
fn apply(
mut img: image::DynamicImage,
orientation: image::metadata::Orientation,
) -> image::DynamicImage {
img.apply_orientation(orientation);
img
}
#[cfg(test)]
mod tests {
use super::*;
use image::GenericImage;
fn jpeg_with_orientation(orientation: u16) -> Vec<u8> {
let mut img = image::DynamicImage::new_rgb8(4, 2);
for y in 0..2u32 {
for x in 0..4u32 {
img.put_pixel(
x,
y,
if y == 0 {
image::Rgba([255, 255, 255, 255])
} else {
image::Rgba([0, 0, 0, 255])
},
);
}
}
let mut jpeg = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut jpeg),
image::ImageFormat::Jpeg,
)
.unwrap();
let mut tiff = Vec::new();
tiff.extend_from_slice(b"MM");
tiff.extend_from_slice(&42u16.to_be_bytes());
tiff.extend_from_slice(&8u32.to_be_bytes()); tiff.extend_from_slice(&1u16.to_be_bytes()); tiff.extend_from_slice(&0x0112u16.to_be_bytes()); tiff.extend_from_slice(&3u16.to_be_bytes()); tiff.extend_from_slice(&1u32.to_be_bytes()); tiff.extend_from_slice(&orientation.to_be_bytes());
tiff.extend_from_slice(&0u16.to_be_bytes()); tiff.extend_from_slice(&0u32.to_be_bytes());
let mut app1 = Vec::new();
app1.extend_from_slice(b"Exif\0\0");
app1.extend_from_slice(&tiff);
let mut out = Vec::new();
out.extend_from_slice(&jpeg[0..2]); out.extend_from_slice(&[0xFF, 0xE1]); out.extend_from_slice(&(app1.len() as u16 + 2).to_be_bytes());
out.extend_from_slice(&app1);
out.extend_from_slice(&jpeg[2..]);
out
}
fn top_left_is_white(img: &image::DynamicImage) -> bool {
let rgb = img.to_rgb8();
rgb.get_pixel(0, 0)[0] > 128
}
#[test]
fn untagged_file_passes_through_untouched() {
let mut plain = Vec::new();
let img = image::DynamicImage::new_rgb8(4, 2);
img.write_to(
&mut std::io::Cursor::new(&mut plain),
image::ImageFormat::Jpeg,
)
.unwrap();
let decoded = decode_oriented_bytes(&plain).unwrap();
assert_eq!((decoded.width(), decoded.height()), (4, 2));
}
#[test]
fn orientation6_yields_the_display_canvas() {
let bytes = jpeg_with_orientation(6);
let decoded = decode_oriented_bytes(&bytes).unwrap();
assert_eq!((decoded.width(), decoded.height()), (2, 4));
let rgb = decoded.to_rgb8();
assert!(
rgb.get_pixel(1, 0)[0] > 128,
"white row must move to the right column"
);
}
#[test]
fn file_and_bytes_variants_agree() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("o6.jpg");
std::fs::write(&path, jpeg_with_orientation(6)).unwrap();
let via_file = decode_oriented_file(&path).unwrap();
let via_bytes = decode_oriented_bytes(&jpeg_with_orientation(6)).unwrap();
assert_eq!(
(via_file.width(), via_file.height()),
(via_bytes.width(), via_bytes.height())
);
assert_eq!(top_left_is_white(&via_file), top_left_is_white(&via_bytes));
}
#[test]
fn raw_variant_returns_the_tag_for_the_caller_to_apply() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("o6.jpg");
std::fs::write(&path, jpeg_with_orientation(6)).unwrap();
let (img, orientation) = decode_raw_with_orientation(&path).unwrap();
assert_eq!(
(img.width(), img.height()),
(4, 2),
"raw canvas is untouched"
);
assert!(matches!(
orientation,
image::metadata::Orientation::Rotate90
));
}
}