use crate::error::ImageError;
use crate::orientation::Orientation;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourcePixels {
pub width: u32,
pub height: u32,
}
impl SourcePixels {
pub fn count(self) -> u64 {
u64::from(self.width) * u64::from(self.height)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Measured {
pub source: SourcePixels,
pub orientation: Orientation,
pub format: image::ImageFormat,
}
impl Measured {
pub fn display_size(&self) -> (u32, u32) {
self.orientation.applied_size(self.source.width, self.source.height)
}
}
pub fn measure(path: &Path) -> Result<Measured, ImageError> {
let unreadable = |source| ImageError::Unreadable { path: path.to_path_buf(), source };
let file = std::fs::File::open(path).map_err(unreadable)?;
let reader = image::ImageReader::new(std::io::BufReader::new(file))
.with_guessed_format()
.map_err(unreadable)?;
let format = reader.format().ok_or_else(|| ImageError::Undecodable {
path: path.to_path_buf(),
source: image::ImageError::Unsupported(
image::error::ImageFormatHint::Unknown.into(),
),
})?;
let (width, height) = reader.into_dimensions().map_err(|source| ImageError::Undecodable {
path: path.to_path_buf(),
source,
})?;
Ok(Measured {
source: SourcePixels { width, height },
orientation: read_orientation(path),
format,
})
}
fn read_orientation(path: &Path) -> Orientation {
let Ok(file) = std::fs::File::open(path) else {
return Orientation::Upright;
};
let mut reader = std::io::BufReader::new(file);
match image::ImageReader::new(&mut reader).with_guessed_format() {
Ok(r) => match r.into_decoder() {
Ok(mut decoder) => match image::ImageDecoder::orientation(&mut decoder) {
Ok(o) => from_image_orientation(o),
Err(_) => Orientation::Upright,
},
Err(_) => Orientation::Upright,
},
Err(_) => Orientation::Upright,
}
}
fn from_image_orientation(o: image::metadata::Orientation) -> Orientation {
match o {
image::metadata::Orientation::NoTransforms => Orientation::Upright,
image::metadata::Orientation::Rotate90 => Orientation::Rotate90,
image::metadata::Orientation::Rotate180 => Orientation::Rotate180,
image::metadata::Orientation::Rotate270 => Orientation::Rotate270,
image::metadata::Orientation::FlipHorizontal => Orientation::FlipHorizontal,
image::metadata::Orientation::FlipVertical => Orientation::FlipVertical,
image::metadata::Orientation::Rotate90FlipH => Orientation::Transpose,
image::metadata::Orientation::Rotate270FlipH => Orientation::Transverse,
}
}
pub fn measured_path(path: &Path) -> PathBuf {
path.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
fn write_png(dir: &Path, name: &str, width: u32, height: u32) -> PathBuf {
let path = dir.join(name);
image::RgbaImage::from_pixel(width, height, image::Rgba([1, 2, 3, 255]))
.save(&path)
.unwrap();
path
}
#[test]
fn a_header_gives_the_size_without_decoding_the_picture() {
let dir = tempfile::tempdir().unwrap();
let path = write_png(dir.path(), "a.png", 640, 480);
let measured = measure(&path).unwrap();
assert_eq!(measured.source, SourcePixels { width: 640, height: 480 });
assert_eq!(measured.format, image::ImageFormat::Png);
assert_eq!(measured.display_size(), (640, 480));
}
#[test]
fn a_file_that_is_not_a_picture_is_undecodable_not_unreadable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("notes.txt");
std::fs::write(&path, b"this is not a picture").unwrap();
assert!(matches!(measure(&path), Err(ImageError::Undecodable { .. })));
}
#[test]
fn a_file_that_is_not_there_is_unreadable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gone.png");
assert!(matches!(measure(&path), Err(ImageError::Unreadable { .. })));
}
#[test]
fn a_picture_with_no_exif_measures_as_upright() {
let dir = tempfile::tempdir().unwrap();
let path = write_png(dir.path(), "b.png", 100, 50);
assert_eq!(measure(&path).unwrap().orientation, Orientation::Upright);
}
#[test]
fn a_truncated_file_is_reported_rather_than_panicking() {
let dir = tempfile::tempdir().unwrap();
let full = write_png(dir.path(), "c.png", 200, 200);
let bytes = std::fs::read(&full).unwrap();
let cut = dir.path().join("cut.png");
std::fs::write(&cut, &bytes[..bytes.len() / 3]).unwrap();
let _ = measure(&cut);
}
}