use crate::{
ImageDescriptor, Orientation, PixelFormat, PixelsError, Region, Result, Sink, TileMut,
};
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Format {
Raw,
Png,
Jpeg,
Gif,
Tiff,
WebP,
Avif,
}
impl Format {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Raw => "raw",
Self::Png => "png",
Self::Jpeg => "jpeg",
Self::Gif => "gif",
Self::Tiff => "tiff",
Self::WebP => "webp",
Self::Avif => "avif",
}
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DecodeCapability {
Sequential,
Regions,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Animation {
pub frame_count: u32,
pub loop_count: u32,
pub frame_durations_ms: Vec<u32>,
}
impl Animation {
#[must_use]
pub fn new(frame_durations_ms: Vec<u32>, loop_count: u32) -> Option<Self> {
let frame_count = u32::try_from(frame_durations_ms.len()).ok()?;
(frame_count >= 2).then_some(Self {
frame_count,
loop_count,
frame_durations_ms,
})
}
#[must_use]
pub fn duration_ms(&self) -> u64 {
self.frame_durations_ms.iter().map(|&d| u64::from(d)).sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Metadata {
pub width: u32,
pub height: u32,
pub format: Format,
pub pixel: PixelFormat,
}
impl Metadata {
#[must_use]
pub const fn new(desc: &ImageDescriptor, format: Format) -> Self {
Self {
width: desc.width,
height: desc.height,
format,
pixel: desc.pixel,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct EncodeOptions {
pub quality: u8,
pub lossless: bool,
}
impl EncodeOptions {
pub const DEFAULT_QUALITY: u8 = 80;
pub fn with_quality(quality: u8) -> Result<Self> {
if !(1..=100).contains(&quality) {
return Err(PixelsError::invalid_argument(
"quality",
format!("must be in 1..=100, got {quality}"),
));
}
Ok(Self {
quality,
..Self::default()
})
}
#[must_use]
pub const fn with_lossless(mut self, lossless: bool) -> Self {
self.lossless = lossless;
self
}
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
quality: Self::DEFAULT_QUALITY,
lossless: false,
}
}
}
pub trait Codec {
fn format(&self) -> Format;
fn magic_len(&self) -> usize;
fn probe(&self, prefix: &[u8]) -> bool;
}
pub trait Decoder: Send + fmt::Debug {
fn descriptor(&self) -> ImageDescriptor;
fn capability(&self) -> DecodeCapability {
DecodeCapability::Sequential
}
fn orientation(&self) -> Orientation {
Orientation::Normal
}
fn animation(&self) -> Option<Animation> {
None
}
fn icc_profile(&self) -> Option<&[u8]> {
None
}
fn read_row(&mut self, out: &mut [u8]) -> Result<()>;
fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
let _ = (region, out);
Err(PixelsError::unsupported(
"this decoder is sequential; region decode requires DecodeCapability::Regions",
))
}
fn reduced_descriptor(&self, target: (u32, u32)) -> Option<ImageDescriptor> {
let _ = target;
None
}
fn reduce_to(&mut self, descriptor: ImageDescriptor) -> Result<()> {
let _ = descriptor;
Err(PixelsError::unsupported(
"this decoder has only one resolution",
))
}
}
pub trait Encoder: Send {
fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
let _ = profile;
Ok(())
}
fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()>;
fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()>;
fn finish(&mut self, sink: &mut dyn Sink) -> Result<()>;
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
use crate::ErrorCode;
#[test]
fn quality_is_validated_at_the_boundary() {
assert_eq!(EncodeOptions::with_quality(1).unwrap().quality, 1);
assert_eq!(EncodeOptions::with_quality(100).unwrap().quality, 100);
assert_eq!(
EncodeOptions::with_quality(0).unwrap_err().code(),
ErrorCode::InvalidArgument
);
assert_eq!(
EncodeOptions::with_quality(101).unwrap_err().code(),
ErrorCode::InvalidArgument
);
assert_eq!(EncodeOptions::default().quality, 80);
}
#[test]
fn format_names_are_stable_and_unique() {
let all = [
Format::Raw,
Format::Png,
Format::Jpeg,
Format::Gif,
Format::Tiff,
Format::WebP,
Format::Avif,
];
let mut seen = std::collections::HashSet::new();
for f in all {
assert!(seen.insert(f.as_str()), "duplicate name {f}");
}
assert_eq!(Format::WebP.as_str(), "webp");
}
#[test]
fn metadata_mirrors_the_descriptor() {
let desc = ImageDescriptor::new(7, 5, PixelFormat::Rgba8).unwrap();
let meta = Metadata::new(&desc, Format::Png);
assert_eq!((meta.width, meta.height), (7, 5));
assert_eq!(meta.pixel, PixelFormat::Rgba8);
assert_eq!(meta.format, Format::Png);
}
#[test]
fn sequential_decoders_decline_region_decode() {
#[derive(Debug)]
struct Seq;
impl Decoder for Seq {
fn descriptor(&self) -> ImageDescriptor {
ImageDescriptor::new(1, 1, PixelFormat::Gray8).unwrap_or(ImageDescriptor {
width: 1,
height: 1,
pixel: PixelFormat::Gray8,
color: crate::ColorModel::Srgb,
})
}
fn read_row(&mut self, _: &mut [u8]) -> Result<()> {
Ok(())
}
}
let mut buf = crate::TileBuf::zeroed(Region::from_size(1, 1), PixelFormat::Gray8).unwrap();
let mut tile = buf.as_tile_mut().unwrap();
let err = Seq
.read_region(Region::from_size(1, 1), &mut tile)
.unwrap_err();
assert_eq!(err.code(), ErrorCode::Unsupported);
assert_eq!(Seq.capability(), DecodeCapability::Sequential);
}
}