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::{DecodeOptions, 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
17/// calls — no per-frame allocations after the first few frames.
18///
19/// # Example
20///
21/// ```rust,no_run
22/// use edgefirst_codec::{ImageDecoder, ImageLoad, DecodeOptions};
23/// use edgefirst_tensor::{Tensor, TensorTrait, TensorMemory, PixelFormat};
24///
25/// let mut decoder = ImageDecoder::new();
26/// let mut tensor = Tensor::<u8>::image(1920, 1080, PixelFormat::Rgb, Some(TensorMemory::Mem))
27///     .expect("alloc");
28///
29/// for _ in 0..100 {
30///     let frame = std::fs::read("frame.jpg").unwrap();
31///     let _info = tensor.load_image(&mut decoder, &frame, &DecodeOptions::default());
32/// }
33/// ```
34pub struct ImageDecoder {
35    /// Reusable JPEG decoder state (Huffman tables, MCU scratch buffers).
36    pub(crate) jpeg_state: crate::jpeg::JpegDecoderState,
37    /// Scratch buffer for PNG decode output.
38    pub(crate) scratch: Vec<u8>,
39    /// Reusable PNG EXIF rotation scratch (used by `apply_exif_u8` for the
40    /// 90°/270° paths). Kept across decodes so EXIF-rotated PNG workloads
41    /// don't re-allocate a multi-megabyte buffer per frame.
42    pub(crate) png_rot_scratch: Vec<u8>,
43    /// Buffer for `Read` → `&[u8]` conversion.
44    pub(crate) input_buffer: Vec<u8>,
45}
46
47impl ImageDecoder {
48    /// Create a new decoder with empty scratch buffers.
49    pub fn new() -> Self {
50        Self {
51            jpeg_state: crate::jpeg::JpegDecoderState::new(),
52            scratch: Vec::new(),
53            png_rot_scratch: Vec::new(),
54            input_buffer: Vec::new(),
55        }
56    }
57
58    /// Decode image data into a typed tensor.
59    ///
60    /// Detects the image format (JPEG or PNG) from magic bytes and decodes
61    /// into `dst`. The tensor must have sufficient capacity for the decoded
62    /// image dimensions.
63    ///
64    /// # Errors
65    ///
66    /// - [`CodecError::InsufficientCapacity`] if image dimensions exceed tensor
67    /// - [`CodecError::UnsupportedDtype`] if `T` is not a supported pixel type
68    /// - [`CodecError::InvalidData`] if the data is not a valid JPEG or PNG
69    pub fn decode_into<T: ImagePixel>(
70        &mut self,
71        data: &[u8],
72        dst: &mut Tensor<T>,
73        opts: &DecodeOptions,
74    ) -> crate::Result<ImageInfo> {
75        if is_jpeg(data) {
76            crate::jpeg::decode_jpeg_into(data, dst, opts, &mut self.jpeg_state)
77        } else if is_png(data) {
78            crate::png::decode_png_into(
79                data,
80                dst,
81                opts,
82                &mut self.scratch,
83                &mut self.png_rot_scratch,
84            )
85        } else {
86            Err(CodecError::InvalidData(
87                "unrecognized image format (expected JPEG or PNG magic bytes)".into(),
88            ))
89        }
90    }
91
92    /// Decode image data into a type-erased tensor.
93    ///
94    /// Dispatches to the appropriate typed decode path based on the tensor's
95    /// [`DType`](edgefirst_tensor::DType).
96    pub fn decode_into_dyn(
97        &mut self,
98        data: &[u8],
99        dst: &mut TensorDyn,
100        opts: &DecodeOptions,
101    ) -> crate::Result<ImageInfo> {
102        match dst {
103            TensorDyn::U8(t) => self.decode_into(data, t, opts),
104            TensorDyn::I8(t) => self.decode_into(data, t, opts),
105            TensorDyn::U16(t) => self.decode_into(data, t, opts),
106            TensorDyn::I16(t) => self.decode_into(data, t, opts),
107            TensorDyn::F32(t) => self.decode_into(data, t, opts),
108            other => Err(CodecError::UnsupportedDtype(other.dtype())),
109        }
110    }
111
112    /// Read all bytes from a `Read` source into the internal input buffer,
113    /// then decode. The input buffer is reused across calls — no per-call
114    /// heap copy of the encoded bytes.
115    pub fn decode_from_reader<T: ImagePixel, R: Read>(
116        &mut self,
117        mut reader: R,
118        dst: &mut Tensor<T>,
119        opts: &DecodeOptions,
120    ) -> crate::Result<ImageInfo> {
121        self.input_buffer.clear();
122        reader.read_to_end(&mut self.input_buffer)?;
123        // Split-borrow: decode_into_inner reads `input_buffer` while
124        // separately holding `&mut jpeg_state` and `&mut scratch`, which
125        // `decode_into(&mut self, &self.input_buffer)` cannot do without
126        // cloning the bytes.
127        decode_into_inner(
128            &mut self.jpeg_state,
129            &mut self.scratch,
130            &mut self.png_rot_scratch,
131            &self.input_buffer,
132            dst,
133            opts,
134        )
135    }
136
137    /// Read all bytes from a `Read` source into the internal input buffer,
138    /// then decode into a type-erased tensor.
139    pub fn decode_from_reader_dyn<R: Read>(
140        &mut self,
141        mut reader: R,
142        dst: &mut TensorDyn,
143        opts: &DecodeOptions,
144    ) -> crate::Result<ImageInfo> {
145        self.input_buffer.clear();
146        reader.read_to_end(&mut self.input_buffer)?;
147        decode_into_inner_dyn(
148            &mut self.jpeg_state,
149            &mut self.scratch,
150            &mut self.png_rot_scratch,
151            &self.input_buffer,
152            dst,
153            opts,
154        )
155    }
156}
157
158/// Internal decode entry point parameterised over disjoint `&mut` borrows
159/// so callers can read from a `&[u8]` borrowed from one field of
160/// [`ImageDecoder`] while mutably borrowing the others.
161pub(crate) fn decode_into_inner<T: ImagePixel>(
162    jpeg_state: &mut crate::jpeg::JpegDecoderState,
163    scratch: &mut Vec<u8>,
164    png_rot_scratch: &mut Vec<u8>,
165    data: &[u8],
166    dst: &mut Tensor<T>,
167    opts: &DecodeOptions,
168) -> crate::Result<ImageInfo> {
169    if is_jpeg(data) {
170        crate::jpeg::decode_jpeg_into(data, dst, opts, jpeg_state)
171    } else if is_png(data) {
172        crate::png::decode_png_into(data, dst, opts, scratch, png_rot_scratch)
173    } else {
174        Err(CodecError::InvalidData(
175            "unrecognized image format (expected JPEG or PNG magic bytes)".into(),
176        ))
177    }
178}
179
180pub(crate) fn decode_into_inner_dyn(
181    jpeg_state: &mut crate::jpeg::JpegDecoderState,
182    scratch: &mut Vec<u8>,
183    png_rot_scratch: &mut Vec<u8>,
184    data: &[u8],
185    dst: &mut TensorDyn,
186    opts: &DecodeOptions,
187) -> crate::Result<ImageInfo> {
188    match dst {
189        TensorDyn::U8(t) => decode_into_inner(jpeg_state, scratch, png_rot_scratch, data, t, opts),
190        TensorDyn::I8(t) => decode_into_inner(jpeg_state, scratch, png_rot_scratch, data, t, opts),
191        TensorDyn::U16(t) => decode_into_inner(jpeg_state, scratch, png_rot_scratch, data, t, opts),
192        TensorDyn::I16(t) => decode_into_inner(jpeg_state, scratch, png_rot_scratch, data, t, opts),
193        TensorDyn::F32(t) => decode_into_inner(jpeg_state, scratch, png_rot_scratch, data, t, opts),
194        other => Err(CodecError::UnsupportedDtype(other.dtype())),
195    }
196}
197
198impl Default for ImageDecoder {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204/// Parse image headers and return image dimensions/format without decoding pixels.
205///
206/// Detects the image format (JPEG or PNG) from magic bytes and returns an
207/// [`ImageInfo`] describing the post-decode layout. For images with an EXIF
208/// 90°/270° orientation tag and `opts.apply_exif == true`, `width` and
209/// `height` reflect the **post-rotation** dimensions — matching exactly what
210/// a subsequent [`ImageDecoder::decode_into`] call would write to the tensor.
211///
212/// This is the recommended entry point for one-shot decode flows that need
213/// to allocate a tensor sized to the image:
214///
215/// ```rust,no_run
216/// use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad, DecodeOptions};
217/// use edgefirst_tensor::{Tensor, TensorMemory, PixelFormat};
218///
219/// let data = std::fs::read("image.jpg").unwrap();
220/// let opts = DecodeOptions::default().with_format(PixelFormat::Rgba);
221/// let info = peek_info(&data, &opts).unwrap();
222/// let mut tensor = Tensor::<u8>::image(info.width, info.height, info.format,
223///                                       Some(TensorMemory::Mem)).unwrap();
224/// let mut decoder = ImageDecoder::new();
225/// tensor.load_image(&mut decoder, &data, &opts).unwrap();
226/// ```
227pub fn peek_info(data: &[u8], opts: &DecodeOptions) -> crate::Result<ImageInfo> {
228    if is_jpeg(data) {
229        crate::jpeg::peek_jpeg_info(data, opts)
230    } else if is_png(data) {
231        crate::png::peek_png_info(data, opts)
232    } else {
233        Err(CodecError::InvalidData(
234            "unrecognized image format (expected JPEG or PNG magic bytes)".into(),
235        ))
236    }
237}
238
239/// Check for JPEG magic bytes (FFD8FF).
240fn is_jpeg(data: &[u8]) -> bool {
241    data.len() >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF
242}
243
244/// Check for PNG magic bytes (89504E47).
245fn is_png(data: &[u8]) -> bool {
246    data.len() >= 4 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn magic_bytes_jpeg() {
255        assert!(is_jpeg(&[0xFF, 0xD8, 0xFF, 0xE0]));
256        assert!(!is_jpeg(&[0x89, 0x50, 0x4E, 0x47]));
257        assert!(!is_jpeg(&[]));
258        assert!(!is_jpeg(&[0xFF]));
259    }
260
261    #[test]
262    fn magic_bytes_png() {
263        assert!(is_png(&[0x89, 0x50, 0x4E, 0x47]));
264        assert!(!is_png(&[0xFF, 0xD8, 0xFF, 0xE0]));
265        assert!(!is_png(&[]));
266    }
267
268    #[test]
269    fn invalid_data() {
270        let mut decoder = ImageDecoder::new();
271        let mut tensor = Tensor::<u8>::image(
272            100,
273            100,
274            edgefirst_tensor::PixelFormat::Rgb,
275            Some(edgefirst_tensor::TensorMemory::Mem),
276        )
277        .unwrap();
278        let result = decoder.decode_into(b"not an image", &mut tensor, &DecodeOptions::default());
279        assert!(matches!(result, Err(CodecError::InvalidData(_))));
280    }
281}