use std::path::Path;
use std::time::Duration;
use crate::color::ColorSpace;
use crate::error::{Error, Result};
use crate::frame::{Frame, PixelFormat};
pub const DEFAULT_MAX_PIXELS: u64 = 16_384 * 16_384;
pub fn load_image(path: impl AsRef<Path>) -> Result<Frame> {
load_image_limited(path, DEFAULT_MAX_PIXELS)
}
pub fn load_image_limited(path: impl AsRef<Path>, max_pixels: u64) -> Result<Frame> {
let path = path.as_ref();
let reader = image::ImageReader::open(path)
.map_err(|error| Error::io(path, error))?
.with_guessed_format()
.map_err(|error| Error::io(path, error))?;
if let Ok((width, height)) = reader.into_dimensions() {
let pixels = u64::from(width) * u64::from(height);
if pixels > max_pixels {
return Err(Error::unsupported(format!(
"{} is {width}×{height} — {pixels} pixels, past the {max_pixels} ceiling",
path.display()
)));
}
}
let decoded = image::ImageReader::open(path)
.map_err(|error| Error::io(path, error))?
.with_guessed_format()
.map_err(|error| Error::io(path, error))?
.decode()
.map_err(|error| Error::unsupported(format!("{}: {error}", path.display())))?;
frame_from_image(decoded)
}
pub fn load_image_bytes(bytes: &[u8]) -> Result<Frame> {
let decoded = image::load_from_memory(bytes)
.map_err(|error| Error::unsupported(format!("not a readable image: {error}")))?;
frame_from_image(decoded)
}
fn frame_from_image(image: image::DynamicImage) -> Result<Frame> {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
Frame::packed(
width,
height,
PixelFormat::Rgba8,
ColorSpace::srgb(),
Duration::ZERO,
rgba.into_raw(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn png(path: &Path, width: u32, height: u32) {
let mut buffer = image::RgbaImage::new(width, height);
for (x, y, pixel) in buffer.enumerate_pixels_mut() {
*pixel = image::Rgba([(x * 60) as u8, (y * 60) as u8, 128, 255]);
}
buffer.save(path).unwrap();
}
#[test]
fn an_image_arrives_as_an_rgba_frame() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("swatch.png");
png(&path, 4, 3);
let frame = load_image(&path).unwrap();
assert_eq!((frame.width(), frame.height()), (4, 3));
assert_eq!(frame.format(), PixelFormat::Rgba8);
assert_eq!(frame.byte_len(), 4 * 3 * 4);
assert_eq!(frame.pts(), Duration::ZERO);
}
#[test]
fn a_still_is_full_range_and_needs_no_matrix() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("swatch.png");
png(&path, 2, 2);
let frame = load_image(&path).unwrap();
assert_eq!(frame.color(), ColorSpace::srgb());
assert_eq!(frame.color().range, crate::color::Range::Full);
}
#[test]
fn the_pixels_survive_the_trip() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("swatch.png");
png(&path, 2, 2);
let frame = load_image(&path).unwrap();
let row = frame.row(0, 0).unwrap();
assert_eq!(&row[..4], &[0, 0, 128, 255]);
assert_eq!(&row[4..8], &[60, 0, 128, 255]);
}
#[test]
fn an_image_in_memory_loads_the_same_way() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("swatch.png");
png(&path, 3, 3);
let bytes = std::fs::read(&path).unwrap();
let frame = load_image_bytes(&bytes).unwrap();
assert_eq!((frame.width(), frame.height()), (3, 3));
}
#[test]
fn a_picture_past_the_ceiling_is_refused_from_its_header() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("swatch.png");
png(&path, 64, 64);
let Err(Error::Unsupported { what }) = load_image_limited(&path, 100) else {
panic!("64×64 is 4096 pixels, past a ceiling of 100");
};
assert!(what.contains("ceiling"), "{what}");
}
#[test]
fn a_file_that_is_not_an_image_is_refused_rather_than_guessed_at() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("notes.txt");
std::fs::write(&path, b"not a picture").unwrap();
assert!(load_image(&path).is_err());
assert!(load_image_bytes(b"not a picture").is_err());
}
#[test]
fn a_missing_file_says_which_file() {
let Err(Error::Io { path, .. }) = load_image("definitely/not/here.png") else {
panic!("that file does not exist");
};
assert!(path.ends_with("here.png"));
}
}