Skip to main content

otf_pixels_codec_raw/
lib.rs

1//! Raw (uncompressed) pixel codec for `otf-pixels`.
2//!
3//! Raw is the degenerate format: there is no container, no header and no magic
4//! bytes, so the caller supplies width, height, pixel format and stride
5//! (SPEC §Formats). That makes it the simplest possible exercise of the
6//! [`Decoder`]/[`Encoder`] contracts, and the format M1's round-trip test uses
7//! at both ends of the pipeline.
8//!
9//! Because there is no header, "malformed" raw input means exactly one thing:
10//! the stream is shorter than the declared dimensions require. That is
11//! reported as a malformed-input error, never a panic.
12//!
13//! # Streaming
14//!
15//! Both directions are strictly row-at-a-time. [`RawDecoder`] reads exactly one
16//! row per [`Decoder::read_row`] call and never buffers the image;
17//! [`RawEncoder`] writes each row straight through to the sink. Raw is
18//! therefore a true constant-memory format in both directions (SPEC §Formats).
19//!
20//! ```
21//! use otf_pixels_codec_raw::{RawDecoder, RawFormat};
22//! use otf_pixels_core::{Decoder, ImageDescriptor, PixelFormat};
23//!
24//! # fn main() -> Result<(), otf_pixels_core::PixelsError> {
25//! let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8)?;
26//! let pixels: &[u8] = &[1, 2, 3, 4];
27//!
28//! let mut decoder = RawDecoder::new(RawFormat::packed(descriptor), pixels)?;
29//! let mut row = vec![0_u8; descriptor.row_bytes()];
30//! decoder.read_row(&mut row)?;
31//! assert_eq!(row, [1, 2]);
32//! # Ok(())
33//! # }
34//! ```
35
36use otf_pixels_core::{
37    Codec, DecodeCapability, Decoder, Encoder, Format, ImageDescriptor, Limits, PixelFormat,
38    PixelsError, Result, Sink, Source,
39};
40
41/// The layout of a raw pixel stream.
42///
43/// Raw carries no self-description, so this is the contract the caller must
44/// supply on both the decode and encode side.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct RawFormat {
48    /// Dimensions and pixel format of the stream.
49    pub descriptor: ImageDescriptor,
50    /// Bytes between the starts of consecutive rows in the *stream*.
51    ///
52    /// At least `descriptor.row_bytes()`. Any excess is row padding, which the
53    /// decoder reads and discards and the encoder writes as zeroes — this is
54    /// how raw dumps from graphics APIs with aligned rows are consumed.
55    pub stride: usize,
56}
57
58impl RawFormat {
59    /// A densely packed layout: stride equals one row of pixels.
60    #[must_use]
61    pub const fn packed(descriptor: ImageDescriptor) -> Self {
62        Self {
63            descriptor,
64            stride: descriptor.row_bytes(),
65        }
66    }
67
68    /// A layout with explicit row padding.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`PixelsError::InvalidArgument`] if `stride` is shorter than one
73    /// packed row.
74    pub fn with_stride(descriptor: ImageDescriptor, stride: usize) -> Result<Self> {
75        let row_bytes = descriptor.row_bytes();
76        if stride < row_bytes {
77            return Err(PixelsError::invalid_argument(
78                "stride",
79                format!("stride {stride} is shorter than a {row_bytes}-byte row"),
80            ));
81        }
82        Ok(Self { descriptor, stride })
83    }
84
85    /// Describe a raw stream from its dimensions, checked against `limits`.
86    ///
87    /// This is the raw equivalent of a header parse: dimensions are validated
88    /// **before** any pixel buffer is allocated (SPEC §Safety), so a caller
89    /// forwarding untrusted dimensions cannot provoke a huge allocation.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`PixelsError::LimitExceeded`] if the dimensions exceed
94    /// `limits`, or [`PixelsError::InvalidArgument`] if either is zero.
95    pub fn from_dimensions(
96        width: u32,
97        height: u32,
98        pixel: PixelFormat,
99        limits: &Limits,
100    ) -> Result<Self> {
101        Ok(Self::packed(ImageDescriptor::with_limits(
102            width, height, pixel, limits,
103        )?))
104    }
105
106    /// Bytes of padding after each row.
107    #[must_use]
108    pub const fn padding(&self) -> usize {
109        self.stride - self.descriptor.row_bytes()
110    }
111}
112
113/// Format sniffing for raw streams.
114///
115/// Raw has no magic bytes, so [`RawCodec::probe`] always returns `false`: raw
116/// can never be *detected*, only requested explicitly. Sniffing an unknown
117/// stream as raw would mean treating arbitrary bytes as pixels of arbitrary
118/// dimensions, which is not a decision the engine can make for the caller.
119#[derive(Debug, Clone, Copy, Default)]
120pub struct RawCodec;
121
122impl Codec for RawCodec {
123    fn format(&self) -> Format {
124        Format::Raw
125    }
126
127    fn magic_len(&self) -> usize {
128        0
129    }
130
131    fn probe(&self, _prefix: &[u8]) -> bool {
132        false
133    }
134}
135
136/// Decodes a raw pixel stream, one row per call.
137#[derive(Debug)]
138pub struct RawDecoder<S: Source> {
139    layout: RawFormat,
140    source: S,
141    rows_read: u32,
142    /// Scratch for reading and discarding row padding.
143    padding: Vec<u8>,
144}
145
146impl<S: Source> RawDecoder<S> {
147    /// Wrap `source` as a decoder for a stream laid out as `layout`.
148    ///
149    /// No pixel bytes are read here: construction is the (empty) header parse,
150    /// so laziness holds and the descriptor is available immediately.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`PixelsError::InvalidArgument`] if the layout's byte length
155    /// overflows `usize` on this platform.
156    pub fn new(layout: RawFormat, source: S) -> Result<Self> {
157        if layout.descriptor.byte_len().is_none() {
158            return Err(PixelsError::invalid_argument(
159                "descriptor",
160                "image byte length overflows this platform's address space",
161            ));
162        }
163        Ok(Self {
164            layout,
165            source,
166            rows_read: 0,
167            padding: vec![0; layout.padding()],
168        })
169    }
170
171    /// The layout this decoder was constructed with.
172    #[must_use]
173    pub const fn layout(&self) -> RawFormat {
174        self.layout
175    }
176
177    /// How many rows have been decoded so far.
178    #[must_use]
179    pub const fn rows_read(&self) -> u32 {
180        self.rows_read
181    }
182}
183
184impl<S: Source + std::fmt::Debug> Decoder for RawDecoder<S> {
185    fn descriptor(&self) -> ImageDescriptor {
186        self.layout.descriptor
187    }
188
189    fn capability(&self) -> DecodeCapability {
190        // Raw is a forward-only byte stream. It could support random access
191        // over a seekable source, but ADR-0005 makes forward-only the contract,
192        // so region decode stays unavailable and M2 pulls rows in order.
193        DecodeCapability::Sequential
194    }
195
196    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
197        let expected = self.layout.descriptor.row_bytes();
198        if out.len() != expected {
199            return Err(PixelsError::invalid_argument(
200                "out",
201                format!("row buffer is {} bytes, expected {expected}", out.len()),
202            ));
203        }
204        if self.rows_read >= self.layout.descriptor.height {
205            return Err(PixelsError::invalid_argument(
206                "out",
207                format!(
208                    "all {} rows have already been read",
209                    self.layout.descriptor.height
210                ),
211            ));
212        }
213        // A stream that ends mid-image surfaces as Malformed from read_exact.
214        self.source.read_exact(out)?;
215        if !self.padding.is_empty() {
216            self.source.read_exact(&mut self.padding)?;
217        }
218        self.rows_read += 1;
219        Ok(())
220    }
221}
222
223/// Encodes rows of pixels as a raw stream.
224#[derive(Debug, Default)]
225pub struct RawEncoder {
226    layout: Option<RawFormat>,
227    rows_written: u32,
228    /// Zero padding written after each row, when the layout calls for it.
229    padding: Vec<u8>,
230}
231
232impl RawEncoder {
233    /// A raw encoder that writes densely packed rows.
234    #[must_use]
235    pub const fn new() -> Self {
236        Self {
237            layout: None,
238            rows_written: 0,
239            padding: Vec::new(),
240        }
241    }
242
243    /// A raw encoder that writes rows padded to `layout`'s stride.
244    #[must_use]
245    pub fn with_layout(layout: RawFormat) -> Self {
246        Self {
247            layout: Some(layout),
248            rows_written: 0,
249            padding: vec![0; layout.padding()],
250        }
251    }
252
253    /// How many rows have been written so far.
254    #[must_use]
255    pub const fn rows_written(&self) -> u32 {
256        self.rows_written
257    }
258}
259
260impl Encoder for RawEncoder {
261    fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
262        if self.rows_written > 0 {
263            return Err(PixelsError::invalid_argument(
264                "descriptor",
265                "write_header called after rows were already written",
266            ));
267        }
268        match self.layout {
269            // A layout fixed at construction must match what the pipeline
270            // actually produced, or the stride padding would be wrong.
271            Some(layout) if layout.descriptor != *desc => {
272                return Err(PixelsError::invalid_argument(
273                    "descriptor",
274                    format!(
275                        "encoder was built for {} but the pipeline produced {desc}",
276                        layout.descriptor
277                    ),
278                ));
279            }
280            Some(_) => {}
281            None => {
282                let layout = RawFormat::packed(*desc);
283                self.padding = vec![0; layout.padding()];
284                self.layout = Some(layout);
285            }
286        }
287        // Raw has no header bytes; this call exists to fix the layout.
288        Ok(())
289    }
290
291    fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()> {
292        let Some(layout) = self.layout else {
293            return Err(PixelsError::invalid_argument(
294                "row",
295                "write_row called before write_header",
296            ));
297        };
298        let expected = layout.descriptor.row_bytes();
299        if row.len() != expected {
300            return Err(PixelsError::invalid_argument(
301                "row",
302                format!("row is {} bytes, expected {expected}", row.len()),
303            ));
304        }
305        if self.rows_written >= layout.descriptor.height {
306            return Err(PixelsError::invalid_argument(
307                "row",
308                format!(
309                    "all {} declared rows have already been written",
310                    layout.descriptor.height
311                ),
312            ));
313        }
314        sink.write_all(row)?;
315        if !self.padding.is_empty() {
316            sink.write_all(&self.padding)?;
317        }
318        self.rows_written += 1;
319        Ok(())
320    }
321
322    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
323        let Some(layout) = self.layout else {
324            return Err(PixelsError::malformed(
325                "raw",
326                "finish called before write_header",
327            ));
328        };
329        let declared = layout.descriptor.height;
330        if self.rows_written != declared {
331            // Partial output is never silently accepted.
332            return Err(PixelsError::malformed(
333                "raw",
334                format!("wrote {} of {declared} declared rows", self.rows_written),
335            ));
336        }
337        sink.flush()
338    }
339}
340
341#[cfg(test)]
342#[allow(
343    clippy::unwrap_used,
344    clippy::indexing_slicing,
345    clippy::panic,
346    reason = "tests operate on known-good values and assert shapes directly"
347)]
348mod tests {
349    use super::*;
350    use otf_pixels_core::{ErrorCode, Limit};
351
352    fn descriptor(width: u32, height: u32) -> ImageDescriptor {
353        ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap()
354    }
355
356    fn decode_all(layout: RawFormat, bytes: &[u8]) -> Result<Vec<u8>> {
357        let mut decoder = RawDecoder::new(layout, bytes)?;
358        let mut out = Vec::new();
359        let mut row = vec![0_u8; layout.descriptor.row_bytes()];
360        for _ in 0..layout.descriptor.height {
361            decoder.read_row(&mut row)?;
362            out.extend_from_slice(&row);
363        }
364        Ok(out)
365    }
366
367    #[test]
368    fn packed_streams_round_trip() {
369        let desc = descriptor(2, 2);
370        let pixels = [1, 2, 3, 4];
371        let decoded = decode_all(RawFormat::packed(desc), &pixels).unwrap();
372        assert_eq!(decoded, pixels);
373
374        let mut sink = Vec::new();
375        let mut encoder = RawEncoder::new();
376        encoder.write_header(&desc, &mut sink).unwrap();
377        encoder.write_row(&[1, 2], &mut sink).unwrap();
378        encoder.write_row(&[3, 4], &mut sink).unwrap();
379        encoder.finish(&mut sink).unwrap();
380        assert_eq!(sink, pixels);
381    }
382
383    #[test]
384    fn stride_padding_is_skipped_on_decode_and_zeroed_on_encode() {
385        let desc = descriptor(2, 2);
386        let layout = RawFormat::with_stride(desc, 4).unwrap();
387        assert_eq!(layout.padding(), 2);
388        // Rows are `1,2` and `3,4`, each followed by two padding bytes.
389        let stream = [1, 2, 9, 9, 3, 4, 9, 9];
390        assert_eq!(decode_all(layout, &stream).unwrap(), [1, 2, 3, 4]);
391
392        let mut sink = Vec::new();
393        let mut encoder = RawEncoder::with_layout(layout);
394        encoder.write_header(&desc, &mut sink).unwrap();
395        encoder.write_row(&[1, 2], &mut sink).unwrap();
396        encoder.write_row(&[3, 4], &mut sink).unwrap();
397        encoder.finish(&mut sink).unwrap();
398        assert_eq!(sink, [1, 2, 0, 0, 3, 4, 0, 0]);
399    }
400
401    #[test]
402    fn a_truncated_stream_is_malformed_not_a_panic() {
403        let layout = RawFormat::packed(descriptor(4, 4));
404        // Every truncation length, including mid-row and empty.
405        for len in 0..16 {
406            let stream = vec![7_u8; len];
407            let err = decode_all(layout, &stream).unwrap_err();
408            assert_eq!(err.code(), ErrorCode::Malformed, "truncated at {len} bytes");
409        }
410        assert!(decode_all(layout, &[7_u8; 16]).is_ok());
411    }
412
413    #[test]
414    fn a_stream_truncated_inside_padding_is_malformed() {
415        let layout = RawFormat::with_stride(descriptor(2, 2), 4).unwrap();
416        // Full first row, but the stream ends inside that row's padding.
417        let err = decode_all(layout, &[1, 2, 9]).unwrap_err();
418        assert_eq!(err.code(), ErrorCode::Malformed);
419    }
420
421    #[test]
422    fn reading_past_the_end_is_an_error_not_a_panic() {
423        let layout = RawFormat::packed(descriptor(2, 1));
424        let stream: &[u8] = &[1, 2];
425        let mut decoder = RawDecoder::new(layout, stream).unwrap();
426        let mut row = vec![0_u8; 2];
427        decoder.read_row(&mut row).unwrap();
428        let err = decoder.read_row(&mut row).unwrap_err();
429        assert_eq!(err.code(), ErrorCode::InvalidArgument);
430        assert_eq!(decoder.rows_read(), 1);
431    }
432
433    #[test]
434    fn a_wrong_sized_row_buffer_is_rejected() {
435        let layout = RawFormat::packed(descriptor(4, 1));
436        let stream: &[u8] = &[1, 2, 3, 4];
437        let mut decoder = RawDecoder::new(layout, stream).unwrap();
438        assert_eq!(
439            decoder.read_row(&mut [0; 3]).unwrap_err().code(),
440            ErrorCode::InvalidArgument
441        );
442        assert_eq!(
443            decoder.read_row(&mut [0; 5]).unwrap_err().code(),
444            ErrorCode::InvalidArgument
445        );
446    }
447
448    #[test]
449    fn hostile_dimensions_are_rejected_before_allocation() {
450        // A caller forwarding untrusted dimensions must not be able to provoke
451        // a huge allocation: the limit check happens at layout construction.
452        let err =
453            RawFormat::from_dimensions(u32::MAX, u32::MAX, PixelFormat::Rgba8, &Limits::default())
454                .unwrap_err();
455        assert_eq!(err.code(), ErrorCode::LimitExceeded);
456        match err {
457            PixelsError::LimitExceeded { limit, .. } => assert_eq!(limit, Limit::MaxPixels),
458            other => panic!("unexpected error: {other}"),
459        }
460    }
461
462    #[test]
463    fn a_short_stride_is_rejected() {
464        let err = RawFormat::with_stride(descriptor(4, 4), 3).unwrap_err();
465        assert_eq!(err.code(), ErrorCode::InvalidArgument);
466        assert_eq!(
467            RawFormat::with_stride(descriptor(4, 4), 4)
468                .unwrap()
469                .padding(),
470            0
471        );
472    }
473
474    #[test]
475    fn finishing_early_never_yields_partial_output() {
476        let desc = descriptor(2, 3);
477        let mut sink = Vec::new();
478        let mut encoder = RawEncoder::new();
479        encoder.write_header(&desc, &mut sink).unwrap();
480        encoder.write_row(&[1, 2], &mut sink).unwrap();
481        let err = encoder.finish(&mut sink).unwrap_err();
482        assert_eq!(err.code(), ErrorCode::Malformed);
483        assert!(err.to_string().contains("1 of 3"), "{err}");
484    }
485
486    #[test]
487    fn writing_more_rows_than_declared_is_rejected() {
488        let desc = descriptor(2, 1);
489        let mut sink = Vec::new();
490        let mut encoder = RawEncoder::new();
491        encoder.write_header(&desc, &mut sink).unwrap();
492        encoder.write_row(&[1, 2], &mut sink).unwrap();
493        let err = encoder.write_row(&[3, 4], &mut sink).unwrap_err();
494        assert_eq!(err.code(), ErrorCode::InvalidArgument);
495    }
496
497    #[test]
498    fn encoding_out_of_order_is_rejected() {
499        let mut sink = Vec::new();
500        let mut encoder = RawEncoder::new();
501        // write_row before write_header.
502        assert_eq!(
503            encoder.write_row(&[1, 2], &mut sink).unwrap_err().code(),
504            ErrorCode::InvalidArgument
505        );
506        // finish before write_header.
507        assert_eq!(
508            encoder.finish(&mut sink).unwrap_err().code(),
509            ErrorCode::Malformed
510        );
511    }
512
513    #[test]
514    fn a_fixed_layout_encoder_rejects_a_mismatched_pipeline() {
515        let layout = RawFormat::with_stride(descriptor(2, 2), 4).unwrap();
516        let mut encoder = RawEncoder::with_layout(layout);
517        let mut sink = Vec::new();
518        let err = encoder
519            .write_header(&descriptor(3, 2), &mut sink)
520            .unwrap_err();
521        assert_eq!(err.code(), ErrorCode::InvalidArgument);
522    }
523
524    #[test]
525    fn every_v1_pixel_format_round_trips() {
526        for &pixel in PixelFormat::ALL {
527            let desc = ImageDescriptor::new(3, 2, pixel).unwrap();
528            let bytes: Vec<u8> = (0..desc.byte_len().unwrap())
529                .map(|i| (i % 251) as u8)
530                .collect();
531            let decoded = decode_all(RawFormat::packed(desc), &bytes).unwrap();
532            assert_eq!(decoded, bytes, "{pixel} did not round-trip");
533        }
534    }
535
536    #[test]
537    fn raw_never_claims_a_stream_by_sniffing() {
538        let codec = RawCodec;
539        assert_eq!(codec.format(), Format::Raw);
540        assert_eq!(codec.magic_len(), 0);
541        assert!(!codec.probe(&[]));
542        assert!(!codec.probe(&[0x89, b'P', b'N', b'G']));
543    }
544
545    #[test]
546    fn decoding_reads_exactly_one_row_at_a_time() {
547        // Proves the decoder streams: after one read_row, only that row's bytes
548        // have been consumed from the source.
549        let desc = descriptor(2, 4);
550        let stream: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8];
551        let mut cursor = std::io::Cursor::new(stream);
552        let mut decoder = RawDecoder::new(RawFormat::packed(desc), &mut cursor).unwrap();
553        let mut row = vec![0_u8; 2];
554        decoder.read_row(&mut row).unwrap();
555        assert_eq!(row, [1, 2]);
556        drop(decoder);
557        assert_eq!(cursor.position(), 2, "only one row was consumed");
558    }
559}