Skip to main content

edgefirst_codec/
decoder.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`ImageDecoder`] — reusable decoder state for zero-allocation hot loops.
5
6use crate::error::CodecError;
7use crate::options::ImageInfo;
8use crate::pixel::ImagePixel;
9use edgefirst_tensor::{Tensor, TensorDyn};
10use std::io::Read;
11
12/// Reusable image decoder with internal scratch buffers.
13///
14/// Create one `ImageDecoder` at program initialisation and pass it to
15/// [`ImageLoad::load_image`](crate::ImageLoad::load_image) in the hot loop.
16/// The scratch buffers grow to the high-water mark and are reused across calls
17/// — no per-frame allocations after the first few frames.
18///
19/// The decoder always produces the source's native format and configures the
20/// destination tensor's dimensions + pixel format accordingly (JPEG →
21/// `Nv12`/`Grey`, PNG → `Rgb`/`Rgba`/`Grey`). It never colour-converts or
22/// rotates; use `ImageProcessor::convert()` for that.
23///
24/// # Example
25///
26/// ```rust,no_run
27/// use edgefirst_codec::{ImageDecoder, ImageLoad};
28/// use edgefirst_tensor::{Tensor, TensorTrait, TensorMemory, PixelFormat};
29///
30/// let mut decoder = ImageDecoder::new();
31/// // Allocate a buffer large enough for the biggest expected image.
32/// let mut tensor = Tensor::<u8>::image(1920, 1080, PixelFormat::Nv12, Some(TensorMemory::Mem))
33///     .expect("alloc");
34///
35/// for _ in 0..100 {
36///     let frame = std::fs::read("frame.jpg").unwrap();
37///     let _info = tensor.load_image(&mut decoder, &frame);
38/// }
39/// ```
40pub struct ImageDecoder {
41    /// Reusable JPEG decoder state (Huffman tables, MCU scratch buffers).
42    pub(crate) jpeg_state: crate::jpeg::JpegDecoderState,
43    /// Scratch buffer for PNG decode output.
44    pub(crate) scratch: Vec<u8>,
45    /// Buffer for `Read` → `&[u8]` conversion.
46    pub(crate) input_buffer: Vec<u8>,
47}
48
49impl ImageDecoder {
50    /// Create a new decoder with empty scratch buffers.
51    pub fn new() -> Self {
52        Self {
53            jpeg_state: crate::jpeg::JpegDecoderState::new(),
54            scratch: Vec::new(),
55            input_buffer: Vec::new(),
56        }
57    }
58
59    /// Decode image data into a typed tensor, configuring its dimensions and
60    /// pixel format to the decoded native format.
61    ///
62    /// Detects the image format (JPEG or PNG) from magic bytes.
63    ///
64    /// # Errors
65    ///
66    /// - [`CodecError::InsufficientCapacity`] if the image is larger than the
67    ///   tensor's allocation
68    /// - [`CodecError::UnsupportedDtype`] if `T` is not valid for the native
69    ///   format (JPEG NV12/GREY require `u8`)
70    /// - [`CodecError::InvalidData`] if the data is not a valid JPEG or PNG
71    pub fn decode_into<T: ImagePixel>(
72        &mut self,
73        data: &[u8],
74        dst: &mut Tensor<T>,
75    ) -> crate::Result<ImageInfo> {
76        if is_jpeg(data) {
77            crate::jpeg::decode_jpeg_into(data, dst, &mut self.jpeg_state)
78        } else if is_png(data) {
79            crate::png::decode_png_into(data, dst, &mut self.scratch)
80        } else {
81            Err(CodecError::InvalidData(
82                "unrecognized image format (expected JPEG or PNG magic bytes)".into(),
83            ))
84        }
85    }
86
87    /// Decode image data into a type-erased tensor.
88    pub fn decode_into_dyn(
89        &mut self,
90        data: &[u8],
91        dst: &mut TensorDyn,
92    ) -> crate::Result<ImageInfo> {
93        match dst {
94            TensorDyn::U8(t) => self.decode_into(data, t),
95            TensorDyn::I8(t) => self.decode_into(data, t),
96            TensorDyn::U16(t) => self.decode_into(data, t),
97            TensorDyn::I16(t) => self.decode_into(data, t),
98            TensorDyn::F32(t) => self.decode_into(data, t),
99            other => Err(CodecError::UnsupportedDtype(other.dtype())),
100        }
101    }
102
103    /// Read all bytes from a `Read` source into the internal input buffer, then
104    /// decode. The input buffer is reused across calls.
105    pub fn decode_from_reader<T: ImagePixel, R: Read>(
106        &mut self,
107        mut reader: R,
108        dst: &mut Tensor<T>,
109    ) -> crate::Result<ImageInfo> {
110        self.input_buffer.clear();
111        reader.read_to_end(&mut self.input_buffer)?;
112        decode_into_inner(
113            &mut self.jpeg_state,
114            &mut self.scratch,
115            &self.input_buffer,
116            dst,
117        )
118    }
119
120    /// Read all bytes from a `Read` source, then decode into a type-erased
121    /// tensor.
122    pub fn decode_from_reader_dyn<R: Read>(
123        &mut self,
124        mut reader: R,
125        dst: &mut TensorDyn,
126    ) -> crate::Result<ImageInfo> {
127        self.input_buffer.clear();
128        reader.read_to_end(&mut self.input_buffer)?;
129        decode_into_inner_dyn(
130            &mut self.jpeg_state,
131            &mut self.scratch,
132            &self.input_buffer,
133            dst,
134        )
135    }
136}
137
138/// Internal decode entry point parameterised over disjoint `&mut` borrows so
139/// callers can read from a `&[u8]` borrowed from one field of [`ImageDecoder`]
140/// while mutably borrowing the others.
141pub(crate) fn decode_into_inner<T: ImagePixel>(
142    jpeg_state: &mut crate::jpeg::JpegDecoderState,
143    scratch: &mut Vec<u8>,
144    data: &[u8],
145    dst: &mut Tensor<T>,
146) -> crate::Result<ImageInfo> {
147    if is_jpeg(data) {
148        crate::jpeg::decode_jpeg_into(data, dst, jpeg_state)
149    } else if is_png(data) {
150        crate::png::decode_png_into(data, dst, scratch)
151    } else {
152        Err(CodecError::InvalidData(
153            "unrecognized image format (expected JPEG or PNG magic bytes)".into(),
154        ))
155    }
156}
157
158pub(crate) fn decode_into_inner_dyn(
159    jpeg_state: &mut crate::jpeg::JpegDecoderState,
160    scratch: &mut Vec<u8>,
161    data: &[u8],
162    dst: &mut TensorDyn,
163) -> crate::Result<ImageInfo> {
164    match dst {
165        TensorDyn::U8(t) => decode_into_inner(jpeg_state, scratch, data, t),
166        TensorDyn::I8(t) => decode_into_inner(jpeg_state, scratch, data, t),
167        TensorDyn::U16(t) => decode_into_inner(jpeg_state, scratch, data, t),
168        TensorDyn::I16(t) => decode_into_inner(jpeg_state, scratch, data, t),
169        TensorDyn::F32(t) => decode_into_inner(jpeg_state, scratch, data, t),
170        other => Err(CodecError::UnsupportedDtype(other.dtype())),
171    }
172}
173
174impl Default for ImageDecoder {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180/// Parse image headers and return the native dimensions, format, and EXIF
181/// orientation without decoding pixels.
182///
183/// Recommended for one-shot flows that allocate a tensor sized to the image:
184///
185/// ```rust,no_run
186/// use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
187/// use edgefirst_tensor::{Tensor, TensorMemory};
188///
189/// let data = std::fs::read("image.jpg").unwrap();
190/// let info = peek_info(&data).unwrap();
191/// let mut tensor = Tensor::<u8>::image(info.width, info.height, info.format,
192///                                       Some(TensorMemory::Mem)).unwrap();
193/// let mut decoder = ImageDecoder::new();
194/// tensor.load_image(&mut decoder, &data).unwrap();
195/// ```
196pub fn peek_info(data: &[u8]) -> crate::Result<ImageInfo> {
197    if is_jpeg(data) {
198        crate::jpeg::peek_jpeg_info(data)
199    } else if is_png(data) {
200        crate::png::peek_png_info(data)
201    } else {
202        Err(CodecError::InvalidData(
203            "unrecognized image format (expected JPEG or PNG magic bytes)".into(),
204        ))
205    }
206}
207
208/// Check for JPEG magic bytes (FFD8FF).
209fn is_jpeg(data: &[u8]) -> bool {
210    data.len() >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF
211}
212
213/// Check for PNG magic bytes (89504E47).
214fn is_png(data: &[u8]) -> bool {
215    data.len() >= 4 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn magic_bytes_jpeg() {
224        assert!(is_jpeg(&[0xFF, 0xD8, 0xFF, 0xE0]));
225        assert!(!is_jpeg(&[0x89, 0x50, 0x4E, 0x47]));
226        assert!(!is_jpeg(&[]));
227        assert!(!is_jpeg(&[0xFF]));
228    }
229
230    #[test]
231    fn magic_bytes_png() {
232        assert!(is_png(&[0x89, 0x50, 0x4E, 0x47]));
233        assert!(!is_png(&[0xFF, 0xD8, 0xFF, 0xE0]));
234        assert!(!is_png(&[]));
235    }
236
237    #[test]
238    fn invalid_data() {
239        let mut decoder = ImageDecoder::new();
240        let mut tensor = Tensor::<u8>::image(
241            100,
242            100,
243            edgefirst_tensor::PixelFormat::Nv12,
244            Some(edgefirst_tensor::TensorMemory::Mem),
245        )
246        .unwrap();
247        let result = decoder.decode_into(b"not an image", &mut tensor);
248        assert!(matches!(result, Err(CodecError::InvalidData(_))));
249    }
250}