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//
8// `#[non_exhaustive]` (added 0.2.14): cargo-copter confirmed sealing it broke
9// zero of zpc's published reverse-dependents — nobody matches `ConvertError`
10// exhaustively — so it shipped as a tolerated 0.2.x break instead of waiting for
11// 0.3.0. That in turn let the `Buffer(BufferError)` variant land in the same
12// patch (adding a variant to an already-`#[non_exhaustive]` enum is not a
13// break). `Buffer` preserves the real `zenpixels::BufferError` cause
14// (`StrideTooSmall` / `InvalidDimensions` / …) instead of collapsing every
15// buffer-construction failure into `AllocationFailed` (an out-of-memory label);
16// the construction sites map via `map_err_at(ConvertError::from)`, which keeps
17// both the cause and the `At` location trace.
18#[derive(Debug, Clone, PartialEq)]
19#[non_exhaustive]
20pub enum ConvertError {
21    /// No supported format could be found for the source descriptor.
22    NoMatch { source: PixelDescriptor },
23    /// No conversion path exists between the two formats.
24    NoPath {
25        from: PixelDescriptor,
26        to: PixelDescriptor,
27    },
28    /// Source and destination buffer sizes don't match the expected dimensions.
29    BufferSize { expected: usize, actual: usize },
30    /// Width is zero or would overflow stride calculations.
31    InvalidWidth(u32),
32    /// The supported format list was empty.
33    EmptyFormatList,
34    /// Conversion between these transfer functions is not yet supported.
35    UnsupportedTransfer {
36        from: TransferFunction,
37        to: TransferFunction,
38    },
39    /// Alpha channel is not fully opaque and [`AlphaPolicy::DiscardIfOpaque`](crate::AlphaPolicy::DiscardIfOpaque) was set.
40    AlphaNotOpaque,
41    /// Depth reduction was requested but [`DepthPolicy::Forbid`](crate::DepthPolicy::Forbid) was set.
42    DepthReductionForbidden,
43    /// Alpha removal was requested but [`AlphaPolicy::Forbid`](crate::AlphaPolicy::Forbid) was set.
44    AlphaRemovalForbidden,
45    /// RGB-to-grayscale conversion requires explicit luma coefficients.
46    RgbToGray,
47    /// Buffer allocation failed.
48    AllocationFailed,
49    /// A pixel buffer or slice could not be constructed: carries the real
50    /// [`zenpixels::BufferError`] cause (`StrideTooSmall`, `InvalidDimensions`,
51    /// …) instead of collapsing it into [`AllocationFailed`](Self::AllocationFailed).
52    Buffer(zenpixels::BufferError),
53    /// CMS transform could not be built (invalid ICC profile, unsupported color space, etc.).
54    CmsError(alloc::string::String),
55    // Future variants (e.g. an HDR tone-mapping-required case; see HdrPolicy in
56    // output.rs and imazen/zenpixels#10) can now be added without a break, since
57    // `ConvertError` is `#[non_exhaustive]`.
58}
59
60impl fmt::Display for ConvertError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Self::NoMatch { source } => {
64                write!(
65                    f,
66                    "no supported format matches source {:?}/{:?}",
67                    source.channel_type(),
68                    source.layout()
69                )
70            }
71            Self::NoPath { from, to } => {
72                write!(
73                    f,
74                    "no conversion path from {:?}/{:?} to {:?}/{:?}",
75                    from.channel_type(),
76                    from.layout(),
77                    to.channel_type(),
78                    to.layout()
79                )?;
80                // A range crossing can otherwise print two identical-looking
81                // descriptors; name the actual blocker.
82                if from.signal_range != to.signal_range {
83                    write!(
84                        f,
85                        " (signal range {} -> {}: no narrow<->full conversion kernels exist; \
86                         relabeling without rescaling would corrupt pixel values)",
87                        from.signal_range, to.signal_range
88                    )?;
89                }
90                Ok(())
91            }
92            Self::BufferSize { expected, actual } => {
93                write!(
94                    f,
95                    "buffer size mismatch: expected {expected} bytes, got {actual}"
96                )
97            }
98            Self::InvalidWidth(w) => write!(f, "invalid width: {w}"),
99            Self::EmptyFormatList => write!(f, "supported format list is empty"),
100            Self::UnsupportedTransfer { from, to } => {
101                write!(f, "unsupported transfer conversion: {from:?} → {to:?}")
102            }
103            Self::AlphaNotOpaque => write!(f, "alpha channel is not fully opaque"),
104            Self::DepthReductionForbidden => write!(f, "depth reduction forbidden by policy"),
105            Self::AlphaRemovalForbidden => write!(f, "alpha removal forbidden by policy"),
106            Self::RgbToGray => {
107                write!(f, "RGB-to-grayscale requires explicit luma coefficients")
108            }
109            Self::AllocationFailed => write!(f, "buffer allocation failed"),
110            Self::Buffer(e) => write!(f, "buffer construction failed: {e}"),
111            Self::CmsError(msg) => write!(f, "CMS transform failed: {msg}"),
112        }
113    }
114}
115
116impl From<zenpixels::BufferError> for ConvertError {
117    /// Wrap a buffer-construction failure's real cause into
118    /// [`ConvertError::Buffer`]. Pair with `map_err_at` at the call sites so the
119    /// `At` location trace is preserved alongside the classified cause.
120    fn from(err: zenpixels::BufferError) -> Self {
121        Self::Buffer(err)
122    }
123}
124
125#[cfg(feature = "std")]
126impl std::error::Error for ConvertError {}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use alloc::format;
132
133    #[test]
134    fn display_no_match() {
135        let e = ConvertError::NoMatch {
136            source: PixelDescriptor::RGB8_SRGB,
137        };
138        let s = format!("{e}");
139        assert!(s.contains("no supported format"));
140        assert!(s.contains("U8"));
141        assert!(s.contains("Rgb"));
142    }
143
144    #[test]
145    fn display_no_path() {
146        let e = ConvertError::NoPath {
147            from: PixelDescriptor::RGB8_SRGB,
148            to: PixelDescriptor::GRAY8_SRGB,
149        };
150        let s = format!("{e}");
151        assert!(s.contains("no conversion path"));
152    }
153
154    #[test]
155    fn display_buffer_size() {
156        let e = ConvertError::BufferSize {
157            expected: 1024,
158            actual: 512,
159        };
160        let s = format!("{e}");
161        assert!(s.contains("1024"));
162        assert!(s.contains("512"));
163    }
164
165    #[test]
166    fn display_invalid_width() {
167        let e = ConvertError::InvalidWidth(0);
168        assert!(format!("{e}").contains("0"));
169    }
170
171    #[test]
172    fn display_empty_format_list() {
173        let s = format!("{}", ConvertError::EmptyFormatList);
174        assert!(s.contains("empty"));
175    }
176
177    #[test]
178    fn display_unsupported_transfer() {
179        let e = ConvertError::UnsupportedTransfer {
180            from: TransferFunction::Pq,
181            to: TransferFunction::Hlg,
182        };
183        let s = format!("{e}");
184        assert!(s.contains("Pq"));
185        assert!(s.contains("Hlg"));
186    }
187
188    #[test]
189    fn display_alpha_not_opaque() {
190        assert!(format!("{}", ConvertError::AlphaNotOpaque).contains("opaque"));
191    }
192
193    #[test]
194    fn display_depth_reduction_forbidden() {
195        assert!(format!("{}", ConvertError::DepthReductionForbidden).contains("forbidden"));
196    }
197
198    #[test]
199    fn display_alpha_removal_forbidden() {
200        assert!(format!("{}", ConvertError::AlphaRemovalForbidden).contains("forbidden"));
201    }
202
203    #[test]
204    fn display_rgb_to_gray() {
205        assert!(format!("{}", ConvertError::RgbToGray).contains("luma"));
206    }
207
208    #[test]
209    fn display_allocation_failed() {
210        assert!(format!("{}", ConvertError::AllocationFailed).contains("allocation"));
211    }
212
213    #[test]
214    fn display_cms_error() {
215        let e = ConvertError::CmsError(alloc::string::String::from("profile mismatch"));
216        let s = format!("{e}");
217        assert!(s.contains("CMS transform failed"));
218        assert!(s.contains("profile mismatch"));
219    }
220
221    #[test]
222    fn error_eq() {
223        assert_eq!(ConvertError::AlphaNotOpaque, ConvertError::AlphaNotOpaque);
224        assert_ne!(ConvertError::AlphaNotOpaque, ConvertError::RgbToGray);
225    }
226
227    #[test]
228    fn error_debug() {
229        let e = ConvertError::AllocationFailed;
230        let s = format!("{e:?}");
231        assert!(s.contains("AllocationFailed"));
232    }
233
234    #[test]
235    fn error_clone() {
236        let e = ConvertError::BufferSize {
237            expected: 100,
238            actual: 50,
239        };
240        let e2 = e.clone();
241        assert_eq!(e, e2);
242    }
243}