Skip to main content

zenpixels_convert/
error.rs

1//! Error types for pixel format conversion.
2
3use crate::{PixelDescriptor, TransferFunction};
4use core::fmt;
5
6/// Errors that can occur during pixel format negotiation or conversion.
7// TODO(0.3.0): add #[non_exhaustive] — removed to avoid semver break vs 0.2.3.
8#[derive(Debug, Clone, PartialEq)]
9pub enum ConvertError {
10    /// No supported format could be found for the source descriptor.
11    NoMatch { source: PixelDescriptor },
12    /// No conversion path exists between the two formats.
13    NoPath {
14        from: PixelDescriptor,
15        to: PixelDescriptor,
16    },
17    /// Source and destination buffer sizes don't match the expected dimensions.
18    BufferSize { expected: usize, actual: usize },
19    /// Width is zero or would overflow stride calculations.
20    InvalidWidth(u32),
21    /// The supported format list was empty.
22    EmptyFormatList,
23    /// Conversion between these transfer functions is not yet supported.
24    UnsupportedTransfer {
25        from: TransferFunction,
26        to: TransferFunction,
27    },
28    /// Alpha channel is not fully opaque and [`AlphaPolicy::DiscardIfOpaque`](crate::AlphaPolicy::DiscardIfOpaque) was set.
29    AlphaNotOpaque,
30    /// Depth reduction was requested but [`DepthPolicy::Forbid`](crate::DepthPolicy::Forbid) was set.
31    DepthReductionForbidden,
32    /// Alpha removal was requested but [`AlphaPolicy::Forbid`](crate::AlphaPolicy::Forbid) was set.
33    AlphaRemovalForbidden,
34    /// RGB-to-grayscale conversion requires explicit luma coefficients.
35    RgbToGray,
36    /// Buffer allocation failed.
37    AllocationFailed,
38    /// CMS transform could not be built (invalid ICC profile, unsupported color space, etc.).
39    CmsError(alloc::string::String),
40    // TODO(0.3.0): add HdrTransferRequiresToneMapping variant here once
41    // ConvertError is #[non_exhaustive]. Adding a variant to an exhaustive
42    // enum is a semver break. See also HdrPolicy in output.rs and
43    // imazen/zenpixels#10 for the full HDR provenance plan.
44}
45
46impl fmt::Display for ConvertError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::NoMatch { source } => {
50                write!(
51                    f,
52                    "no supported format matches source {:?}/{:?}",
53                    source.channel_type(),
54                    source.layout()
55                )
56            }
57            Self::NoPath { from, to } => {
58                write!(
59                    f,
60                    "no conversion path from {:?}/{:?} to {:?}/{:?}",
61                    from.channel_type(),
62                    from.layout(),
63                    to.channel_type(),
64                    to.layout()
65                )
66            }
67            Self::BufferSize { expected, actual } => {
68                write!(
69                    f,
70                    "buffer size mismatch: expected {expected} bytes, got {actual}"
71                )
72            }
73            Self::InvalidWidth(w) => write!(f, "invalid width: {w}"),
74            Self::EmptyFormatList => write!(f, "supported format list is empty"),
75            Self::UnsupportedTransfer { from, to } => {
76                write!(f, "unsupported transfer conversion: {from:?} → {to:?}")
77            }
78            Self::AlphaNotOpaque => write!(f, "alpha channel is not fully opaque"),
79            Self::DepthReductionForbidden => write!(f, "depth reduction forbidden by policy"),
80            Self::AlphaRemovalForbidden => write!(f, "alpha removal forbidden by policy"),
81            Self::RgbToGray => {
82                write!(f, "RGB-to-grayscale requires explicit luma coefficients")
83            }
84            Self::AllocationFailed => write!(f, "buffer allocation failed"),
85            Self::CmsError(msg) => write!(f, "CMS transform failed: {msg}"),
86        }
87    }
88}
89
90#[cfg(feature = "std")]
91impl std::error::Error for ConvertError {}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use alloc::format;
97
98    #[test]
99    fn display_no_match() {
100        let e = ConvertError::NoMatch {
101            source: PixelDescriptor::RGB8_SRGB,
102        };
103        let s = format!("{e}");
104        assert!(s.contains("no supported format"));
105        assert!(s.contains("U8"));
106        assert!(s.contains("Rgb"));
107    }
108
109    #[test]
110    fn display_no_path() {
111        let e = ConvertError::NoPath {
112            from: PixelDescriptor::RGB8_SRGB,
113            to: PixelDescriptor::GRAY8_SRGB,
114        };
115        let s = format!("{e}");
116        assert!(s.contains("no conversion path"));
117    }
118
119    #[test]
120    fn display_buffer_size() {
121        let e = ConvertError::BufferSize {
122            expected: 1024,
123            actual: 512,
124        };
125        let s = format!("{e}");
126        assert!(s.contains("1024"));
127        assert!(s.contains("512"));
128    }
129
130    #[test]
131    fn display_invalid_width() {
132        let e = ConvertError::InvalidWidth(0);
133        assert!(format!("{e}").contains("0"));
134    }
135
136    #[test]
137    fn display_empty_format_list() {
138        let s = format!("{}", ConvertError::EmptyFormatList);
139        assert!(s.contains("empty"));
140    }
141
142    #[test]
143    fn display_unsupported_transfer() {
144        let e = ConvertError::UnsupportedTransfer {
145            from: TransferFunction::Pq,
146            to: TransferFunction::Hlg,
147        };
148        let s = format!("{e}");
149        assert!(s.contains("Pq"));
150        assert!(s.contains("Hlg"));
151    }
152
153    #[test]
154    fn display_alpha_not_opaque() {
155        assert!(format!("{}", ConvertError::AlphaNotOpaque).contains("opaque"));
156    }
157
158    #[test]
159    fn display_depth_reduction_forbidden() {
160        assert!(format!("{}", ConvertError::DepthReductionForbidden).contains("forbidden"));
161    }
162
163    #[test]
164    fn display_alpha_removal_forbidden() {
165        assert!(format!("{}", ConvertError::AlphaRemovalForbidden).contains("forbidden"));
166    }
167
168    #[test]
169    fn display_rgb_to_gray() {
170        assert!(format!("{}", ConvertError::RgbToGray).contains("luma"));
171    }
172
173    #[test]
174    fn display_allocation_failed() {
175        assert!(format!("{}", ConvertError::AllocationFailed).contains("allocation"));
176    }
177
178    #[test]
179    fn display_cms_error() {
180        let e = ConvertError::CmsError(alloc::string::String::from("profile mismatch"));
181        let s = format!("{e}");
182        assert!(s.contains("CMS transform failed"));
183        assert!(s.contains("profile mismatch"));
184    }
185
186    #[test]
187    fn error_eq() {
188        assert_eq!(ConvertError::AlphaNotOpaque, ConvertError::AlphaNotOpaque);
189        assert_ne!(ConvertError::AlphaNotOpaque, ConvertError::RgbToGray);
190    }
191
192    #[test]
193    fn error_debug() {
194        let e = ConvertError::AllocationFailed;
195        let s = format!("{e:?}");
196        assert!(s.contains("AllocationFailed"));
197    }
198
199    #[test]
200    fn error_clone() {
201        let e = ConvertError::BufferSize {
202            expected: 100,
203            actual: 50,
204        };
205        let e2 = e.clone();
206        assert_eq!(e, e2);
207    }
208}