Skip to main content

oximedia_codec/jpegxl/
decoder.rs

1//! JPEG-XL decoder implementation.
2//!
3//! Decodes JPEG-XL codestreams (bare and container format) into raw pixel data.
4//! Currently supports lossless Modular mode for 8-bit and 16-bit images in
5//! grayscale, RGB, and RGBA color spaces.
6
7use std::io::{self, Read};
8
9use super::bitreader::BitReader;
10use super::modular::{ModularDecoder, ModularTransform};
11use super::types::{
12    JxlAnimation, JxlColorSpace, JxlFrame, JxlHeader, JXL_CODESTREAM_SIGNATURE,
13    JXL_CONTAINER_SIGNATURE,
14};
15use crate::container::isobmff::BoxIter;
16use crate::error::{CodecError, CodecResult};
17
18/// Internal reader type: peeked bytes re-prepended to the original reader.
19/// The alias avoids `clippy::type_complexity` on struct fields.
20type PeekedReader<R> = io::Chain<io::Cursor<Vec<u8>>, R>;
21
22/// Decoded JPEG-XL image.
23#[derive(Clone, Debug)]
24pub struct DecodedImage {
25    /// Image width in pixels.
26    pub width: u32,
27    /// Image height in pixels.
28    pub height: u32,
29    /// Number of channels (1=gray, 3=RGB, 4=RGBA).
30    pub channels: u8,
31    /// Bits per sample (8 or 16).
32    pub bit_depth: u8,
33    /// Interleaved pixel data.
34    /// For 8-bit: one byte per sample.
35    /// For 16-bit: two bytes per sample (little-endian).
36    pub data: Vec<u8>,
37    /// Color space of the decoded image.
38    pub color_space: JxlColorSpace,
39}
40
41impl DecodedImage {
42    /// Total number of samples in the image.
43    pub fn sample_count(&self) -> usize {
44        self.width as usize * self.height as usize * self.channels as usize
45    }
46
47    /// Total size of pixel data in bytes.
48    pub fn data_size(&self) -> usize {
49        let bytes_per_sample = if self.bit_depth > 8 { 2 } else { 1 };
50        self.sample_count() * bytes_per_sample
51    }
52}
53
54/// JPEG-XL decoder.
55///
56/// Decodes JPEG-XL files (both bare codestream and ISOBMFF container format)
57/// into raw pixel data.
58pub struct JxlDecoder;
59
60impl JxlDecoder {
61    /// Create a new JPEG-XL decoder.
62    pub fn new() -> Self {
63        Self
64    }
65
66    /// Check if the data starts with a valid JXL signature.
67    ///
68    /// Returns `true` for both bare codestream (0xFF 0x0A) and
69    /// container format signatures.
70    pub fn is_jxl(data: &[u8]) -> bool {
71        Self::is_codestream(data) || Self::is_container(data)
72    }
73
74    /// Check for bare codestream signature.
75    pub fn is_codestream(data: &[u8]) -> bool {
76        data.len() >= 2
77            && data[0] == JXL_CODESTREAM_SIGNATURE[0]
78            && data[1] == JXL_CODESTREAM_SIGNATURE[1]
79    }
80
81    /// Check for ISOBMFF container signature.
82    pub fn is_container(data: &[u8]) -> bool {
83        data.len() >= 12 && data[..12] == JXL_CONTAINER_SIGNATURE
84    }
85
86    /// Decode a JPEG-XL file from bytes.
87    ///
88    /// # Errors
89    ///
90    /// Returns error if:
91    /// - The data does not have a valid JXL signature
92    /// - The header is malformed
93    /// - The image data is corrupt
94    /// - Unsupported features are encountered
95    pub fn decode(&self, data: &[u8]) -> CodecResult<DecodedImage> {
96        let codestream = self.extract_codestream(data)?;
97        let mut reader = BitReader::new(&codestream);
98
99        // Skip signature (2 bytes = 16 bits)
100        let _ = reader.read_bits(16)?;
101
102        // Parse size header
103        let (width, height) = self.parse_size_header(&mut reader)?;
104
105        // Parse image metadata
106        let header = self.parse_image_metadata(&mut reader, width, height)?;
107        header.validate()?;
108
109        // Decode using modular mode
110        let channels_data = self.decode_modular(&mut reader, &header)?;
111
112        // Convert channel data to interleaved byte output
113        let pixel_data = self.channels_to_interleaved(&channels_data, &header)?;
114
115        Ok(DecodedImage {
116            width: header.width,
117            height: header.height,
118            channels: header.num_channels,
119            bit_depth: header.bits_per_sample,
120            data: pixel_data,
121            color_space: header.color_space,
122        })
123    }
124
125    /// Read only the header from a JPEG-XL file without fully decoding.
126    ///
127    /// # Errors
128    ///
129    /// Returns error if the signature or header is invalid.
130    pub fn read_header(&self, data: &[u8]) -> CodecResult<JxlHeader> {
131        let codestream = self.extract_codestream(data)?;
132        let mut reader = BitReader::new(&codestream);
133
134        // Skip signature
135        let _ = reader.read_bits(16)?;
136
137        let (width, height) = self.parse_size_header(&mut reader)?;
138        let header = self.parse_image_metadata(&mut reader, width, height)?;
139        header.validate()?;
140        Ok(header)
141    }
142
143    /// Extract the bare codestream from either format.
144    ///
145    /// If the data is a bare codestream, returns it as-is.
146    /// If it is a container, extracts the jxlc box contents.
147    fn extract_codestream<'a>(&self, data: &'a [u8]) -> CodecResult<&'a [u8]> {
148        if Self::is_codestream(data) {
149            return Ok(data);
150        }
151        if Self::is_container(data) {
152            // Parse ISOBMFF boxes to find jxlc (codestream) box
153            return self.find_jxlc_box(data);
154        }
155        Err(CodecError::InvalidBitstream(
156            "Not a valid JPEG-XL file: invalid signature".into(),
157        ))
158    }
159
160    /// Find the jxlc box in an ISOBMFF container.
161    fn find_jxlc_box<'a>(&self, data: &'a [u8]) -> CodecResult<&'a [u8]> {
162        let mut offset = 0;
163        while offset + 8 <= data.len() {
164            let box_size = u32::from_be_bytes([
165                data[offset],
166                data[offset + 1],
167                data[offset + 2],
168                data[offset + 3],
169            ]) as usize;
170
171            let box_type = &data[offset + 4..offset + 8];
172
173            if box_size < 8 {
174                break;
175            }
176
177            if box_type == b"jxlc" {
178                let content_start = offset + 8;
179                let content_end = offset + box_size;
180                if content_end <= data.len() {
181                    return Ok(&data[content_start..content_end]);
182                }
183                return Err(CodecError::InvalidBitstream(
184                    "jxlc box extends past end of file".into(),
185                ));
186            }
187
188            offset += box_size;
189        }
190
191        Err(CodecError::InvalidBitstream(
192            "No jxlc (codestream) box found in container".into(),
193        ))
194    }
195
196    /// Parse the JPEG-XL SizeHeader.
197    ///
198    /// The size header uses a compact variable-length encoding:
199    /// - 1 bit: small flag
200    /// - If small: 5 bits height_div8, 5 bits width_div8 (sizes * 8)
201    /// - If not small: read height and width using U32 encoding
202    fn parse_size_header(&self, reader: &mut BitReader) -> CodecResult<(u32, u32)> {
203        let small = reader.read_bool()?;
204
205        if small {
206            let height_div8 = reader.read_bits(5)? + 1;
207            let width_div8 = reader.read_bits(5)?;
208            // Width uses ratio based on height if not specified
209            let width_div8 = if width_div8 == 0 {
210                height_div8
211            } else {
212                width_div8
213            };
214            Ok((width_div8 * 8, height_div8 * 8))
215        } else {
216            // Full U32 encoding for height and width
217            let height = self.read_size_u32(reader)?;
218            let width = self.read_size_u32(reader)?;
219            Ok((width, height))
220        }
221    }
222
223    /// Read a size value using JPEG-XL's SizeHeader U32 encoding.
224    ///
225    /// Distribution: d0=1, d1=1+read(9), d2=1+read(13), d3=1+read(18)
226    fn read_size_u32(&self, reader: &mut BitReader) -> CodecResult<u32> {
227        let selector = reader.read_bits(2)?;
228        match selector {
229            0 => Ok(1),
230            1 => {
231                let extra = reader.read_bits(9)?;
232                Ok(1 + extra)
233            }
234            2 => {
235                let extra = reader.read_bits(13)?;
236                Ok(1 + extra)
237            }
238            3 => {
239                let extra = reader.read_bits(18)?;
240                Ok(1 + extra)
241            }
242            _ => Err(CodecError::InvalidBitstream("Invalid size selector".into())),
243        }
244    }
245
246    /// Parse the ImageMetadata section.
247    ///
248    /// This is a simplified parser that reads the essential fields:
249    /// - all_default flag
250    /// - bit_depth
251    /// - color space
252    /// - alpha flag
253    /// - animation header (if present)
254    fn parse_image_metadata(
255        &self,
256        reader: &mut BitReader,
257        width: u32,
258        height: u32,
259    ) -> CodecResult<JxlHeader> {
260        // all_default flag: if true, use default 8-bit sRGB, no animation
261        let all_default = reader.read_bool()?;
262
263        if all_default {
264            return Ok(JxlHeader {
265                width,
266                height,
267                bits_per_sample: 8,
268                num_channels: 3,
269                is_float: false,
270                has_alpha: false,
271                color_space: JxlColorSpace::Srgb,
272                orientation: 1,
273                animation: None,
274            });
275        }
276
277        // Extra fields present
278        let has_extra_fields = reader.read_bool()?;
279        let orientation = if has_extra_fields {
280            reader.read_bits(3)? as u8 + 1
281        } else {
282            1
283        };
284
285        // Bit depth
286        let float_flag = reader.read_bool()?;
287        let bits_per_sample = if float_flag {
288            // Float samples: read exponent bits
289            let _exp_bits = reader.read_bits(4)?;
290            let mantissa_bits = reader.read_bits(4)? + 1;
291            (mantissa_bits + 1) as u8 // approximate total bits
292        } else {
293            let depth_selector = reader.read_bits(2)?;
294            match depth_selector {
295                0 => 8,
296                1 => 10,
297                2 => 12,
298                3 => {
299                    let custom = reader.read_bits(6)?;
300                    (custom + 1) as u8
301                }
302                _ => 8,
303            }
304        };
305
306        // Color space
307        let color_space_selector = reader.read_bits(2)?;
308        let color_space = match color_space_selector {
309            0 => JxlColorSpace::Srgb,
310            1 => JxlColorSpace::LinearSrgb,
311            2 => JxlColorSpace::Gray,
312            3 => JxlColorSpace::Xyb,
313            _ => JxlColorSpace::Srgb,
314        };
315
316        let num_color_channels = if color_space == JxlColorSpace::Gray {
317            1u8
318        } else {
319            3u8
320        };
321
322        // Alpha
323        let has_alpha = reader.read_bool()?;
324        let num_channels = if has_alpha {
325            num_color_channels + 1
326        } else {
327            num_color_channels
328        };
329
330        // Animation header
331        let has_animation = reader.read_bool()?;
332        let animation = if has_animation {
333            Some(Self::parse_animation_header(reader)?)
334        } else {
335            None
336        };
337
338        Ok(JxlHeader {
339            width,
340            height,
341            bits_per_sample,
342            num_channels,
343            is_float: float_flag,
344            has_alpha,
345            color_space,
346            orientation,
347            animation,
348        })
349    }
350
351    /// Parse the animation header fields from the bitstream.
352    fn parse_animation_header(reader: &mut BitReader) -> CodecResult<JxlAnimation> {
353        let tps_numerator = reader.read_bits(32)?;
354        let tps_denominator = reader.read_bits(32)?;
355        let num_loops = reader.read_bits(32)?;
356        let have_timecodes = reader.read_bool()?;
357
358        if tps_numerator == 0 {
359            return Err(CodecError::InvalidBitstream(
360                "Animation tps_numerator must be non-zero".into(),
361            ));
362        }
363        if tps_denominator == 0 {
364            return Err(CodecError::InvalidBitstream(
365                "Animation tps_denominator must be non-zero".into(),
366            ));
367        }
368
369        Ok(JxlAnimation {
370            tps_numerator,
371            tps_denominator,
372            num_loops,
373            have_timecodes,
374        })
375    }
376
377    /// Parse a per-frame header from the bitstream.
378    ///
379    /// Returns (duration_ticks, is_last).
380    fn parse_frame_header(reader: &mut BitReader) -> CodecResult<(u32, bool)> {
381        let duration_ticks = reader.read_bits(32)?;
382        let is_last = reader.read_bool()?;
383        Ok((duration_ticks, is_last))
384    }
385
386    /// Decode the image data using the Modular sub-codec.
387    fn decode_modular(
388        &self,
389        reader: &mut BitReader,
390        header: &JxlHeader,
391    ) -> CodecResult<Vec<Vec<i32>>> {
392        reader.align_to_byte();
393
394        // Collect remaining data for the modular decoder
395        let remaining_bits = reader.remaining_bits();
396        if remaining_bits == 0 {
397            return Err(CodecError::InvalidBitstream(
398                "No image data after header".into(),
399            ));
400        }
401
402        // Read all remaining bytes into a buffer for the modular decoder
403        let remaining_bytes = (remaining_bits + 7) / 8;
404        let mut data = Vec::with_capacity(remaining_bytes);
405        for _ in 0..remaining_bytes {
406            match reader.read_u8(8) {
407                Ok(byte) => data.push(byte),
408                Err(_) => break,
409            }
410        }
411
412        let mut decoder = ModularDecoder::new();
413
414        // Add RCT transform for RGB/RGBA images (3+ color channels)
415        if header.color_channels() >= 3 {
416            decoder.add_transform(ModularTransform::Rct {
417                begin_channel: 0,
418                rct_type: 0,
419            });
420        }
421
422        decoder.decode_image(
423            &data,
424            header.width,
425            header.height,
426            header.num_channels as u32,
427            header.bits_per_sample,
428        )
429    }
430
431    /// Convert decoded channel data to interleaved byte output.
432    fn channels_to_interleaved(
433        &self,
434        channels: &[Vec<i32>],
435        header: &JxlHeader,
436    ) -> CodecResult<Vec<u8>> {
437        let pixel_count = header.width as usize * header.height as usize;
438        let num_channels = header.num_channels as usize;
439        let bytes_per_sample = header.bytes_per_sample();
440
441        if channels.len() != num_channels {
442            return Err(CodecError::Internal(format!(
443                "Expected {} channels, got {}",
444                num_channels,
445                channels.len()
446            )));
447        }
448
449        let total_bytes = pixel_count * num_channels * bytes_per_sample;
450        let mut output = Vec::with_capacity(total_bytes);
451
452        for i in 0..pixel_count {
453            for ch in 0..num_channels {
454                let value = channels[ch][i];
455
456                match bytes_per_sample {
457                    1 => {
458                        // Clamp to [0, 255]
459                        let clamped = value.clamp(0, 255) as u8;
460                        output.push(clamped);
461                    }
462                    2 => {
463                        // Clamp to [0, 65535], little-endian
464                        let clamped = value.clamp(0, 65535) as u16;
465                        output.push(clamped as u8);
466                        output.push((clamped >> 8) as u8);
467                    }
468                    _ => {
469                        // 32-bit: store as 4 bytes, little-endian
470                        let bytes = (value as u32).to_le_bytes();
471                        output.extend_from_slice(&bytes);
472                    }
473                }
474            }
475        }
476
477        Ok(output)
478    }
479
480    /// Decode an animated JPEG-XL codestream into a sequence of frames.
481    ///
482    /// If the codestream is not animated, returns a single frame with
483    /// `duration_ticks = 0` and `is_last = true`.
484    ///
485    /// # Errors
486    ///
487    /// Returns error if the codestream is invalid.
488    pub fn decode_animated(&self, data: &[u8]) -> CodecResult<Vec<JxlFrame>> {
489        let codestream = self.extract_codestream(data)?;
490        let mut reader = BitReader::new(codestream);
491
492        // Skip signature (2 bytes = 16 bits)
493        let _ = reader.read_bits(16)?;
494
495        // Parse size header
496        let (width, height) = self.parse_size_header(&mut reader)?;
497
498        // Parse image metadata (including animation header)
499        let header = self.parse_image_metadata(&mut reader, width, height)?;
500        header.validate()?;
501
502        if header.animation.is_none() {
503            // Not animated -- decode as single frame, reuse existing logic
504            let channels_data = self.decode_modular(&mut reader, &header)?;
505            let pixel_data = self.channels_to_interleaved(&channels_data, &header)?;
506
507            return Ok(vec![JxlFrame {
508                data: pixel_data,
509                width: header.width,
510                height: header.height,
511                channels: header.num_channels,
512                bit_depth: header.bits_per_sample,
513                duration_ticks: 0,
514                is_last: true,
515                color_space: header.color_space,
516            }]);
517        }
518
519        // Animated codestream: read frame-by-frame
520        let mut frames = Vec::new();
521
522        loop {
523            // Check if we have enough bits for a frame header
524            if reader.remaining_bits() < 33 {
525                // Need at least 32 bits for duration + 1 bit for is_last
526                break;
527            }
528
529            let (duration_ticks, is_last) = Self::parse_frame_header(&mut reader)?;
530
531            // Align to byte boundary before frame data
532            reader.align_to_byte();
533
534            // Read frame data length
535            if reader.remaining_bits() < 32 {
536                return Err(CodecError::InvalidBitstream(
537                    "Unexpected end of animated codestream before frame data length".into(),
538                ));
539            }
540            let data_len = reader.read_bits(32)? as usize;
541
542            // Read frame data bytes
543            if reader.remaining_bits() < data_len * 8 {
544                return Err(CodecError::InvalidBitstream(format!(
545                    "Animated frame data truncated: expected {data_len} bytes, \
546                     have {} bits remaining",
547                    reader.remaining_bits()
548                )));
549            }
550
551            let mut frame_data_bytes = Vec::with_capacity(data_len);
552            for _ in 0..data_len {
553                frame_data_bytes.push(reader.read_u8(8)?);
554            }
555
556            // Decode this frame's modular data
557            let channels_data = self.decode_frame_modular(&frame_data_bytes, &header)?;
558            let pixel_data = self.channels_to_interleaved(&channels_data, &header)?;
559
560            frames.push(JxlFrame {
561                data: pixel_data,
562                width: header.width,
563                height: header.height,
564                channels: header.num_channels,
565                bit_depth: header.bits_per_sample,
566                duration_ticks,
567                is_last,
568                color_space: header.color_space,
569            });
570
571            if is_last {
572                break;
573            }
574        }
575
576        if frames.is_empty() {
577            return Err(CodecError::InvalidBitstream(
578                "Animated codestream contains no frames".into(),
579            ));
580        }
581
582        Ok(frames)
583    }
584
585    /// Decode a single frame's modular data from its raw bytes.
586    fn decode_frame_modular(&self, data: &[u8], header: &JxlHeader) -> CodecResult<Vec<Vec<i32>>> {
587        let mut decoder = ModularDecoder::new();
588
589        // Add RCT transform for RGB/RGBA images
590        if header.color_channels() >= 3 {
591            decoder.add_transform(ModularTransform::Rct {
592                begin_channel: 0,
593                rct_type: 0,
594            });
595        }
596
597        decoder.decode_image(
598            data,
599            header.width,
600            header.height,
601            header.num_channels as u32,
602            header.bits_per_sample,
603        )
604    }
605
606    /// Check if a codestream is animated by reading just the header.
607    ///
608    /// # Errors
609    ///
610    /// Returns error if the header is invalid.
611    pub fn is_animated(&self, data: &[u8]) -> CodecResult<bool> {
612        let header = self.read_header(data)?;
613        Ok(header.animation.is_some())
614    }
615
616    /// Read the animation header from a JPEG-XL file.
617    ///
618    /// Returns `None` if the file is not animated.
619    ///
620    /// # Errors
621    ///
622    /// Returns error if the header is invalid.
623    pub fn read_animation_header(&self, data: &[u8]) -> CodecResult<Option<JxlAnimation>> {
624        let header = self.read_header(data)?;
625        Ok(header.animation)
626    }
627}
628
629impl Default for JxlDecoder {
630    fn default() -> Self {
631        Self::new()
632    }
633}
634
635// ── JxlFormat ─────────────────────────────────────────────────────────────────
636
637/// Container format detected from the first 12 bytes of a stream.
638#[derive(Copy, Clone, Debug, PartialEq, Eq)]
639enum JxlFormat {
640    /// ISOBMFF container from `AnimatedJxlEncoder::finish_isobmff()`.
641    /// Detected by `bytes[4..8] == b"ftyp"` and `bytes[8..12] == b"jxl "`.
642    Isobmff,
643    /// OxiMedia native bare codestream from `AnimatedJxlEncoder::finish()`.
644    /// Detected by the JXL codestream signature `0xFF 0x0A`.
645    Native,
646}
647
648// ── JxlStreamingDecoder ───────────────────────────────────────────────────────
649
650/// A streaming decoder that lazily yields [`JxlFrame`]s one at a time.
651///
652/// Supports two input formats — auto-detected from the first 12 bytes:
653///
654/// | Format  | Detection | Producer |
655/// |---------|-----------|----------|
656/// | ISOBMFF | `bytes[4..8] == b"ftyp"` and `bytes[8..12] == b"jxl "` | `AnimatedJxlEncoder::finish_isobmff()` |
657/// | Native  | `bytes[0..2] == [0xFF, 0x0A]` | `AnimatedJxlEncoder::finish()` |
658///
659/// ## Example
660///
661/// ```ignore
662/// use oximedia_codec::jpegxl::{AnimatedJxlEncoder, JxlAnimation, JxlStreamingDecoder};
663/// use std::io::Cursor;
664///
665/// let bytes = encoder.finish_isobmff()?;
666/// for frame_result in JxlStreamingDecoder::new(Cursor::new(bytes))? {
667///     let frame = frame_result?;
668///     println!("frame {}x{} ticks={}", frame.width, frame.height, frame.duration_ticks);
669/// }
670/// ```
671pub struct JxlStreamingDecoder<R: Read> {
672    format: JxlFormat,
673    /// ISOBMFF path: box iterator over the stream.
674    /// Set to `None` after the last jxlp has been decoded.
675    box_iter: Option<BoxIter<PeekedReader<R>>>,
676    /// Accumulated bare-codestream bytes from `jxlp` boxes (ISOBMFF path).
677    codestream_buf: Vec<u8>,
678    /// Frames decoded from a jxlp batch, pending yield on subsequent calls.
679    pending_frames: std::vec::IntoIter<JxlFrame>,
680    /// True once this iterator has reached a terminal state.
681    done: bool,
682}
683
684impl<R: Read> JxlStreamingDecoder<R> {
685    /// Create a new streaming decoder, auto-detecting the format from `reader`.
686    ///
687    /// Reads at most 12 bytes for detection, then re-prepends them so no
688    /// input is skipped before the first `next()` call.
689    ///
690    /// # Errors
691    ///
692    /// Returns an error if the initial peek read fails, or (for the native
693    /// format) if the codestream cannot be decoded.
694    pub fn new(mut reader: R) -> CodecResult<Self> {
695        // Peek the first 12 bytes to detect the format.
696        let mut peek = [0u8; 12];
697        let n = reader.read(&mut peek)?;
698        let peek_bytes = peek[..n].to_vec();
699
700        // ISOBMFF: bytes[4..8] == b"ftyp"  and  bytes[8..12] == b"jxl "
701        let format = if n >= 12 && peek_bytes[4..8] == *b"ftyp" && peek_bytes[8..12] == *b"jxl " {
702            JxlFormat::Isobmff
703        } else {
704            JxlFormat::Native
705        };
706
707        // Re-prepend peeked bytes so downstream reads start from byte 0.
708        let mut chained: PeekedReader<R> = io::Cursor::new(peek_bytes).chain(reader);
709
710        match format {
711            JxlFormat::Isobmff => Ok(Self {
712                format,
713                box_iter: Some(BoxIter::new(chained)),
714                codestream_buf: Vec::new(),
715                pending_frames: Vec::new().into_iter(),
716                done: false,
717            }),
718            JxlFormat::Native => {
719                // Native: read everything and decode all frames eagerly.
720                let mut all_bytes = Vec::new();
721                chained
722                    .read_to_end(&mut all_bytes)
723                    .map_err(CodecError::Io)?;
724
725                let frames = JxlDecoder::new().decode_animated(&all_bytes)?;
726                Ok(Self {
727                    format,
728                    box_iter: None,
729                    codestream_buf: Vec::new(),
730                    pending_frames: frames.into_iter(),
731                    done: false,
732                })
733            }
734        }
735    }
736}
737
738impl<R: Read> Iterator for JxlStreamingDecoder<R> {
739    type Item = CodecResult<JxlFrame>;
740
741    fn next(&mut self) -> Option<Self::Item> {
742        if self.done {
743            return None;
744        }
745
746        // Yield any frames that were decoded in a prior iteration step.
747        if let Some(frame) = self.pending_frames.next() {
748            return Some(Ok(frame));
749        }
750
751        match self.format {
752            // ── Native path ───────────────────────────────────────────────────
753            // All frames were decoded eagerly in the constructor and yielded
754            // via `pending_frames`; reaching here means exhaustion.
755            JxlFormat::Native => {
756                self.done = true;
757                None
758            }
759
760            // ── ISOBMFF path ──────────────────────────────────────────────────
761            // Parse `jxlp` boxes; accumulate codestream; decode on last-box flag.
762            JxlFormat::Isobmff => {
763                let box_iter = match self.box_iter.as_mut() {
764                    Some(bi) => bi,
765                    None => {
766                        self.done = true;
767                        return None;
768                    }
769                };
770
771                loop {
772                    match box_iter.next() {
773                        // Stream ended.
774                        None => {
775                            self.done = true;
776                            if !self.codestream_buf.is_empty() {
777                                let buf = std::mem::take(&mut self.codestream_buf);
778                                return Some(Self::flush_codestream(buf, &mut self.pending_frames));
779                            }
780                            return None;
781                        }
782
783                        // I/O error.
784                        Some(Err(e)) => {
785                            self.done = true;
786                            return Some(Err(CodecError::Io(e)));
787                        }
788
789                        // Box parsed.
790                        Some(Ok((fourcc, payload))) => {
791                            if fourcc != *b"jxlp" {
792                                // Skip ftyp, jxll, etc.
793                                continue;
794                            }
795
796                            // jxlp layout: [4 bytes: index/flags][codestream bytes...]
797                            if payload.len() < 4 {
798                                self.done = true;
799                                return Some(Err(CodecError::InvalidBitstream(
800                                    "jxlp box payload too short (< 4 bytes)".into(),
801                                )));
802                            }
803
804                            let mut idx_buf = [0u8; 4];
805                            idx_buf.copy_from_slice(&payload[0..4]);
806                            let is_last = (u32::from_be_bytes(idx_buf) & 0x8000_0000) != 0;
807
808                            self.codestream_buf.extend_from_slice(&payload[4..]);
809
810                            if is_last {
811                                let buf = std::mem::take(&mut self.codestream_buf);
812                                self.box_iter = None;
813                                return Some(Self::flush_codestream(buf, &mut self.pending_frames));
814                            }
815                            // Not last yet — keep accumulating.
816                        }
817                    }
818                }
819            }
820        }
821    }
822}
823
824impl<R: Read> JxlStreamingDecoder<R> {
825    /// Decode a complete accumulated bare codestream, yield the first frame,
826    /// and stash remaining frames in `pending`.
827    fn flush_codestream(
828        buf: Vec<u8>,
829        pending: &mut std::vec::IntoIter<JxlFrame>,
830    ) -> CodecResult<JxlFrame> {
831        let mut frames = JxlDecoder::new().decode_animated(&buf)?;
832        if frames.is_empty() {
833            return Err(CodecError::InvalidBitstream(
834                "jxlp codestream contained no frames".into(),
835            ));
836        }
837        let first = frames.remove(0);
838        *pending = frames.into_iter();
839        Ok(first)
840    }
841}
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    #[ignore]
849    fn test_is_codestream_signature() {
850        assert!(JxlDecoder::is_codestream(&[0xFF, 0x0A, 0x00]));
851        assert!(!JxlDecoder::is_codestream(&[0xFF, 0x0B, 0x00]));
852        assert!(!JxlDecoder::is_codestream(&[0xFF]));
853        assert!(!JxlDecoder::is_codestream(&[]));
854    }
855
856    #[test]
857    #[ignore]
858    fn test_is_container_signature() {
859        let mut container = vec![0u8; 16];
860        container[..12].copy_from_slice(&JXL_CONTAINER_SIGNATURE);
861        assert!(JxlDecoder::is_container(&container));
862        assert!(!JxlDecoder::is_container(&[0xFF, 0x0A]));
863    }
864
865    #[test]
866    #[ignore]
867    fn test_is_jxl() {
868        assert!(JxlDecoder::is_jxl(&[0xFF, 0x0A]));
869        let mut container = vec![0u8; 16];
870        container[..12].copy_from_slice(&JXL_CONTAINER_SIGNATURE);
871        assert!(JxlDecoder::is_jxl(&container));
872        assert!(!JxlDecoder::is_jxl(&[0x00, 0x00]));
873    }
874
875    #[test]
876    #[ignore]
877    fn test_extract_codestream_bare() {
878        let decoder = JxlDecoder::new();
879        let data = [0xFF, 0x0A, 0x01, 0x02];
880        let result = decoder.extract_codestream(&data).expect("ok");
881        assert_eq!(result, &data);
882    }
883
884    #[test]
885    #[ignore]
886    fn test_extract_codestream_invalid() {
887        let decoder = JxlDecoder::new();
888        assert!(decoder.extract_codestream(&[0x00, 0x00]).is_err());
889    }
890
891    #[test]
892    #[ignore]
893    fn test_parse_size_header_small() {
894        // small=1, height_div8=3 (24px), width_div8=0 (use height -> 24px)
895        let decoder = JxlDecoder::new();
896        let mut writer = super::super::bitreader::BitWriter::new();
897        writer.write_bool(true); // small = true
898        writer.write_bits(2, 5); // height_div8 - 1 = 2 -> height = 3*8 = 24
899        writer.write_bits(0, 5); // width_div8 = 0 -> use height_div8
900        let data = writer.finish();
901
902        let mut reader = BitReader::new(&data);
903        let (w, h) = decoder.parse_size_header(&mut reader).expect("ok");
904        assert_eq!(h, 24);
905        assert_eq!(w, 24);
906    }
907
908    #[test]
909    #[ignore]
910    fn test_read_header_invalid_data() {
911        let decoder = JxlDecoder::new();
912        assert!(decoder.read_header(&[0x00]).is_err());
913    }
914
915    #[test]
916    #[ignore]
917    fn test_decoded_image_metrics() {
918        let img = DecodedImage {
919            width: 10,
920            height: 10,
921            channels: 3,
922            bit_depth: 8,
923            data: vec![0u8; 300],
924            color_space: JxlColorSpace::Srgb,
925        };
926        assert_eq!(img.sample_count(), 300);
927        assert_eq!(img.data_size(), 300);
928    }
929
930    #[test]
931    #[ignore]
932    fn test_decoded_image_16bit() {
933        let img = DecodedImage {
934            width: 10,
935            height: 10,
936            channels: 3,
937            bit_depth: 16,
938            data: vec![0u8; 600],
939            color_space: JxlColorSpace::Srgb,
940        };
941        assert_eq!(img.sample_count(), 300);
942        assert_eq!(img.data_size(), 600);
943    }
944}