Skip to main content

viva_pfnc/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! Pixel Format Naming Convention helpers.
3
4use core::fmt;
5
6/// Enumeration of the pixel formats supported by the helper routines.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[non_exhaustive]
10#[repr(u32)]
11pub enum PixelFormat {
12    Mono8 = 0x0108_0001,
13    Mono10 = 0x0110_0003,
14    Mono12 = 0x0110_0005,
15    Mono14 = 0x0110_0025,
16    Mono16 = 0x0110_0007,
17    Confidence8 = 0x0108_00C6,
18    Coord3DC32f = 0x0120_00BF,
19    Coord3DAC16 = 0x0220_00BB,
20    Coord3DAC32f = 0x0240_00C2,
21    Coord3DABC32f = 0x0260_00C0,
22    BayerRG8 = 0x0108_0009,
23    BayerGB8 = 0x0108_000A,
24    BayerBG8 = 0x0108_000B,
25    BayerGR8 = 0x0108_0008,
26    BayerGR16 = 0x0110_000E,
27    BayerRG16 = 0x0110_000F,
28    BayerGB16 = 0x0110_0010,
29    BayerBG16 = 0x0110_0011,
30    RGB8Packed = 0x0218_0014,
31    BGR8Packed = 0x0218_0015,
32    /// Unknown PFNC code reported by the device.
33    Unknown(u32),
34}
35
36impl PixelFormat {
37    /// Convert a raw PFNC code into a [`PixelFormat`] enumeration.
38    pub const fn from_code(code: u32) -> PixelFormat {
39        match code {
40            0x0108_0001 => PixelFormat::Mono8,
41            0x0110_0003 => PixelFormat::Mono10,
42            0x0110_0005 => PixelFormat::Mono12,
43            0x0110_0025 => PixelFormat::Mono14,
44            0x0110_0007 => PixelFormat::Mono16,
45            0x0108_00C6 => PixelFormat::Confidence8,
46            0x0120_00BF => PixelFormat::Coord3DC32f,
47            0x0220_00BB => PixelFormat::Coord3DAC16,
48            0x0240_00C2 => PixelFormat::Coord3DAC32f,
49            0x0260_00C0 => PixelFormat::Coord3DABC32f,
50            0x0108_0009 => PixelFormat::BayerRG8,
51            0x0108_000A => PixelFormat::BayerGB8,
52            0x0108_000B => PixelFormat::BayerBG8,
53            0x0108_0008 => PixelFormat::BayerGR8,
54            0x0110_000E => PixelFormat::BayerGR16,
55            0x0110_000F => PixelFormat::BayerRG16,
56            0x0110_0010 => PixelFormat::BayerGB16,
57            0x0110_0011 => PixelFormat::BayerBG16,
58            0x0218_0014 => PixelFormat::RGB8Packed,
59            0x0218_0015 => PixelFormat::BGR8Packed,
60            other => PixelFormat::Unknown(other),
61        }
62    }
63
64    /// Return the PFNC code associated with the pixel format.
65    pub const fn code(self) -> u32 {
66        match self {
67            PixelFormat::Mono8 => 0x0108_0001,
68            PixelFormat::Mono10 => 0x0110_0003,
69            PixelFormat::Mono12 => 0x0110_0005,
70            PixelFormat::Mono14 => 0x0110_0025,
71            PixelFormat::Mono16 => 0x0110_0007,
72            PixelFormat::Confidence8 => 0x0108_00C6,
73            PixelFormat::Coord3DC32f => 0x0120_00BF,
74            PixelFormat::Coord3DAC16 => 0x0220_00BB,
75            PixelFormat::Coord3DAC32f => 0x0240_00C2,
76            PixelFormat::Coord3DABC32f => 0x0260_00C0,
77            PixelFormat::BayerRG8 => 0x0108_0009,
78            PixelFormat::BayerGB8 => 0x0108_000A,
79            PixelFormat::BayerBG8 => 0x0108_000B,
80            PixelFormat::BayerGR8 => 0x0108_0008,
81            PixelFormat::BayerGR16 => 0x0110_000E,
82            PixelFormat::BayerRG16 => 0x0110_000F,
83            PixelFormat::BayerGB16 => 0x0110_0010,
84            PixelFormat::BayerBG16 => 0x0110_0011,
85            PixelFormat::RGB8Packed => 0x0218_0014,
86            PixelFormat::BGR8Packed => 0x0218_0015,
87            PixelFormat::Unknown(code) => code,
88        }
89    }
90
91    /// Number of bytes used to encode a single pixel.
92    ///
93    /// For a format this enumeration does not name, the answer is derived from
94    /// bits 23-16 of the PFNC code, which carry the pixel's bit depth. `None`
95    /// means the pixel genuinely has no whole-byte size — a packed format — not
96    /// that we failed to look it up.
97    pub const fn bytes_per_pixel(self) -> Option<usize> {
98        match self {
99            PixelFormat::Mono8 => Some(1),
100            PixelFormat::Mono10 | PixelFormat::Mono12 | PixelFormat::Mono14 => Some(2),
101            PixelFormat::Mono16 => Some(2),
102            PixelFormat::Confidence8 => Some(1),
103            PixelFormat::Coord3DC32f => Some(4),
104            PixelFormat::Coord3DAC16 => Some(4),
105            PixelFormat::Coord3DAC32f => Some(8),
106            PixelFormat::Coord3DABC32f => Some(12),
107            PixelFormat::RGB8Packed | PixelFormat::BGR8Packed => Some(3),
108            PixelFormat::BayerRG8
109            | PixelFormat::BayerGB8
110            | PixelFormat::BayerBG8
111            | PixelFormat::BayerGR8 => Some(1),
112            PixelFormat::BayerGR16
113            | PixelFormat::BayerRG16
114            | PixelFormat::BayerGB16
115            | PixelFormat::BayerBG16 => Some(2),
116            PixelFormat::Unknown(code) => PixelFormat::bytes_from_code(code),
117        }
118    }
119
120    /// Bytes per pixel read straight out of a PFNC code.
121    ///
122    /// Bits 23-16 of every PFNC value are the pixel's bit depth, so a format
123    /// this enumeration has no variant for still has a usable size. That is the
124    /// difference between a receiver sizing a `Coord3D_ABC32f` frame at twelve
125    /// bytes per pixel and sizing it at one: callers overwhelmingly write
126    /// `bytes_per_pixel().unwrap_or(1)`, and a `None` there is not a neutral
127    /// answer, it is a wrong one.
128    ///
129    /// Returns `None` when the depth is not a whole number of bytes, because
130    /// then no `usize` is correct. **Packed formats are the reason this check
131    /// exists**: `Mono12Packed`, `Mono10Packed`, `YUV411Packed`,
132    /// `BayerGR12Packed` and `BayerRG12Packed` all declare 12 bits, and eleven
133    /// of the 37 vendor-corpus documents offer at least one of them. Rounding
134    /// 12 bits up to 2 bytes overstates a frame by a third, which a length
135    /// check downstream reads as a *short payload* — a confidently wrong size
136    /// is worse than an absent one.
137    const fn bytes_from_code(code: u32) -> Option<usize> {
138        let bits = (code >> 16) & 0xFF;
139        if bits == 0 || !bits.is_multiple_of(8) {
140            return None;
141        }
142        Some((bits / 8) as usize)
143    }
144
145    /// Convert a PFNC name string to a [`PixelFormat`].
146    ///
147    /// Returns `PixelFormat::Unknown(0)` for unrecognised names.
148    pub fn from_name(name: &str) -> PixelFormat {
149        match name {
150            "Mono8" => PixelFormat::Mono8,
151            "Mono10" => PixelFormat::Mono10,
152            "Mono12" => PixelFormat::Mono12,
153            "Mono14" => PixelFormat::Mono14,
154            "Mono16" => PixelFormat::Mono16,
155            "Confidence8" => PixelFormat::Confidence8,
156            "Coord3D_C32f" => PixelFormat::Coord3DC32f,
157            "Coord3D_AC16" => PixelFormat::Coord3DAC16,
158            "Coord3D_AC32f" => PixelFormat::Coord3DAC32f,
159            "Coord3D_ABC32f" => PixelFormat::Coord3DABC32f,
160            "BayerRG8" => PixelFormat::BayerRG8,
161            "BayerGB8" => PixelFormat::BayerGB8,
162            "BayerBG8" => PixelFormat::BayerBG8,
163            "BayerGR8" => PixelFormat::BayerGR8,
164            "BayerGR16" => PixelFormat::BayerGR16,
165            "BayerRG16" => PixelFormat::BayerRG16,
166            "BayerGB16" => PixelFormat::BayerGB16,
167            "BayerBG16" => PixelFormat::BayerBG16,
168            "RGB8Packed" | "RGB8" => PixelFormat::RGB8Packed,
169            "BGR8Packed" | "BGR8" => PixelFormat::BGR8Packed,
170            _ => PixelFormat::Unknown(0),
171        }
172    }
173
174    /// Whether the pixel format represents a Bayer mosaic.
175    pub const fn is_bayer(self) -> bool {
176        matches!(
177            self,
178            PixelFormat::BayerRG8
179                | PixelFormat::BayerGB8
180                | PixelFormat::BayerBG8
181                | PixelFormat::BayerGR8
182                | PixelFormat::BayerGR16
183                | PixelFormat::BayerRG16
184                | PixelFormat::BayerGB16
185                | PixelFormat::BayerBG16
186        )
187    }
188
189    /// Return the Color Filter Array pattern and canonical offsets.
190    ///
191    /// The tuple encodes `(pattern, x_offset, y_offset)` where the offsets
192    /// describe how the sensor mosaic aligns to the canonical `"RGGB"`
193    /// ordering.
194    pub const fn cfa_pattern(self) -> Option<(&'static str, u8, u8)> {
195        match self {
196            PixelFormat::BayerRG8 => Some(("RGGB", 0, 0)),
197            PixelFormat::BayerGR8 => Some(("RGGB", 1, 0)),
198            PixelFormat::BayerGB8 => Some(("RGGB", 0, 1)),
199            PixelFormat::BayerBG8 => Some(("RGGB", 1, 1)),
200            _ => None,
201        }
202    }
203}
204
205impl fmt::Display for PixelFormat {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        match self {
208            PixelFormat::Mono8 => f.write_str("Mono8"),
209            PixelFormat::Mono10 => f.write_str("Mono10"),
210            PixelFormat::Mono12 => f.write_str("Mono12"),
211            PixelFormat::Mono14 => f.write_str("Mono14"),
212            PixelFormat::Mono16 => f.write_str("Mono16"),
213            PixelFormat::Confidence8 => f.write_str("Confidence8"),
214            PixelFormat::Coord3DC32f => f.write_str("Coord3D_C32f"),
215            PixelFormat::Coord3DAC16 => f.write_str("Coord3D_AC16"),
216            PixelFormat::Coord3DAC32f => f.write_str("Coord3D_AC32f"),
217            PixelFormat::Coord3DABC32f => f.write_str("Coord3D_ABC32f"),
218            PixelFormat::BayerRG8 => f.write_str("BayerRG8"),
219            PixelFormat::BayerGB8 => f.write_str("BayerGB8"),
220            PixelFormat::BayerBG8 => f.write_str("BayerBG8"),
221            PixelFormat::BayerGR8 => f.write_str("BayerGR8"),
222            PixelFormat::BayerGR16 => f.write_str("BayerGR16"),
223            PixelFormat::BayerRG16 => f.write_str("BayerRG16"),
224            PixelFormat::BayerGB16 => f.write_str("BayerGB16"),
225            PixelFormat::BayerBG16 => f.write_str("BayerBG16"),
226            PixelFormat::RGB8Packed => f.write_str("RGB8Packed"),
227            PixelFormat::BGR8Packed => f.write_str("BGR8Packed"),
228            PixelFormat::Unknown(code) => write!(f, "Unknown(0x{code:08X})"),
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::PixelFormat;
236
237    #[test]
238    fn roundtrip_known_codes() {
239        let formats = [
240            PixelFormat::Mono8,
241            PixelFormat::Mono10,
242            PixelFormat::Mono12,
243            PixelFormat::Mono14,
244            PixelFormat::Mono16,
245            PixelFormat::Confidence8,
246            PixelFormat::Coord3DC32f,
247            PixelFormat::Coord3DAC16,
248            PixelFormat::Coord3DAC32f,
249            PixelFormat::Coord3DABC32f,
250            PixelFormat::BayerRG8,
251            PixelFormat::BayerGB8,
252            PixelFormat::BayerBG8,
253            PixelFormat::BayerGR8,
254            PixelFormat::BayerGR16,
255            PixelFormat::BayerRG16,
256            PixelFormat::BayerGB16,
257            PixelFormat::BayerBG16,
258            PixelFormat::RGB8Packed,
259            PixelFormat::BGR8Packed,
260        ];
261
262        for fmt in formats {
263            let code = fmt.code();
264            assert_eq!(PixelFormat::from_code(code), fmt);
265        }
266    }
267
268    #[test]
269    fn unknown_code_roundtrip() {
270        let code = 0xDEAD_BEEF;
271        let fmt = PixelFormat::from_code(code);
272        assert!(matches!(fmt, PixelFormat::Unknown(value) if value == code));
273        assert_eq!(fmt.code(), code);
274    }
275
276    #[test]
277    fn bytes_per_pixel_matches_expectations() {
278        assert_eq!(PixelFormat::Mono8.bytes_per_pixel(), Some(1));
279        assert_eq!(PixelFormat::Mono10.bytes_per_pixel(), Some(2));
280        assert_eq!(PixelFormat::Mono16.bytes_per_pixel(), Some(2));
281        assert_eq!(PixelFormat::Confidence8.bytes_per_pixel(), Some(1));
282        assert_eq!(PixelFormat::Coord3DC32f.bytes_per_pixel(), Some(4));
283        assert_eq!(PixelFormat::Coord3DAC16.bytes_per_pixel(), Some(4));
284        assert_eq!(PixelFormat::Coord3DAC32f.bytes_per_pixel(), Some(8));
285        assert_eq!(PixelFormat::Coord3DABC32f.bytes_per_pixel(), Some(12));
286        assert_eq!(PixelFormat::RGB8Packed.bytes_per_pixel(), Some(3));
287        assert_eq!(PixelFormat::BayerRG8.bytes_per_pixel(), Some(1));
288        assert_eq!(PixelFormat::BayerRG16.bytes_per_pixel(), Some(2));
289        assert_eq!(PixelFormat::BayerGR16.bytes_per_pixel(), Some(2));
290        assert_eq!(PixelFormat::Unknown(0).bytes_per_pixel(), None);
291    }
292
293    /// Every named format must agree with the size encoded in its own code,
294    /// which is what makes the `Unknown` derivation trustworthy.
295    #[test]
296    fn named_formats_agree_with_their_own_pfnc_code() {
297        let formats = [
298            PixelFormat::Mono8,
299            PixelFormat::Mono10,
300            PixelFormat::Mono12,
301            PixelFormat::Mono14,
302            PixelFormat::Mono16,
303            PixelFormat::Confidence8,
304            PixelFormat::Coord3DC32f,
305            PixelFormat::Coord3DAC16,
306            PixelFormat::Coord3DAC32f,
307            PixelFormat::Coord3DABC32f,
308            PixelFormat::BayerRG8,
309            PixelFormat::BayerGB8,
310            PixelFormat::BayerBG8,
311            PixelFormat::BayerGR8,
312            PixelFormat::BayerGR16,
313            PixelFormat::BayerRG16,
314            PixelFormat::BayerGB16,
315            PixelFormat::BayerBG16,
316            PixelFormat::RGB8Packed,
317            PixelFormat::BGR8Packed,
318        ];
319
320        for fmt in formats {
321            // Keep the size nibble, replace the unique ID with one no format
322            // uses, so `from_code` yields `Unknown` and the size has to be
323            // derived rather than looked up.
324            let disguised = PixelFormat::from_code((fmt.code() & 0xFFFF_0000) | 0xFFFF);
325            assert!(
326                matches!(disguised, PixelFormat::Unknown(_)),
327                "{fmt}: the disguise resolved to a named format"
328            );
329            assert_eq!(
330                fmt.bytes_per_pixel(),
331                disguised.bytes_per_pixel(),
332                "{fmt}: the hardcoded size disagrees with bits 23-16 of its own code"
333            );
334        }
335    }
336
337    /// A format we have no variant for still gets a size, so callers stop
338    /// falling back to one byte per pixel.
339    #[test]
340    fn unknown_formats_are_sized_from_their_code() {
341        // Real PFNC codes we deliberately do not enumerate.
342        // RGBa8: 32 bits. RGB16: 48 bits. Coord3D_ABC32: 96 bits.
343        assert_eq!(
344            PixelFormat::from_code(0x0220_0016).bytes_per_pixel(),
345            Some(4)
346        );
347        assert_eq!(
348            PixelFormat::from_code(0x0230_0033).bytes_per_pixel(),
349            Some(6)
350        );
351        assert_eq!(
352            PixelFormat::from_code(0x0260_00C1).bytes_per_pixel(),
353            Some(12)
354        );
355    }
356
357    /// Packed formats have a fractional byte size, and no `usize` is right.
358    ///
359    /// These five all appear in the vendor XML corpus — `Mono12Packed` in
360    /// eleven of its 37 documents. Rounding 12 bits up to 2 bytes would
361    /// overstate a frame by a third and make a length check downstream reject
362    /// it as short.
363    #[test]
364    fn packed_formats_report_no_whole_byte_size() {
365        for (name, code) in [
366            ("Mono10Packed", 0x010C_0004_u32),
367            ("Mono12Packed", 0x010C_0006),
368            ("YUV411Packed", 0x020C_001E),
369            ("BayerGR12Packed", 0x010C_002C),
370            ("BayerRG12Packed", 0x010C_002D),
371        ] {
372            assert_eq!(
373                PixelFormat::from_code(code).bytes_per_pixel(),
374                None,
375                "{name} declares 12 bits per pixel and must not be rounded"
376            );
377        }
378    }
379
380    #[test]
381    fn scancontrol_names_resolve_to_known_formats() {
382        let formats = [
383            ("Mono10", PixelFormat::Mono10),
384            ("Confidence8", PixelFormat::Confidence8),
385            ("Coord3D_C32f", PixelFormat::Coord3DC32f),
386            ("Coord3D_AC16", PixelFormat::Coord3DAC16),
387            ("Coord3D_AC32f", PixelFormat::Coord3DAC32f),
388            ("Coord3D_ABC32f", PixelFormat::Coord3DABC32f),
389        ];
390
391        for (name, format) in formats {
392            assert_eq!(PixelFormat::from_name(name), format);
393            assert_eq!(format.to_string(), name);
394        }
395    }
396
397    #[test]
398    fn cfa_offsets_align_to_rggb() {
399        assert_eq!(PixelFormat::BayerRG8.cfa_pattern(), Some(("RGGB", 0, 0)));
400        assert_eq!(PixelFormat::BayerGR8.cfa_pattern(), Some(("RGGB", 1, 0)));
401        assert_eq!(PixelFormat::BayerGB8.cfa_pattern(), Some(("RGGB", 0, 1)));
402        assert_eq!(PixelFormat::BayerBG8.cfa_pattern(), Some(("RGGB", 1, 1)));
403        assert_eq!(PixelFormat::Mono8.cfa_pattern(), None);
404    }
405}