use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SampleKind {
U8,
U16,
F32,
}
impl SampleKind {
#[must_use]
pub const fn size(self) -> usize {
match self {
Self::U8 => 1,
Self::U16 => 2,
Self::F32 => 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChannelLayout {
Gray,
GrayAlpha,
Rgb,
Rgba,
}
impl ChannelLayout {
#[must_use]
pub const fn channels(self) -> usize {
match self {
Self::Gray => 1,
Self::GrayAlpha => 2,
Self::Rgb => 3,
Self::Rgba => 4,
}
}
#[must_use]
pub const fn has_alpha(self) -> bool {
matches!(self, Self::GrayAlpha | Self::Rgba)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PixelFormat {
Gray8,
Gray16,
GrayA8,
Rgb8,
Rgba8,
Rgb16,
Rgba16,
RgbF32,
RgbaF32,
}
impl PixelFormat {
pub const ALL: &'static [Self] = &[
Self::Gray8,
Self::Gray16,
Self::GrayA8,
Self::Rgb8,
Self::Rgba8,
Self::Rgb16,
Self::Rgba16,
Self::RgbF32,
Self::RgbaF32,
];
#[must_use]
pub const fn layout(self) -> ChannelLayout {
match self {
Self::Gray8 | Self::Gray16 => ChannelLayout::Gray,
Self::GrayA8 => ChannelLayout::GrayAlpha,
Self::Rgb8 | Self::Rgb16 | Self::RgbF32 => ChannelLayout::Rgb,
Self::Rgba8 | Self::Rgba16 | Self::RgbaF32 => ChannelLayout::Rgba,
}
}
#[must_use]
pub const fn from_parts(layout: ChannelLayout, kind: SampleKind) -> Option<Self> {
match (layout, kind) {
(ChannelLayout::Gray, SampleKind::U8) => Some(Self::Gray8),
(ChannelLayout::Gray, SampleKind::U16) => Some(Self::Gray16),
(ChannelLayout::GrayAlpha, SampleKind::U8) => Some(Self::GrayA8),
(ChannelLayout::Rgb, SampleKind::U8) => Some(Self::Rgb8),
(ChannelLayout::Rgb, SampleKind::U16) => Some(Self::Rgb16),
(ChannelLayout::Rgb, SampleKind::F32) => Some(Self::RgbF32),
(ChannelLayout::Rgba, SampleKind::U8) => Some(Self::Rgba8),
(ChannelLayout::Rgba, SampleKind::U16) => Some(Self::Rgba16),
(ChannelLayout::Rgba, SampleKind::F32) => Some(Self::RgbaF32),
_ => None,
}
}
#[must_use]
pub const fn sample_kind(self) -> SampleKind {
match self {
Self::Gray8 | Self::GrayA8 | Self::Rgb8 | Self::Rgba8 => SampleKind::U8,
Self::Gray16 | Self::Rgb16 | Self::Rgba16 => SampleKind::U16,
Self::RgbF32 | Self::RgbaF32 => SampleKind::F32,
}
}
#[must_use]
pub const fn channels(self) -> usize {
self.layout().channels()
}
#[must_use]
pub const fn has_alpha(self) -> bool {
self.layout().has_alpha()
}
#[must_use]
pub const fn bytes_per_pixel(self) -> usize {
self.channels() * self.sample_kind().size()
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Gray8 => "gray8",
Self::Gray16 => "gray16",
Self::GrayA8 => "graya8",
Self::Rgb8 => "rgb8",
Self::Rgba8 => "rgba8",
Self::Rgb16 => "rgb16",
Self::Rgba16 => "rgba16",
Self::RgbF32 => "rgbf32",
Self::RgbaF32 => "rgbaf32",
}
}
}
impl fmt::Display for PixelFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ColorModel {
#[default]
Srgb,
}
pub trait Sample: sealed::Sealed + Copy + Send + Sync + 'static {
const KIND: SampleKind;
const FULL_SCALE: Self;
const ZERO: Self;
}
impl Sample for u8 {
const KIND: SampleKind = SampleKind::U8;
const FULL_SCALE: Self = u8::MAX;
const ZERO: Self = 0;
}
impl Sample for u16 {
const KIND: SampleKind = SampleKind::U16;
const FULL_SCALE: Self = u16::MAX;
const ZERO: Self = 0;
}
impl Sample for f32 {
const KIND: SampleKind = SampleKind::F32;
const FULL_SCALE: Self = 1.0;
const ZERO: Self = 0.0;
}
mod sealed {
pub trait Sealed {}
impl Sealed for u8 {}
impl Sealed for u16 {}
impl Sealed for f32 {}
}
#[macro_export]
macro_rules! dispatch_sample {
($kind:expr, $ty:ident => $body:block) => {
match $kind {
$crate::SampleKind::U8 => {
type $ty = u8;
$body
}
$crate::SampleKind::U16 => {
type $ty = u16;
$body
}
$crate::SampleKind::F32 => {
type $ty = f32;
$body
}
}
};
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
#[test]
fn bytes_per_pixel_matches_layout_times_sample_size() {
for &format in PixelFormat::ALL {
let expected = format.channels() * format.sample_kind().size();
assert_eq!(format.bytes_per_pixel(), expected, "{format}");
assert!(format.bytes_per_pixel() > 0, "{format}");
}
}
#[test]
fn v1_format_set_has_expected_sizes() {
assert_eq!(PixelFormat::Gray8.bytes_per_pixel(), 1);
assert_eq!(PixelFormat::Gray16.bytes_per_pixel(), 2);
assert_eq!(PixelFormat::GrayA8.bytes_per_pixel(), 2);
assert_eq!(PixelFormat::Rgb8.bytes_per_pixel(), 3);
assert_eq!(PixelFormat::Rgba8.bytes_per_pixel(), 4);
assert_eq!(PixelFormat::Rgb16.bytes_per_pixel(), 6);
assert_eq!(PixelFormat::Rgba16.bytes_per_pixel(), 8);
assert_eq!(PixelFormat::RgbF32.bytes_per_pixel(), 12);
assert_eq!(PixelFormat::RgbaF32.bytes_per_pixel(), 16);
}
#[test]
fn alpha_layouts_are_reported_consistently() {
for &format in PixelFormat::ALL {
assert_eq!(format.has_alpha(), format.layout().has_alpha(), "{format}");
}
assert!(PixelFormat::Rgba8.has_alpha());
assert!(PixelFormat::GrayA8.has_alpha());
assert!(!PixelFormat::Rgb8.has_alpha());
assert!(!PixelFormat::Gray8.has_alpha());
}
#[test]
fn format_names_are_unique_and_stable() {
let mut seen = std::collections::HashSet::new();
for &format in PixelFormat::ALL {
assert!(seen.insert(format.as_str()), "duplicate name {format}");
}
assert_eq!(PixelFormat::Rgba8.as_str(), "rgba8");
assert_eq!(
PixelFormat::ALL.len(),
9,
"SPEC §Pixel formats lists 9 v1 formats"
);
}
#[test]
fn dispatch_selects_the_matching_sample_type() {
fn sample_size(format: PixelFormat) -> usize {
dispatch_sample!(format.sample_kind(), S => { size_of::<S>() })
}
for &format in PixelFormat::ALL {
assert_eq!(sample_size(format), format.sample_kind().size(), "{format}");
}
}
#[test]
fn sample_constants_match_their_kind() {
assert_eq!(<u8 as Sample>::KIND, SampleKind::U8);
assert_eq!(<u16 as Sample>::KIND, SampleKind::U16);
assert_eq!(<f32 as Sample>::KIND, SampleKind::F32);
assert_eq!(<u8 as Sample>::FULL_SCALE, 255);
assert_eq!(<u16 as Sample>::FULL_SCALE, 65535);
assert_eq!(<u8 as Sample>::ZERO, 0);
}
#[test]
fn full_scale_is_the_nominal_range_not_the_type_maximum() {
assert!((<f32 as Sample>::FULL_SCALE - 1.0).abs() < f32::EPSILON);
let (full_scale, type_max) = (<f32 as Sample>::FULL_SCALE, f32::MAX);
assert!(
full_scale < type_max,
"{full_scale} should be far below {type_max}"
);
assert_eq!(<u8 as Sample>::FULL_SCALE, u8::MAX);
}
}