#![forbid(unsafe_code)]
mod image;
mod pixel;
pub use image::{ImageBuf, ImageRef};
pub use pixel::{
Bilevel, Cmyk8, ColorModel, Gray8, Gray16, Indexed8, Pixel, Rgb8, Rgb16, Rgba8, Rgba16, Sample,
};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid input: {0}")]
InvalidInput(&'static str),
#[error("unsupported: {0}")]
Unsupported(&'static str),
}
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Dimensions {
pub width: u32,
pub height: u32,
}
impl Dimensions {
pub fn new(width: u32, height: u32) -> Result<Self> {
if width == 0 || height == 0 {
return Err(Error::InvalidInput("zero-sized image"));
}
Ok(Self { width, height })
}
#[must_use]
pub fn num_pixels(self) -> Option<usize> {
(self.width as usize).checked_mul(self.height as usize)
}
#[must_use]
pub fn sample_count(self, channels: usize) -> Option<usize> {
self.num_pixels()?.checked_mul(channels)
}
#[must_use]
pub fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
}
pub trait EncodeImage<P: Pixel> {
fn encode_image(&self, image: ImageRef<'_, P>, out: &mut Vec<u8>) -> Result<usize>;
}
pub trait DecodeImage<P: Pixel> {
fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<P>>;
fn decode_image_into(&self, data: &[u8], dst: &mut ImageBuf<P>) -> Result<()> {
*dst = self.decode_image(data)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_displays_and_dimensions_fields() {
assert!(!Error::Unsupported("x").to_string().is_empty());
assert!(!Error::InvalidInput("y").to_string().is_empty());
let d = Dimensions {
width: 1920,
height: 1080,
};
assert_eq!(d.width, 1920);
assert_eq!(d.height, 1080);
}
#[test]
fn dimensions_new_rejects_zero() {
assert!(Dimensions::new(0, 4).is_err());
assert!(Dimensions::new(4, 0).is_err());
assert!(Dimensions::new(0, 0).is_err());
let d = Dimensions::new(4, 3).unwrap();
assert_eq!((d.width, d.height), (4, 3));
}
#[test]
fn dimensions_pixel_and_sample_counts() {
let d = Dimensions {
width: 4,
height: 3,
};
assert_eq!(d.num_pixels(), Some(12));
assert_eq!(d.sample_count(3), Some(36));
assert_eq!(d.sample_count(1), Some(12));
assert!(!d.is_empty());
}
#[test]
fn dimensions_is_empty() {
assert!(
Dimensions {
width: 0,
height: 5
}
.is_empty()
);
assert!(
Dimensions {
width: 5,
height: 0
}
.is_empty()
);
assert!(
!Dimensions {
width: 5,
height: 5
}
.is_empty()
);
}
#[test]
fn dimensions_sample_count_overflow_is_none() {
let d = Dimensions {
width: 0xFFFF,
height: 0xFFFF,
};
assert_eq!(d.num_pixels(), Some(0xFFFF * 0xFFFF));
assert_eq!(d.sample_count(usize::MAX), None);
}
}
#[cfg(test)]
mod trait_tests {
use super::*;
struct Trivial;
impl EncodeImage<Gray8> for Trivial {
fn encode_image(&self, image: ImageRef<'_, Gray8>, out: &mut Vec<u8>) -> Result<usize> {
out.extend_from_slice(image.as_samples());
Ok(image.as_samples().len())
}
}
impl DecodeImage<Gray8> for Trivial {
fn decode_image(&self, _data: &[u8]) -> Result<ImageBuf<Gray8>> {
ImageBuf::<Gray8>::new(vec![42u8], Dimensions::new(1, 1)?)
}
}
#[test]
fn encode_image_appends_and_counts() {
let img = ImageBuf::<Gray8>::new(vec![1, 2, 3, 4], Dimensions::new(2, 2).unwrap()).unwrap();
let mut out = vec![0xFF];
let n = Trivial.encode_image(img.as_ref(), &mut out).unwrap();
assert_eq!(n, 4);
assert_eq!(out, vec![0xFF, 1, 2, 3, 4]);
}
#[test]
fn decode_image_into_default_forwards() {
let mut dst = ImageBuf::<Gray8>::zeroed(Dimensions::new(1, 1).unwrap()).unwrap();
Trivial.decode_image_into(&[], &mut dst).unwrap();
assert_eq!(dst.as_samples(), &[42]);
}
#[test]
fn traits_are_object_safe() {
let enc: &dyn EncodeImage<Gray8> = &Trivial;
let dec: &dyn DecodeImage<Gray8> = &Trivial;
let img = ImageBuf::<Gray8>::new(vec![7u8], Dimensions::new(1, 1).unwrap()).unwrap();
let mut out = Vec::new();
assert_eq!(enc.encode_image(img.as_ref(), &mut out).unwrap(), 1);
assert_eq!(dec.decode_image(&[]).unwrap().as_samples(), &[42]);
}
}