use std::path::Path;
use image::GenericImageView;
use thiserror::Error;
use crate::pixel::PixelFormat;
use crate::raster::Raster;
#[derive(Debug, Error)]
pub enum SourceError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("image decode error: {0}")]
Decode(#[from] image::ImageError),
#[error("unsupported color type: {0:?}")]
UnsupportedColorType(image::ColorType),
#[error("raster construction error: {0}")]
Raster(#[from] crate::raster::RasterError),
}
fn color_type_to_format(ct: image::ColorType) -> Result<PixelFormat, SourceError> {
match ct {
image::ColorType::L8 => Ok(PixelFormat::Gray8),
image::ColorType::L16 => Ok(PixelFormat::Gray16),
image::ColorType::Rgb8 => Ok(PixelFormat::Rgb8),
image::ColorType::Rgba8 => Ok(PixelFormat::Rgba8),
image::ColorType::Rgb16 => Ok(PixelFormat::Rgb16),
image::ColorType::Rgba16 => Ok(PixelFormat::Rgba16),
image::ColorType::La8 => Ok(PixelFormat::Rgba8),
image::ColorType::La16 => Ok(PixelFormat::Rgba16),
other => Err(SourceError::UnsupportedColorType(other)),
}
}
pub fn decode_file(path: &Path) -> Result<Raster, SourceError> {
let img = image::open(path)?;
let (width, height) = img.dimensions();
let color = img.color();
let format = color_type_to_format(color)?;
let data = match color {
image::ColorType::La8 => img.to_rgba8().into_raw(),
image::ColorType::La16 => {
let rgba16 = img.to_rgba16();
let pixels = rgba16.as_raw();
let mut bytes = Vec::with_capacity(pixels.len() * 2);
for &sample in pixels {
bytes.extend_from_slice(&sample.to_ne_bytes());
}
bytes
}
_ => img.into_bytes(),
};
Ok(Raster::new(width, height, format, data)?)
}
pub fn decode_bytes(bytes: &[u8]) -> Result<Raster, SourceError> {
let img = image::load_from_memory(bytes)?;
let (width, height) = img.dimensions();
let color = img.color();
let format = color_type_to_format(color)?;
let data = match color {
image::ColorType::La8 => img.to_rgba8().into_raw(),
image::ColorType::La16 => {
let rgba16 = img.to_rgba16();
let pixels = rgba16.as_raw();
let mut bytes = Vec::with_capacity(pixels.len() * 2);
for &sample in pixels {
bytes.extend_from_slice(&sample.to_ne_bytes());
}
bytes
}
_ => img.into_bytes(),
};
Ok(Raster::new(width, height, format, data)?)
}
pub fn generate_test_raster(width: u32, height: u32) -> Result<Raster, SourceError> {
let bpp = PixelFormat::Rgb8.bytes_per_pixel();
let mut data = vec![0u8; width as usize * height as usize * bpp];
for y in 0..height {
for x in 0..width {
let offset = (y as usize * width as usize + x as usize) * bpp;
data[offset] = (x * 255 / width.max(1)) as u8;
data[offset + 1] = (y * 255 / height.max(1)) as u8;
data[offset + 2] = ((x + y) * 255 / (width + height).max(1)) as u8;
}
}
Ok(Raster::new(width, height, PixelFormat::Rgb8, data)?)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn create_test_png(w: u32, h: u32) -> Vec<u8> {
let mut buf = Vec::new();
{
let encoder = image::codecs::png::PngEncoder::new(Cursor::new(&mut buf));
let data = vec![128u8; w as usize * h as usize * 3];
image::ImageEncoder::write_image(encoder, &data, w, h, image::ColorType::Rgb8.into())
.unwrap();
}
buf
}
fn create_test_jpeg(w: u32, h: u32) -> Vec<u8> {
let mut buf = Vec::new();
{
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(Cursor::new(&mut buf), 95);
let data = vec![128u8; w as usize * h as usize * 3];
image::ImageEncoder::write_image(encoder, &data, w, h, image::ColorType::Rgb8.into())
.unwrap();
}
buf
}
#[test]
fn decode_png_from_memory() {
let png = create_test_png(32, 24);
let raster = decode_bytes(&png).unwrap();
assert_eq!(raster.width(), 32);
assert_eq!(raster.height(), 24);
assert_eq!(raster.format(), PixelFormat::Rgb8);
assert_eq!(raster.data().len(), 32 * 24 * 3);
}
#[test]
fn decode_jpeg_from_memory() {
let jpeg = create_test_jpeg(16, 16);
let raster = decode_bytes(&jpeg).unwrap();
assert_eq!(raster.width(), 16);
assert_eq!(raster.height(), 16);
assert_eq!(raster.format(), PixelFormat::Rgb8);
}
#[test]
fn decode_invalid_bytes_returns_error() {
let result = decode_bytes(b"not an image");
assert!(result.is_err());
}
#[test]
fn decode_empty_bytes_returns_error() {
let result = decode_bytes(b"");
assert!(result.is_err());
}
#[test]
fn generate_test_raster_dimensions() {
let r = generate_test_raster(100, 50).unwrap();
assert_eq!(r.width(), 100);
assert_eq!(r.height(), 50);
assert_eq!(r.format(), PixelFormat::Rgb8);
assert_eq!(r.data().len(), 100 * 50 * 3);
}
#[test]
fn color_type_mapping() {
assert_eq!(
color_type_to_format(image::ColorType::L8).unwrap(),
PixelFormat::Gray8
);
assert_eq!(
color_type_to_format(image::ColorType::Rgb8).unwrap(),
PixelFormat::Rgb8
);
assert_eq!(
color_type_to_format(image::ColorType::Rgba8).unwrap(),
PixelFormat::Rgba8
);
assert_eq!(
color_type_to_format(image::ColorType::Rgb16).unwrap(),
PixelFormat::Rgb16
);
assert_eq!(
color_type_to_format(image::ColorType::La8).unwrap(),
PixelFormat::Rgba8
);
}
#[test]
fn decode_file_from_disk() {
let png = create_test_png(8, 8);
let raster = decode_bytes(&png).unwrap();
assert_eq!(raster.width(), 8);
assert_eq!(raster.height(), 8);
assert_eq!(raster.format(), PixelFormat::Rgb8);
#[cfg(not(miri))]
{
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.png");
std::fs::write(&path, &png).unwrap();
let from_disk = decode_file(&path).unwrap();
assert_eq!(from_disk.width(), 8);
assert_eq!(from_disk.height(), 8);
assert_eq!(from_disk.format(), PixelFormat::Rgb8);
}
}
#[test]
#[cfg_attr(miri, ignore)] fn decode_file_not_found() {
let result = decode_file(Path::new("/nonexistent/image.png"));
assert!(result.is_err());
}
}