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,
Jxl,
}
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",
Self::Jxl => "jxl",
}
}
}
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, Hash)]
#[non_exhaustive]
pub enum ResolutionUnit {
Inch,
Centimetre,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Density {
pub x: f64,
pub y: f64,
pub unit: ResolutionUnit,
}
impl Density {
#[must_use]
pub const fn dpi(dpi: f64) -> Self {
Self {
x: dpi,
y: dpi,
unit: ResolutionUnit::Inch,
}
}
#[must_use]
pub fn from_exif_block(exif: &[u8]) -> Option<Self> {
let tree = crate::exif::Exif::parse(exif)?;
let x = tree.rational(0x011A)?;
let y = tree.rational(0x011B).unwrap_or(x);
let unit = match tree.integer(0x0128).unwrap_or(2) {
3 => ResolutionUnit::Centimetre,
2 => ResolutionUnit::Inch,
_ => return None,
};
(x > 0.0 && y > 0.0).then_some(Self { x, y, unit })
}
#[must_use]
pub fn x_dpi(&self) -> f64 {
match self.unit {
ResolutionUnit::Inch => self.x,
ResolutionUnit::Centimetre => self.x * 2.54,
}
}
#[must_use]
pub fn y_dpi(&self) -> f64 {
match self.unit {
ResolutionUnit::Inch => self.y,
ResolutionUnit::Centimetre => self.y * 2.54,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Comment {
pub keyword: String,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Background {
Grey(u16),
Rgb(u16, u16, u16),
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct SourceInfo {
pub density: Option<Density>,
pub progressive: bool,
pub palette: bool,
pub bits_per_sample: Option<u8>,
pub chroma_subsampling: Option<&'static str>,
pub background: Option<Background>,
pub compression: Option<&'static str>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ChromaSubsampling {
Yuv420,
Yuv444,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct EncodeOptions {
pub quality: u8,
pub lossless: bool,
pub effort: Option<u8>,
pub compression_level: Option<u8>,
pub palette: bool,
pub colours: Option<u16>,
pub dither: Option<f32>,
pub alpha_quality: u8,
pub near_lossless: bool,
pub chroma_subsampling: Option<ChromaSubsampling>,
pub keep_exif: bool,
pub keep_xmp: bool,
pub progressive: bool,
}
fn within(name: &'static str, value: u16, range: core::ops::RangeInclusive<u16>) -> Result<()> {
if range.contains(&value) {
Ok(())
} else {
Err(PixelsError::invalid_argument(
name,
format!(
"must be in {}..={}, got {value}",
range.start(),
range.end()
),
))
}
}
impl EncodeOptions {
pub const DEFAULT_QUALITY: u8 = 80;
pub fn with_quality(quality: u8) -> Result<Self> {
within("quality", quality.into(), 1..=100)?;
Ok(Self {
quality,
..Self::default()
})
}
#[must_use]
pub const fn with_lossless(mut self, lossless: bool) -> Self {
self.lossless = lossless;
self
}
pub fn with_effort(mut self, effort: u8) -> Result<Self> {
within("effort", effort.into(), 0..=10)?;
self.effort = Some(effort);
Ok(self)
}
pub fn with_compression_level(mut self, level: u8) -> Result<Self> {
within("compression_level", level.into(), 0..=9)?;
self.compression_level = Some(level);
Ok(self)
}
#[must_use]
pub const fn with_palette(mut self, palette: bool) -> Self {
self.palette = palette;
self
}
pub fn with_colours(mut self, colours: u16) -> Result<Self> {
within("colours", colours, 2..=256)?;
self.colours = Some(colours);
Ok(self)
}
pub fn with_dither(mut self, dither: f32) -> Result<Self> {
if !(0.0..=1.0).contains(&dither) {
return Err(PixelsError::invalid_argument(
"dither",
format!("must be in 0..=1, got {dither}"),
));
}
self.dither = Some(dither);
Ok(self)
}
pub fn with_alpha_quality(mut self, quality: u8) -> Result<Self> {
within("alpha_quality", quality.into(), 0..=100)?;
self.alpha_quality = quality;
Ok(self)
}
#[must_use]
pub const fn with_near_lossless(mut self, near_lossless: bool) -> Self {
self.near_lossless = near_lossless;
self
}
#[must_use]
pub const fn with_chroma_subsampling(mut self, subsampling: ChromaSubsampling) -> Self {
self.chroma_subsampling = Some(subsampling);
self
}
#[must_use]
pub const fn with_keep_exif(mut self, keep: bool) -> Self {
self.keep_exif = keep;
self
}
#[must_use]
pub const fn with_keep_xmp(mut self, keep: bool) -> Self {
self.keep_xmp = keep;
self
}
#[must_use]
pub const fn with_keep_metadata(mut self, keep: bool) -> Self {
self.keep_exif = keep;
self.keep_xmp = keep;
self
}
#[must_use]
pub const fn with_progressive(mut self, progressive: bool) -> Self {
self.progressive = progressive;
self
}
#[must_use]
pub const fn wants_palette(&self) -> bool {
self.palette || self.colours.is_some() || self.dither.is_some()
}
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
quality: Self::DEFAULT_QUALITY,
lossless: false,
effort: None,
compression_level: None,
palette: false,
colours: None,
dither: None,
alpha_quality: 100,
near_lossless: false,
chroma_subsampling: None,
keep_exif: false,
keep_xmp: false,
progressive: 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 pages(&self) -> u32 {
1
}
fn icc_profile(&self) -> Option<&[u8]> {
None
}
fn exif(&self) -> Option<&[u8]> {
None
}
fn xmp(&self) -> Option<&[u8]> {
None
}
fn source_info(&self) -> SourceInfo {
SourceInfo::default()
}
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 set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
let _ = exif;
Ok(())
}
fn set_xmp(&mut self, xmp: Option<&[u8]>) -> Result<()> {
let _ = xmp;
Ok(())
}
fn set_density(&mut self, density: Option<Density>) -> Result<()> {
let _ = density;
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,
Format::Jxl,
];
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);
}
}