Skip to main content

zenjxl_decoder/api/
decoder.rs

1// Copyright (c) the JPEG XL Project Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6use super::{
7    JxlBasicInfo, JxlBitstreamInput, JxlColorProfile, JxlDecoderInner, JxlDecoderOptions,
8    JxlOutputBuffer, JxlPixelFormat, ProcessingResult,
9};
10#[cfg(test)]
11use crate::frame::Frame;
12use crate::{
13    api::JxlFrameHeader,
14    container::{frame_index::FrameIndexBox, gain_map::GainMapBundle},
15    error::Result,
16};
17use states::*;
18use std::marker::PhantomData;
19
20pub mod states {
21    pub trait JxlState {}
22    pub struct Initialized;
23    pub struct WithImageInfo;
24    pub struct WithFrameInfo;
25    impl JxlState for Initialized {}
26    impl JxlState for WithImageInfo {}
27    impl JxlState for WithFrameInfo {}
28}
29
30// Q: do we plan to add support for box decoding?
31// If we do, one way is to take a callback &[u8; 4] -> Box<dyn Write>.
32
33/// High level API using the typestate pattern to forbid invalid usage.
34pub struct JxlDecoder<State: JxlState> {
35    inner: Box<JxlDecoderInner>,
36    _state: PhantomData<State>,
37}
38
39#[cfg(test)]
40pub type FrameCallback = dyn FnMut(&Frame, usize) -> Result<()>;
41
42impl<S: JxlState> JxlDecoder<S> {
43    fn wrap_inner(inner: Box<JxlDecoderInner>) -> Self {
44        Self {
45            inner,
46            _state: PhantomData,
47        }
48    }
49
50    /// Sets a callback that processes all frames by calling `callback(frame, frame_index)`.
51    #[cfg(test)]
52    pub fn set_frame_callback(&mut self, callback: Box<FrameCallback>) {
53        self.inner.set_frame_callback(callback);
54    }
55
56    #[cfg(test)]
57    pub fn decoded_frames(&self) -> usize {
58        self.inner.decoded_frames()
59    }
60
61    /// Returns the reconstructed JPEG bytes if the file contained a JBRD box.
62    /// Call after decoding a frame. Returns `None` if no JBRD box was present
63    /// or the `jpeg` feature is not enabled.
64    #[cfg(feature = "jpeg")]
65    pub fn take_jpeg_reconstruction(&mut self) -> Option<Vec<u8>> {
66        self.inner.take_jpeg_reconstruction()
67    }
68
69    /// Returns the parsed frame index box, if the file contained one.
70    ///
71    /// The frame index box (`jxli`) is an optional part of the JXL container
72    /// format that provides a seek table for animated files, listing keyframe
73    /// byte offsets, timestamps, and frame counts.
74    pub fn frame_index(&self) -> Option<&FrameIndexBox> {
75        self.inner.frame_index()
76    }
77
78    /// Returns a reference to the parsed gain map bundle, if the file contained
79    /// a `jhgm` box (ISO 21496-1 HDR gain map).
80    ///
81    /// The gain map codestream is a bare JXL codestream that can be decoded
82    /// with the same decoder. The ISO 21496-1 metadata blob is stored as raw
83    /// bytes for the caller to parse.
84    pub fn gain_map(&self) -> Option<&GainMapBundle> {
85        self.inner.gain_map()
86    }
87
88    /// Takes the parsed gain map bundle, if the file contained a `jhgm` box.
89    /// After calling this, `gain_map()` will return `None`.
90    pub fn take_gain_map(&mut self) -> Option<GainMapBundle> {
91        self.inner.take_gain_map()
92    }
93
94    /// Returns the raw EXIF data from the `Exif` container box, if present.
95    ///
96    /// The 4-byte TIFF header offset prefix is stripped; this returns the raw
97    /// EXIF/TIFF bytes starting with the byte-order marker (`II` or `MM`).
98    /// Returns `None` for bare codestreams or files without an `Exif` box.
99    ///
100    /// Note: the `Exif` box may appear after the codestream in the container.
101    /// Call this after decoding at least one frame for the most complete results.
102    pub fn exif(&self) -> Option<&[u8]> {
103        self.inner.exif()
104    }
105
106    /// Takes the EXIF data, leaving `None` in its place.
107    pub fn take_exif(&mut self) -> Option<Vec<u8>> {
108        self.inner.take_exif()
109    }
110
111    /// Returns the raw XMP data from the `xml ` container box, if present.
112    ///
113    /// Returns `None` for bare codestreams or files without an `xml ` box.
114    ///
115    /// Note: the `xml ` box may appear after the codestream in the container.
116    /// Call this after decoding at least one frame for the most complete results.
117    pub fn xmp(&self) -> Option<&[u8]> {
118        self.inner.xmp()
119    }
120
121    /// Takes the XMP data, leaving `None` in its place.
122    pub fn take_xmp(&mut self) -> Option<Vec<u8>> {
123        self.inner.take_xmp()
124    }
125
126    /// Rewinds a decoder to the start of the file, allowing past frames to be displayed again.
127    pub fn rewind(mut self) -> JxlDecoder<Initialized> {
128        self.inner.rewind();
129        JxlDecoder::wrap_inner(self.inner)
130    }
131
132    fn map_inner_processing_result<SuccessState: JxlState>(
133        self,
134        inner_result: ProcessingResult<(), ()>,
135    ) -> ProcessingResult<JxlDecoder<SuccessState>, Self> {
136        match inner_result {
137            ProcessingResult::Complete { .. } => ProcessingResult::Complete {
138                result: JxlDecoder::wrap_inner(self.inner),
139            },
140            ProcessingResult::NeedsMoreInput { size_hint, .. } => {
141                ProcessingResult::NeedsMoreInput {
142                    size_hint,
143                    fallback: self,
144                }
145            }
146        }
147    }
148}
149
150impl JxlDecoder<Initialized> {
151    pub fn new(options: JxlDecoderOptions) -> Self {
152        Self::wrap_inner(Box::new(JxlDecoderInner::new(options)))
153    }
154
155    pub fn process(
156        mut self,
157        input: &mut impl JxlBitstreamInput,
158    ) -> Result<ProcessingResult<JxlDecoder<WithImageInfo>, Self>> {
159        let inner_result = self.inner.process(input, None)?;
160        Ok(self.map_inner_processing_result(inner_result))
161    }
162}
163
164impl JxlDecoder<WithImageInfo> {
165    // TODO(veluca): once frame skipping is implemented properly, expose that in the API.
166
167    /// Obtains the image's basic information.
168    pub fn basic_info(&self) -> &JxlBasicInfo {
169        self.inner.basic_info().unwrap()
170    }
171
172    /// Retrieves the file's color profile.
173    pub fn embedded_color_profile(&self) -> &JxlColorProfile {
174        self.inner.embedded_color_profile().unwrap()
175    }
176
177    /// Retrieves the current output color profile.
178    pub fn output_color_profile(&self) -> &JxlColorProfile {
179        self.inner.output_color_profile().unwrap()
180    }
181
182    /// Specifies the preferred color profile to be used for outputting data.
183    /// Same semantics as JxlDecoderSetOutputColorProfile.
184    pub fn set_output_color_profile(&mut self, profile: JxlColorProfile) -> Result<()> {
185        self.inner.set_output_color_profile(profile)
186    }
187
188    /// Retrieves the current pixel format for output buffers.
189    pub fn current_pixel_format(&self) -> &JxlPixelFormat {
190        self.inner.current_pixel_format().unwrap()
191    }
192
193    /// Specifies pixel format for output buffers.
194    ///
195    /// Setting this may also change output color profile in some cases, if the profile was not set
196    /// manually before.
197    pub fn set_pixel_format(&mut self, pixel_format: JxlPixelFormat) {
198        self.inner.set_pixel_format(pixel_format);
199    }
200
201    pub fn process(
202        mut self,
203        input: &mut impl JxlBitstreamInput,
204    ) -> Result<ProcessingResult<JxlDecoder<WithFrameInfo>, Self>> {
205        let inner_result = self.inner.process(input, None)?;
206        Ok(self.map_inner_processing_result(inner_result))
207    }
208
209    /// Draws all the pixels we have data for. This is useful for i.e. previewing LF frames.
210    ///
211    /// Note: see `process` for alignment requirements for the buffer data.
212    pub fn flush_pixels(&mut self, buffers: &mut [JxlOutputBuffer<'_>]) -> Result<()> {
213        self.inner.flush_pixels(buffers)
214    }
215
216    pub fn has_more_frames(&self) -> bool {
217        self.inner.has_more_frames()
218    }
219
220    #[cfg(test)]
221    pub(crate) fn set_use_simple_pipeline(&mut self, u: bool) {
222        self.inner.set_use_simple_pipeline(u);
223    }
224}
225
226impl JxlDecoder<WithFrameInfo> {
227    /// Skip the current frame.
228    pub fn skip_frame(
229        mut self,
230        input: &mut impl JxlBitstreamInput,
231    ) -> Result<ProcessingResult<JxlDecoder<WithImageInfo>, Self>> {
232        let inner_result = self.inner.process(input, None)?;
233        Ok(self.map_inner_processing_result(inner_result))
234    }
235
236    pub fn frame_header(&self) -> JxlFrameHeader {
237        self.inner.frame_header().unwrap()
238    }
239
240    /// Number of passes we have full data for.
241    pub fn num_completed_passes(&self) -> usize {
242        self.inner.num_completed_passes().unwrap()
243    }
244
245    /// Draws all the pixels we have data for.
246    ///
247    /// Note: see `process` for alignment requirements for the buffer data.
248    pub fn flush_pixels(&mut self, buffers: &mut [JxlOutputBuffer<'_>]) -> Result<()> {
249        self.inner.flush_pixels(buffers)
250    }
251
252    /// Guarantees to populate exactly the appropriate part of the buffers.
253    /// Wants one buffer for each non-ignored pixel type, i.e. color channels and each extra channel.
254    ///
255    /// Note: the data in `buffers` should have alignment requirements that are compatible with the
256    /// requested pixel format. This means that, if we are asking for 2-byte or 4-byte output (i.e.
257    /// u16/f16 and f32 respectively), each row in the provided buffers must be aligned to 2 or 4
258    /// bytes respectively. If that is not the case, the library may panic.
259    pub fn process<In: JxlBitstreamInput>(
260        mut self,
261        input: &mut In,
262        buffers: &mut [JxlOutputBuffer<'_>],
263    ) -> Result<ProcessingResult<JxlDecoder<WithImageInfo>, Self>> {
264        let inner_result = self.inner.process(input, Some(buffers))?;
265        Ok(self.map_inner_processing_result(inner_result))
266    }
267}
268
269#[cfg(test)]
270pub(crate) mod tests {
271    use super::*;
272    use crate::api::{JxlDataFormat, JxlDecoderOptions};
273    use crate::error::Error;
274    use crate::image::{Image, Rect};
275    use jxl_macros::for_each_test_file;
276    use std::path::Path;
277
278    #[test]
279    fn decode_small_chunks() {
280        arbtest::arbtest(|u| {
281            decode(
282                &std::fs::read("resources/test/green_queen_vardct_e3.jxl").unwrap(),
283                u.arbitrary::<u8>().unwrap() as usize + 1,
284                false,
285                false,
286                None,
287            )
288            .unwrap();
289            Ok(())
290        });
291    }
292
293    #[allow(clippy::type_complexity)]
294    pub fn decode(
295        mut input: &[u8],
296        chunk_size: usize,
297        use_simple_pipeline: bool,
298        do_flush: bool,
299        callback: Option<Box<dyn FnMut(&Frame, usize) -> Result<(), Error>>>,
300    ) -> Result<(usize, Vec<Vec<Image<f32>>>), Error> {
301        let options = JxlDecoderOptions::default();
302        let mut initialized_decoder = JxlDecoder::<states::Initialized>::new(options);
303
304        if let Some(callback) = callback {
305            initialized_decoder.set_frame_callback(callback);
306        }
307
308        let mut chunk_input = &input[0..0];
309
310        macro_rules! advance_decoder {
311            ($decoder: ident $(, $extra_arg: expr)? $(; $flush_arg: expr)?) => {
312                loop {
313                    chunk_input =
314                        &input[..(chunk_input.len().saturating_add(chunk_size)).min(input.len())];
315                    let available_before = chunk_input.len();
316                    let process_result = $decoder.process(&mut chunk_input $(, $extra_arg)?);
317                    input = &input[(available_before - chunk_input.len())..];
318                    match process_result.unwrap() {
319                        ProcessingResult::Complete { result } => break result,
320                        ProcessingResult::NeedsMoreInput { fallback, .. } => {
321                            $(
322                                let mut fallback = fallback;
323                                if do_flush && !input.is_empty() {
324                                    fallback.flush_pixels($flush_arg)?;
325                                }
326                            )?
327                            if input.is_empty() {
328                                panic!("Unexpected end of input");
329                            }
330                            $decoder = fallback;
331                        }
332                    }
333                }
334            };
335        }
336
337        // Process until we have image info
338        let mut decoder_with_image_info = advance_decoder!(initialized_decoder);
339        decoder_with_image_info.set_use_simple_pipeline(use_simple_pipeline);
340
341        // Get basic info
342        let basic_info = decoder_with_image_info.basic_info().clone();
343        assert!(basic_info.bit_depth.bits_per_sample() > 0);
344
345        // Get image dimensions (after upsampling, which is the actual output size)
346        let (buffer_width, buffer_height) = basic_info.size;
347        assert!(buffer_width > 0);
348        assert!(buffer_height > 0);
349
350        // Explicitly request F32 pixel format (test helper returns Image<f32>)
351        let default_format = decoder_with_image_info.current_pixel_format();
352        let requested_format = JxlPixelFormat {
353            color_type: default_format.color_type,
354            color_data_format: Some(JxlDataFormat::f32()),
355            extra_channel_format: default_format
356                .extra_channel_format
357                .iter()
358                .map(|_| Some(JxlDataFormat::f32()))
359                .collect(),
360        };
361        decoder_with_image_info.set_pixel_format(requested_format);
362
363        // Get the configured pixel format
364        let pixel_format = decoder_with_image_info.current_pixel_format().clone();
365
366        let num_channels = pixel_format.color_type.samples_per_pixel();
367        assert!(num_channels > 0);
368
369        let mut frames = vec![];
370
371        loop {
372            // First channel is interleaved.
373            let mut buffers = vec![Image::new_with_value(
374                (buffer_width * num_channels, buffer_height),
375                f32::NAN,
376            )?];
377
378            for ecf in pixel_format.extra_channel_format.iter() {
379                if ecf.is_none() {
380                    continue;
381                }
382                buffers.push(Image::new_with_value(
383                    (buffer_width, buffer_height),
384                    f32::NAN,
385                )?);
386            }
387
388            let mut api_buffers: Vec<_> = buffers
389                .iter_mut()
390                .map(|b| {
391                    JxlOutputBuffer::from_image_rect_mut(
392                        b.get_rect_mut(Rect {
393                            origin: (0, 0),
394                            size: b.size(),
395                        })
396                        .into_raw(),
397                    )
398                })
399                .collect();
400
401            // Process until we have frame info
402            let mut decoder_with_frame_info =
403                advance_decoder!(decoder_with_image_info; &mut api_buffers);
404            decoder_with_image_info =
405                advance_decoder!(decoder_with_frame_info, &mut api_buffers; &mut api_buffers);
406
407            // All pixels should have been overwritten, so they should no longer be NaNs.
408            for buf in buffers.iter() {
409                let (xs, ys) = buf.size();
410                for y in 0..ys {
411                    let row = buf.row(y);
412                    for (x, v) in row.iter().enumerate() {
413                        assert!(!v.is_nan(), "NaN at {x} {y} (image size {xs}x{ys})");
414                    }
415                }
416            }
417
418            frames.push(buffers);
419
420            // Check if there are more frames
421            if !decoder_with_image_info.has_more_frames() {
422                let decoded_frames = decoder_with_image_info.decoded_frames();
423
424                // Ensure we decoded at least one frame
425                assert!(decoded_frames > 0, "No frames were decoded");
426
427                return Ok((decoded_frames, frames));
428            }
429        }
430    }
431
432    fn decode_test_file(path: &Path) -> Result<(), Error> {
433        decode(&std::fs::read(path)?, usize::MAX, false, false, None)?;
434        Ok(())
435    }
436
437    for_each_test_file!(decode_test_file);
438
439    fn decode_test_file_chunks(path: &Path) -> Result<(), Error> {
440        decode(&std::fs::read(path)?, 1, false, false, None)?;
441        Ok(())
442    }
443
444    for_each_test_file!(decode_test_file_chunks);
445
446    fn compare_frames(
447        _path: &Path,
448        fc: usize,
449        f: &[Image<f32>],
450        sf: &[Image<f32>],
451    ) -> Result<(), Error> {
452        assert_eq!(
453            f.len(),
454            sf.len(),
455            "Frame {fc} has different channels counts",
456        );
457        for (c, (b, sb)) in f.iter().zip(sf.iter()).enumerate() {
458            assert_eq!(
459                b.size(),
460                sb.size(),
461                "Channel {c} in frame {fc} has different sizes",
462            );
463            let sz = b.size();
464            for y in 0..sz.1 {
465                for x in 0..sz.0 {
466                    assert_eq!(
467                        b.row(y)[x],
468                        sb.row(y)[x],
469                        "Pixels differ at position ({x}, {y}), channel {c}"
470                    );
471                }
472            }
473        }
474        Ok(())
475    }
476
477    fn compare_pipelines(path: &Path) -> Result<(), Error> {
478        let file = std::fs::read(path)?;
479        let simple_frames = decode(&file, usize::MAX, true, false, None)?.1;
480        let frames = decode(&file, usize::MAX, false, false, None)?.1;
481        assert_eq!(frames.len(), simple_frames.len());
482        for (fc, (f, sf)) in frames
483            .into_iter()
484            .zip(simple_frames.into_iter())
485            .enumerate()
486        {
487            compare_frames(path, fc, &f, &sf)?;
488        }
489        Ok(())
490    }
491
492    for_each_test_file!(compare_pipelines);
493
494    fn compare_incremental(path: &Path) -> Result<(), Error> {
495        let file = std::fs::read(path).unwrap();
496        // One-shot decode
497        let (_, one_shot_frames) = decode(&file, usize::MAX, false, false, None)?;
498        // Incremental decode with arbitrary flushes.
499        let (_, frames) = decode(&file, 123, false, true, None)?;
500
501        // Compare one_shot_frames and frames
502        assert_eq!(one_shot_frames.len(), frames.len());
503        for (fc, (f, sf)) in frames
504            .into_iter()
505            .zip(one_shot_frames.into_iter())
506            .enumerate()
507        {
508            compare_frames(path, fc, &f, &sf)?;
509        }
510
511        Ok(())
512    }
513
514    for_each_test_file!(compare_incremental);
515
516    #[test]
517    fn test_preview_size_none_for_regular_files() {
518        let file = std::fs::read("resources/test/basic.jxl").unwrap();
519        let options = JxlDecoderOptions::default();
520        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
521        let mut input = file.as_slice();
522        let decoder = loop {
523            match decoder.process(&mut input).unwrap() {
524                ProcessingResult::Complete { result } => break result,
525                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
526            }
527        };
528        assert!(decoder.basic_info().preview_size.is_none());
529    }
530
531    #[test]
532    fn test_preview_size_some_for_preview_files() {
533        let file = std::fs::read("resources/test/with_preview.jxl").unwrap();
534        let options = JxlDecoderOptions::default();
535        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
536        let mut input = file.as_slice();
537        let decoder = loop {
538            match decoder.process(&mut input).unwrap() {
539                ProcessingResult::Complete { result } => break result,
540                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
541            }
542        };
543        assert_eq!(decoder.basic_info().preview_size, Some((16, 16)));
544    }
545
546    #[test]
547    fn test_num_completed_passes() {
548        use crate::image::{Image, Rect};
549        let file = std::fs::read("resources/test/basic.jxl").unwrap();
550        let options = JxlDecoderOptions::default();
551        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
552        let mut input = file.as_slice();
553        // Process until we have image info
554        let mut decoder_with_info = loop {
555            match decoder.process(&mut input).unwrap() {
556                ProcessingResult::Complete { result } => break result,
557                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
558            }
559        };
560        let info = decoder_with_info.basic_info().clone();
561        let mut decoder_with_frame = loop {
562            match decoder_with_info.process(&mut input).unwrap() {
563                ProcessingResult::Complete { result } => break result,
564                ProcessingResult::NeedsMoreInput { fallback, .. } => {
565                    decoder_with_info = fallback;
566                }
567            }
568        };
569        // Before processing frame, passes should be 0
570        assert_eq!(decoder_with_frame.num_completed_passes(), 0);
571        // Process the frame
572        let mut output = Image::<f32>::new((info.size.0 * 3, info.size.1)).unwrap();
573        let rect = Rect {
574            size: output.size(),
575            origin: (0, 0),
576        };
577        let mut bufs = [JxlOutputBuffer::from_image_rect_mut(
578            output.get_rect_mut(rect).into_raw(),
579        )];
580        loop {
581            match decoder_with_frame.process(&mut input, &mut bufs).unwrap() {
582                ProcessingResult::Complete { .. } => break,
583                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder_with_frame = fallback,
584            }
585        }
586    }
587
588    #[test]
589    fn test_set_pixel_format() {
590        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
591
592        let file = std::fs::read("resources/test/basic.jxl").unwrap();
593        let options = JxlDecoderOptions::default();
594        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
595        let mut input = file.as_slice();
596        let mut decoder = loop {
597            match decoder.process(&mut input).unwrap() {
598                ProcessingResult::Complete { result } => break result,
599                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
600            }
601        };
602        // Check default pixel format
603        let default_format = decoder.current_pixel_format().clone();
604        assert_eq!(default_format.color_type, JxlColorType::Rgb);
605
606        // Set a new pixel format
607        let new_format = JxlPixelFormat {
608            color_type: JxlColorType::Grayscale,
609            color_data_format: Some(JxlDataFormat::U8 { bit_depth: 8 }),
610            extra_channel_format: vec![],
611        };
612        decoder.set_pixel_format(new_format.clone());
613
614        // Verify it was set
615        assert_eq!(decoder.current_pixel_format(), &new_format);
616    }
617
618    #[test]
619    fn test_set_output_color_profile() {
620        use crate::api::JxlColorProfile;
621
622        let file = std::fs::read("resources/test/basic.jxl").unwrap();
623        let options = JxlDecoderOptions::default();
624        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
625        let mut input = file.as_slice();
626        let mut decoder = loop {
627            match decoder.process(&mut input).unwrap() {
628                ProcessingResult::Complete { result } => break result,
629                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
630            }
631        };
632
633        // Get the embedded profile and set it as output (should work)
634        let embedded = decoder.embedded_color_profile().clone();
635        let result = decoder.set_output_color_profile(embedded);
636        assert!(result.is_ok());
637
638        // Setting an ICC profile without CMS should fail
639        let icc_profile = JxlColorProfile::Icc(vec![0u8; 100]);
640        let result = decoder.set_output_color_profile(icc_profile);
641        assert!(result.is_err());
642    }
643
644    #[test]
645    fn test_default_output_tf_by_pixel_format() {
646        use crate::api::{JxlColorEncoding, JxlTransferFunction};
647
648        // Using test image with ICC profile to trigger default transfer function path
649        let file = std::fs::read("resources/test/lossy_with_icc.jxl").unwrap();
650        let options = JxlDecoderOptions::default();
651        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
652        let mut input = file.as_slice();
653        let mut decoder = loop {
654            match decoder.process(&mut input).unwrap() {
655                ProcessingResult::Complete { result } => break result,
656                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
657            }
658        };
659
660        // Output data format will default to F32, so output color profile will be linear sRGB
661        assert_eq!(
662            *decoder.output_color_profile().transfer_function().unwrap(),
663            JxlTransferFunction::Linear,
664        );
665
666        // Integer data format will set output color profile to sRGB
667        decoder.set_pixel_format(JxlPixelFormat::rgba8(0));
668        assert_eq!(
669            *decoder.output_color_profile().transfer_function().unwrap(),
670            JxlTransferFunction::SRGB,
671        );
672
673        decoder.set_pixel_format(JxlPixelFormat::rgba_f16(0));
674        assert_eq!(
675            *decoder.output_color_profile().transfer_function().unwrap(),
676            JxlTransferFunction::Linear,
677        );
678
679        decoder.set_pixel_format(JxlPixelFormat::rgba16(0));
680        assert_eq!(
681            *decoder.output_color_profile().transfer_function().unwrap(),
682            JxlTransferFunction::SRGB,
683        );
684
685        // Once output color profile is set by user, it will remain as is regardless of what pixel
686        // format is set
687        let profile = JxlColorProfile::Simple(JxlColorEncoding::srgb(false));
688        decoder.set_output_color_profile(profile.clone()).unwrap();
689        decoder.set_pixel_format(JxlPixelFormat::rgba_f16(0));
690        assert!(decoder.output_color_profile() == &profile);
691    }
692
693    #[test]
694    fn test_fill_opaque_alpha_both_pipelines() {
695        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
696        use crate::image::{Image, Rect};
697
698        // Use basic.jxl which has no alpha channel
699        let file = std::fs::read("resources/test/basic.jxl").unwrap();
700
701        // Request RGBA format even though image has no alpha
702        let rgba_format = JxlPixelFormat {
703            color_type: JxlColorType::Rgba,
704            color_data_format: Some(JxlDataFormat::f32()),
705            extra_channel_format: vec![],
706        };
707
708        // Test both pipelines (simple and low-memory)
709        for use_simple in [true, false] {
710            let options = JxlDecoderOptions::default();
711            let decoder = JxlDecoder::<states::Initialized>::new(options);
712            let mut input = file.as_slice();
713
714            // Advance to image info
715            macro_rules! advance_decoder {
716                ($decoder:expr) => {
717                    loop {
718                        match $decoder.process(&mut input).unwrap() {
719                            ProcessingResult::Complete { result } => break result,
720                            ProcessingResult::NeedsMoreInput { fallback, .. } => {
721                                if input.is_empty() {
722                                    panic!("Unexpected end of input");
723                                }
724                                $decoder = fallback;
725                            }
726                        }
727                    }
728                };
729                ($decoder:expr, $buffers:expr) => {
730                    loop {
731                        match $decoder.process(&mut input, $buffers).unwrap() {
732                            ProcessingResult::Complete { result } => break result,
733                            ProcessingResult::NeedsMoreInput { fallback, .. } => {
734                                if input.is_empty() {
735                                    panic!("Unexpected end of input");
736                                }
737                                $decoder = fallback;
738                            }
739                        }
740                    }
741                };
742            }
743
744            let mut decoder = decoder;
745            let mut decoder = advance_decoder!(decoder);
746            decoder.set_use_simple_pipeline(use_simple);
747
748            // Set RGBA format
749            decoder.set_pixel_format(rgba_format.clone());
750
751            let basic_info = decoder.basic_info().clone();
752            let (width, height) = basic_info.size;
753
754            // Advance to frame info
755            let mut decoder = advance_decoder!(decoder);
756
757            // Prepare buffer for RGBA (4 channels interleaved)
758            let mut color_buffer = Image::<f32>::new((width * 4, height)).unwrap();
759            let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
760                color_buffer
761                    .get_rect_mut(Rect {
762                        origin: (0, 0),
763                        size: (width * 4, height),
764                    })
765                    .into_raw(),
766            )];
767
768            // Decode frame
769            let _decoder = advance_decoder!(decoder, &mut buffers);
770
771            // Verify all alpha values are 1.0 (opaque)
772            for y in 0..height {
773                let row = color_buffer.row(y);
774                for x in 0..width {
775                    let alpha = row[x * 4 + 3];
776                    assert_eq!(
777                        alpha, 1.0,
778                        "Alpha at ({},{}) should be 1.0, got {} (use_simple={})",
779                        x, y, alpha, use_simple
780                    );
781                }
782            }
783        }
784    }
785
786    /// Test that premultiply_output=true produces premultiplied alpha output
787    /// from a source with straight (non-premultiplied) alpha.
788    #[test]
789    fn test_premultiply_output_straight_alpha() {
790        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
791
792        // Use alpha_nonpremultiplied.jxl which has straight alpha (alpha_associated=false)
793        let file =
794            std::fs::read("resources/test/conformance_test_images/alpha_nonpremultiplied.jxl")
795                .unwrap();
796
797        // Alpha is included in RGBA, so we set extra_channel_format to None
798        // to indicate no separate buffer for the alpha extra channel
799        let rgba_format = JxlPixelFormat {
800            color_type: JxlColorType::Rgba,
801            color_data_format: Some(JxlDataFormat::f32()),
802            extra_channel_format: vec![None],
803        };
804
805        // Test both pipelines
806        for use_simple in [true, false] {
807            let (straight_buffer, width, height) =
808                decode_with_format::<f32>(&file, &rgba_format, use_simple, false);
809            let (premul_buffer, _, _) =
810                decode_with_format::<f32>(&file, &rgba_format, use_simple, true);
811
812            // Verify premultiplied values: premul_rgb should equal straight_rgb * alpha
813            let mut found_semitransparent = false;
814            for y in 0..height {
815                let straight_row = straight_buffer.row(y);
816                let premul_row = premul_buffer.row(y);
817                for x in 0..width {
818                    let sr = straight_row[x * 4];
819                    let sg = straight_row[x * 4 + 1];
820                    let sb = straight_row[x * 4 + 2];
821                    let sa = straight_row[x * 4 + 3];
822
823                    let pr = premul_row[x * 4];
824                    let pg = premul_row[x * 4 + 1];
825                    let pb = premul_row[x * 4 + 2];
826                    let pa = premul_row[x * 4 + 3];
827
828                    // Alpha should be unchanged
829                    assert!(
830                        (sa - pa).abs() < 1e-5,
831                        "Alpha mismatch at ({},{}): straight={}, premul={} (use_simple={})",
832                        x,
833                        y,
834                        sa,
835                        pa,
836                        use_simple
837                    );
838
839                    // Check premultiplication: premul = straight * alpha
840                    let expected_r = sr * sa;
841                    let expected_g = sg * sa;
842                    let expected_b = sb * sa;
843
844                    // Allow 1% tolerance for precision differences between pipelines
845                    let tol = 0.01;
846                    assert!(
847                        (expected_r - pr).abs() < tol,
848                        "R mismatch at ({},{}): expected={}, got={} (use_simple={})",
849                        x,
850                        y,
851                        expected_r,
852                        pr,
853                        use_simple
854                    );
855                    assert!(
856                        (expected_g - pg).abs() < tol,
857                        "G mismatch at ({},{}): expected={}, got={} (use_simple={})",
858                        x,
859                        y,
860                        expected_g,
861                        pg,
862                        use_simple
863                    );
864                    assert!(
865                        (expected_b - pb).abs() < tol,
866                        "B mismatch at ({},{}): expected={}, got={} (use_simple={})",
867                        x,
868                        y,
869                        expected_b,
870                        pb,
871                        use_simple
872                    );
873
874                    if sa > 0.01 && sa < 0.99 {
875                        found_semitransparent = true;
876                    }
877                }
878            }
879
880            // Ensure the test image actually has some semi-transparent pixels
881            assert!(
882                found_semitransparent,
883                "Test image should have semi-transparent pixels (use_simple={})",
884                use_simple
885            );
886        }
887    }
888
889    /// Test that premultiply_output=true doesn't double-premultiply
890    /// when the source already has premultiplied alpha (alpha_associated=true).
891    #[test]
892    fn test_premultiply_output_already_premultiplied() {
893        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
894
895        // Use alpha_premultiplied.jxl which has alpha_associated=true
896        let file = std::fs::read("resources/test/conformance_test_images/alpha_premultiplied.jxl")
897            .unwrap();
898
899        // Alpha is included in RGBA, so we set extra_channel_format to None
900        let rgba_format = JxlPixelFormat {
901            color_type: JxlColorType::Rgba,
902            color_data_format: Some(JxlDataFormat::f32()),
903            extra_channel_format: vec![None],
904        };
905
906        // Test both pipelines
907        for use_simple in [true, false] {
908            let (without_flag_buffer, width, height) =
909                decode_with_format::<f32>(&file, &rgba_format, use_simple, false);
910            let (with_flag_buffer, _, _) =
911                decode_with_format::<f32>(&file, &rgba_format, use_simple, true);
912
913            // Both outputs should be identical since source is already premultiplied
914            // and we shouldn't double-premultiply
915            for y in 0..height {
916                let without_row = without_flag_buffer.row(y);
917                let with_row = with_flag_buffer.row(y);
918                for x in 0..width {
919                    for c in 0..4 {
920                        let without_val = without_row[x * 4 + c];
921                        let with_val = with_row[x * 4 + c];
922                        assert!(
923                            (without_val - with_val).abs() < 1e-5,
924                            "Mismatch at ({},{}) channel {}: without_flag={}, with_flag={} (use_simple={})",
925                            x,
926                            y,
927                            c,
928                            without_val,
929                            with_val,
930                            use_simple
931                        );
932                    }
933                }
934            }
935        }
936    }
937
938    /// Test that animations with reference frames work correctly.
939    /// This exercises the buffer index calculation fix where reference frame
940    /// save stages use indices beyond the API-provided buffer array.
941    #[test]
942    fn test_animation_with_reference_frames() {
943        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
944        use crate::image::{Image, Rect};
945
946        // Use animation_spline.jxl which has multiple frames with references
947        let file =
948            std::fs::read("resources/test/conformance_test_images/animation_spline.jxl").unwrap();
949
950        let options = JxlDecoderOptions::default();
951        let decoder = JxlDecoder::<states::Initialized>::new(options);
952        let mut input = file.as_slice();
953
954        // Advance to image info
955        let mut decoder = decoder;
956        let mut decoder = loop {
957            match decoder.process(&mut input).unwrap() {
958                ProcessingResult::Complete { result } => break result,
959                ProcessingResult::NeedsMoreInput { fallback, .. } => {
960                    decoder = fallback;
961                }
962            }
963        };
964
965        // Set RGB format with no extra channels
966        let rgb_format = JxlPixelFormat {
967            color_type: JxlColorType::Rgb,
968            color_data_format: Some(JxlDataFormat::f32()),
969            extra_channel_format: vec![],
970        };
971        decoder.set_pixel_format(rgb_format);
972
973        let basic_info = decoder.basic_info().clone();
974        let (width, height) = basic_info.size;
975
976        let mut frame_count = 0;
977
978        // Decode all frames
979        loop {
980            // Advance to frame info
981            let mut decoder_frame = loop {
982                match decoder.process(&mut input).unwrap() {
983                    ProcessingResult::Complete { result } => break result,
984                    ProcessingResult::NeedsMoreInput { fallback, .. } => {
985                        decoder = fallback;
986                    }
987                }
988            };
989
990            // Prepare buffer for RGB (3 channels interleaved)
991            let mut color_buffer = Image::<f32>::new((width * 3, height)).unwrap();
992            let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
993                color_buffer
994                    .get_rect_mut(Rect {
995                        origin: (0, 0),
996                        size: (width * 3, height),
997                    })
998                    .into_raw(),
999            )];
1000
1001            // Decode frame - this should not panic even though reference frame
1002            // save stages target buffer indices beyond buffers.len()
1003            decoder = loop {
1004                match decoder_frame.process(&mut input, &mut buffers).unwrap() {
1005                    ProcessingResult::Complete { result } => break result,
1006                    ProcessingResult::NeedsMoreInput { fallback, .. } => {
1007                        decoder_frame = fallback;
1008                    }
1009                }
1010            };
1011
1012            frame_count += 1;
1013
1014            // Check if there are more frames
1015            if !decoder.has_more_frames() {
1016                break;
1017            }
1018        }
1019
1020        // Verify we decoded multiple frames
1021        assert!(
1022            frame_count > 1,
1023            "Expected multiple frames in animation, got {}",
1024            frame_count
1025        );
1026    }
1027
1028    #[test]
1029    fn test_skip_frame_then_decode_next() {
1030        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
1031        use crate::image::{Image, Rect};
1032
1033        // Use animation_spline.jxl which has multiple frames
1034        let file =
1035            std::fs::read("resources/test/conformance_test_images/animation_spline.jxl").unwrap();
1036
1037        let options = JxlDecoderOptions::default();
1038        let decoder = JxlDecoder::<states::Initialized>::new(options);
1039        let mut input = file.as_slice();
1040
1041        // Advance to image info
1042        let mut decoder = decoder;
1043        let mut decoder = loop {
1044            match decoder.process(&mut input).unwrap() {
1045                ProcessingResult::Complete { result } => break result,
1046                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1047                    decoder = fallback;
1048                }
1049            }
1050        };
1051
1052        // Set RGB format
1053        let rgb_format = JxlPixelFormat {
1054            color_type: JxlColorType::Rgb,
1055            color_data_format: Some(JxlDataFormat::f32()),
1056            extra_channel_format: vec![],
1057        };
1058        decoder.set_pixel_format(rgb_format);
1059
1060        let basic_info = decoder.basic_info().clone();
1061        let (width, height) = basic_info.size;
1062
1063        // Advance to frame info for first frame
1064        let mut decoder_frame = loop {
1065            match decoder.process(&mut input).unwrap() {
1066                ProcessingResult::Complete { result } => break result,
1067                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1068                    decoder = fallback;
1069                }
1070            }
1071        };
1072
1073        // Skip the first frame (this is where the bug would leave stale frame state)
1074        let mut decoder = loop {
1075            match decoder_frame.skip_frame(&mut input).unwrap() {
1076                ProcessingResult::Complete { result } => break result,
1077                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1078                    decoder_frame = fallback;
1079                }
1080            }
1081        };
1082
1083        assert!(
1084            decoder.has_more_frames(),
1085            "Animation should have more frames"
1086        );
1087
1088        // Advance to frame info for second frame
1089        // Without the fix, this would panic at assert!(self.frame.is_none())
1090        let mut decoder_frame = loop {
1091            match decoder.process(&mut input).unwrap() {
1092                ProcessingResult::Complete { result } => break result,
1093                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1094                    decoder = fallback;
1095                }
1096            }
1097        };
1098
1099        // Decode the second frame to verify everything works
1100        let mut color_buffer = Image::<f32>::new((width * 3, height)).unwrap();
1101        let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
1102            color_buffer
1103                .get_rect_mut(Rect {
1104                    origin: (0, 0),
1105                    size: (width * 3, height),
1106                })
1107                .into_raw(),
1108        )];
1109
1110        let decoder = loop {
1111            match decoder_frame.process(&mut input, &mut buffers).unwrap() {
1112                ProcessingResult::Complete { result } => break result,
1113                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1114                    decoder_frame = fallback;
1115                }
1116            }
1117        };
1118
1119        // If we got here without panicking, the fix works
1120        // Optionally verify we can continue with more frames
1121        let _ = decoder.has_more_frames();
1122    }
1123
1124    /// Test that u8 output matches f32 output within quantization tolerance.
1125    /// This test would catch bugs like the offset miscalculation in PR #586
1126    /// that caused black bars in u8 output.
1127    #[test]
1128    fn test_output_format_u8_matches_f32() {
1129        use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
1130
1131        // Use bicycles.jxl - a larger image that exercises offset calculations
1132        let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1133
1134        // Test both RGB and BGRA to catch channel reordering bugs
1135        for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] {
1136            let f32_format = JxlPixelFormat {
1137                color_type,
1138                color_data_format: Some(JxlDataFormat::f32()),
1139                extra_channel_format: vec![],
1140            };
1141            let u8_format = JxlPixelFormat {
1142                color_type,
1143                color_data_format: Some(JxlDataFormat::U8 { bit_depth: 8 }),
1144                extra_channel_format: vec![],
1145            };
1146
1147            // Test both pipelines
1148            for use_simple in [true, false] {
1149                let (f32_buffer, width, height) =
1150                    decode_with_format::<f32>(&file, &f32_format, use_simple, false);
1151                let (u8_buffer, _, _) =
1152                    decode_with_format::<u8>(&file, &u8_format, use_simple, false);
1153
1154                // Compare values: u8 / 255.0 should match f32
1155                // Tolerance: quantization error of ±0.5/255 ≈ 0.00196 plus small rounding
1156                let tolerance = 0.003;
1157                let mut max_error: f32 = 0.0;
1158
1159                for y in 0..height {
1160                    let f32_row = f32_buffer.row(y);
1161                    let u8_row = u8_buffer.row(y);
1162                    for x in 0..(width * num_samples) {
1163                        let f32_val = f32_row[x].clamp(0.0, 1.0);
1164                        let u8_val = u8_row[x] as f32 / 255.0;
1165                        let error = (f32_val - u8_val).abs();
1166                        max_error = max_error.max(error);
1167                        assert!(
1168                            error < tolerance,
1169                            "{:?} u8 mismatch at ({},{}): f32={}, u8={} (scaled={}), error={} (use_simple={})",
1170                            color_type,
1171                            x,
1172                            y,
1173                            f32_val,
1174                            u8_row[x],
1175                            u8_val,
1176                            error,
1177                            use_simple
1178                        );
1179                    }
1180                }
1181            }
1182        }
1183    }
1184
1185    /// Test that u16 output matches f32 output within quantization tolerance.
1186    #[test]
1187    fn test_output_format_u16_matches_f32() {
1188        use crate::api::{Endianness, JxlColorType, JxlDataFormat, JxlPixelFormat};
1189
1190        let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1191
1192        // Test both RGB and BGRA
1193        for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] {
1194            let f32_format = JxlPixelFormat {
1195                color_type,
1196                color_data_format: Some(JxlDataFormat::f32()),
1197                extra_channel_format: vec![],
1198            };
1199            let u16_format = JxlPixelFormat {
1200                color_type,
1201                color_data_format: Some(JxlDataFormat::U16 {
1202                    endianness: Endianness::native(),
1203                    bit_depth: 16,
1204                }),
1205                extra_channel_format: vec![],
1206            };
1207
1208            for use_simple in [true, false] {
1209                let (f32_buffer, width, height) =
1210                    decode_with_format::<f32>(&file, &f32_format, use_simple, false);
1211                let (u16_buffer, _, _) =
1212                    decode_with_format::<u16>(&file, &u16_format, use_simple, false);
1213
1214                // Tolerance: quantization error of ±0.5/65535 plus small rounding
1215                let tolerance = 0.0001;
1216
1217                for y in 0..height {
1218                    let f32_row = f32_buffer.row(y);
1219                    let u16_row = u16_buffer.row(y);
1220                    for x in 0..(width * num_samples) {
1221                        let f32_val = f32_row[x].clamp(0.0, 1.0);
1222                        let u16_val = u16_row[x] as f32 / 65535.0;
1223                        let error = (f32_val - u16_val).abs();
1224                        assert!(
1225                            error < tolerance,
1226                            "{:?} u16 mismatch at ({},{}): f32={}, u16={} (scaled={}), error={} (use_simple={})",
1227                            color_type,
1228                            x,
1229                            y,
1230                            f32_val,
1231                            u16_row[x],
1232                            u16_val,
1233                            error,
1234                            use_simple
1235                        );
1236                    }
1237                }
1238            }
1239        }
1240    }
1241
1242    /// Test that f16 output matches f32 output within f16 precision tolerance.
1243    #[test]
1244    fn test_output_format_f16_matches_f32() {
1245        use crate::api::{Endianness, JxlColorType, JxlDataFormat, JxlPixelFormat};
1246        use crate::util::f16;
1247
1248        let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1249
1250        // Test both RGB and BGRA
1251        for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] {
1252            let f32_format = JxlPixelFormat {
1253                color_type,
1254                color_data_format: Some(JxlDataFormat::f32()),
1255                extra_channel_format: vec![],
1256            };
1257            let f16_format = JxlPixelFormat {
1258                color_type,
1259                color_data_format: Some(JxlDataFormat::F16 {
1260                    endianness: Endianness::native(),
1261                }),
1262                extra_channel_format: vec![],
1263            };
1264
1265            for use_simple in [true, false] {
1266                let (f32_buffer, width, height) =
1267                    decode_with_format::<f32>(&file, &f32_format, use_simple, false);
1268                let (f16_buffer, _, _) =
1269                    decode_with_format::<f16>(&file, &f16_format, use_simple, false);
1270
1271                // f16 has about 3 decimal digits of precision
1272                // For values in [0,1], the relative error is about 0.001
1273                let tolerance = 0.002;
1274
1275                for y in 0..height {
1276                    let f32_row = f32_buffer.row(y);
1277                    let f16_row = f16_buffer.row(y);
1278                    for x in 0..(width * num_samples) {
1279                        let f32_val = f32_row[x];
1280                        let f16_val = f16_row[x].to_f32();
1281                        let error = (f32_val - f16_val).abs();
1282                        assert!(
1283                            error < tolerance,
1284                            "{:?} f16 mismatch at ({},{}): f32={}, f16={}, error={} (use_simple={})",
1285                            color_type,
1286                            x,
1287                            y,
1288                            f32_val,
1289                            f16_val,
1290                            error,
1291                            use_simple
1292                        );
1293                    }
1294                }
1295            }
1296        }
1297    }
1298
1299    /// Helper function to decode an image with a specific format.
1300    fn decode_with_format<T: crate::image::ImageDataType>(
1301        file: &[u8],
1302        pixel_format: &JxlPixelFormat,
1303        use_simple: bool,
1304        premultiply: bool,
1305    ) -> (Image<T>, usize, usize) {
1306        let options = JxlDecoderOptions {
1307            premultiply_output: premultiply,
1308            ..Default::default()
1309        };
1310        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1311        let mut input = file;
1312
1313        // Advance to image info
1314        let mut decoder = loop {
1315            match decoder.process(&mut input).unwrap() {
1316                ProcessingResult::Complete { result } => break result,
1317                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1318                    if input.is_empty() {
1319                        panic!("Unexpected end of input");
1320                    }
1321                    decoder = fallback;
1322                }
1323            }
1324        };
1325        decoder.set_use_simple_pipeline(use_simple);
1326        decoder.set_pixel_format(pixel_format.clone());
1327
1328        let basic_info = decoder.basic_info().clone();
1329        let (width, height) = basic_info.size;
1330
1331        let num_samples = pixel_format.color_type.samples_per_pixel();
1332
1333        // Advance to frame info
1334        let decoder = loop {
1335            match decoder.process(&mut input).unwrap() {
1336                ProcessingResult::Complete { result } => break result,
1337                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1338                    if input.is_empty() {
1339                        panic!("Unexpected end of input");
1340                    }
1341                    decoder = fallback;
1342                }
1343            }
1344        };
1345
1346        let mut buffer = Image::<T>::new((width * num_samples, height)).unwrap();
1347        let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
1348            buffer
1349                .get_rect_mut(Rect {
1350                    origin: (0, 0),
1351                    size: (width * num_samples, height),
1352                })
1353                .into_raw(),
1354        )];
1355
1356        // Decode
1357        let mut decoder = decoder;
1358        loop {
1359            match decoder.process(&mut input, &mut buffers).unwrap() {
1360                ProcessingResult::Complete { .. } => break,
1361                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1362                    if input.is_empty() {
1363                        panic!("Unexpected end of input");
1364                    }
1365                    decoder = fallback;
1366                }
1367            }
1368        }
1369
1370        (buffer, width, height)
1371    }
1372
1373    /// Regression test for ClusterFuzz issue 5342436251336704
1374    /// Tests that malformed JXL files with overflow-inducing data don't panic
1375    #[test]
1376    fn test_fuzzer_smallbuffer_overflow() {
1377        use std::panic;
1378
1379        let data = include_bytes!("../../tests/testdata/fuzzer_smallbuffer_overflow.jxl");
1380
1381        // The test passes if it doesn't panic with "attempt to add with overflow"
1382        // It's OK if it returns an error or panics with "Unexpected end of input"
1383        let result = panic::catch_unwind(|| {
1384            let _ = decode(data, 1024, false, false, None);
1385        });
1386
1387        // If it panicked, make sure it wasn't an overflow panic
1388        if let Err(e) = result {
1389            let panic_msg = e
1390                .downcast_ref::<&str>()
1391                .map(|s| s.to_string())
1392                .or_else(|| e.downcast_ref::<String>().cloned())
1393                .unwrap_or_default();
1394            assert!(
1395                !panic_msg.contains("overflow"),
1396                "Unexpected overflow panic: {}",
1397                panic_msg
1398            );
1399        }
1400    }
1401
1402    /// Helper to wrap a bare codestream in a JXL container with a jxli frame index box.
1403    fn wrap_with_frame_index(
1404        codestream: &[u8],
1405        tnum: u32,
1406        tden: u32,
1407        entries: &[(u64, u64, u64)], // (OFF_delta, T, F)
1408    ) -> Vec<u8> {
1409        use crate::util::test::build_frame_index_content;
1410
1411        fn make_box(ty: &[u8; 4], content: &[u8]) -> Vec<u8> {
1412            let len = (8 + content.len()) as u32;
1413            let mut buf = Vec::new();
1414            buf.extend(len.to_be_bytes());
1415            buf.extend(ty);
1416            buf.extend(content);
1417            buf
1418        }
1419
1420        let jxli_content = build_frame_index_content(tnum, tden, entries);
1421
1422        // JXL signature box
1423        let sig = [
1424            0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a,
1425        ];
1426        // ftyp box
1427        let ftyp = make_box(b"ftyp", b"jxl \x00\x00\x00\x00jxl ");
1428        let jxli = make_box(b"jxli", &jxli_content);
1429        let jxlc = make_box(b"jxlc", codestream);
1430
1431        let mut container = Vec::new();
1432        container.extend(&sig);
1433        container.extend(&ftyp);
1434        container.extend(&jxli);
1435        container.extend(&jxlc);
1436        container
1437    }
1438
1439    #[test]
1440    fn test_frame_index_parsed_from_container() {
1441        // Read a bare animation codestream and wrap it in a container with a jxli box.
1442        let codestream =
1443            std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
1444
1445        // Create synthetic frame index entries (delta offsets).
1446        // These are synthetic -- we don't know real frame offsets, but we can verify parsing.
1447        let entries = vec![
1448            (0u64, 100u64, 1u64), // Frame 0 at offset 0
1449            (500, 100, 1),        // Frame 1 at offset 500
1450            (600, 100, 1),        // Frame 2 at offset 1100
1451        ];
1452
1453        let container = wrap_with_frame_index(&codestream, 1, 1000, &entries);
1454
1455        // Decode with a large chunk size so the jxli box is fully consumed.
1456        let options = JxlDecoderOptions::default();
1457        let mut dec = JxlDecoder::<states::Initialized>::new(options);
1458        let mut input: &[u8] = &container;
1459        let dec = loop {
1460            match dec.process(&mut input).unwrap() {
1461                ProcessingResult::Complete { result } => break result,
1462                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1463                    if input.is_empty() {
1464                        panic!("Unexpected end of input");
1465                    }
1466                    dec = fallback;
1467                }
1468            }
1469        };
1470
1471        // Check that frame index was parsed.
1472        let fi = dec.frame_index().expect("frame_index should be Some");
1473        assert_eq!(fi.num_frames(), 3);
1474        assert_eq!(fi.tnum, 1);
1475        assert_eq!(fi.tden.get(), 1000);
1476        // Verify absolute offsets (accumulated from deltas)
1477        assert_eq!(fi.entries[0].codestream_offset, 0);
1478        assert_eq!(fi.entries[1].codestream_offset, 500);
1479        assert_eq!(fi.entries[2].codestream_offset, 1100);
1480        assert_eq!(fi.entries[0].duration_ticks, 100);
1481        assert_eq!(fi.entries[2].frame_count, 1);
1482    }
1483
1484    #[test]
1485    fn test_frame_index_none_for_bare_codestream() {
1486        // A bare codestream has no container, so no frame index.
1487        let data =
1488            std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
1489        let options = JxlDecoderOptions::default();
1490        let mut dec = JxlDecoder::<states::Initialized>::new(options);
1491        let mut input: &[u8] = &data;
1492        let dec = loop {
1493            match dec.process(&mut input).unwrap() {
1494                ProcessingResult::Complete { result } => break result,
1495                ProcessingResult::NeedsMoreInput { fallback, .. } => {
1496                    if input.is_empty() {
1497                        panic!("Unexpected end of input");
1498                    }
1499                    dec = fallback;
1500                }
1501            }
1502        };
1503        assert!(dec.frame_index().is_none());
1504    }
1505
1506    /// Regression test for Chromium ClusterFuzz issue 474401148.
1507    #[test]
1508    fn test_fuzzer_xyb_icc_no_panic() {
1509        use crate::api::ProcessingResult;
1510
1511        #[rustfmt::skip]
1512        let data: &[u8] = &[
1513            0xff, 0x0a, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00,
1514            0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x25, 0x00,
1515        ];
1516
1517        let opts = JxlDecoderOptions::default();
1518        let mut decoder = JxlDecoderInner::new(opts);
1519        let mut input = data;
1520
1521        if let Ok(ProcessingResult::Complete { .. }) = decoder.process(&mut input, None)
1522            && let Some(profile) = decoder.output_color_profile()
1523        {
1524            let _ = profile.try_as_icc();
1525        }
1526    }
1527
1528    #[test]
1529    fn test_pixel_limit_enforcement() {
1530        // Load a test image - green_queen is 256x256 = 65536 pixels
1531        let input = std::fs::read("resources/test/green_queen_vardct_e3.jxl").unwrap();
1532
1533        // Create options with a very restrictive pixel limit (smaller than the image)
1534        let mut options = JxlDecoderOptions::default();
1535        options.limits.max_pixels = Some(100); // Only 100 pixels allowed
1536
1537        let decoder = JxlDecoder::<states::Initialized>::new(options);
1538        let mut input_slice = &input[..];
1539
1540        // The decoder should fail when parsing the header with LimitExceeded error
1541        let result = decoder.process(&mut input_slice);
1542        match result {
1543            Err(err) => {
1544                assert!(
1545                    matches!(
1546                        err,
1547                        Error::LimitExceeded {
1548                            resource: "pixels",
1549                            ..
1550                        }
1551                    ),
1552                    "Expected LimitExceeded for pixels, got {:?}",
1553                    err
1554                );
1555            }
1556            Ok(ProcessingResult::NeedsMoreInput { .. }) => {
1557                panic!("Expected error, got needs more input");
1558            }
1559            Ok(ProcessingResult::Complete { .. }) => {
1560                panic!("Expected error, got success");
1561            }
1562        }
1563    }
1564
1565    #[test]
1566    fn test_restrictive_limits_preset() {
1567        // Verify the restrictive preset is reasonable
1568        let limits = crate::api::JxlDecoderLimits::restrictive();
1569        assert_eq!(limits.max_pixels, Some(100_000_000));
1570        assert_eq!(limits.max_extra_channels, Some(16));
1571        assert_eq!(limits.max_icc_size, Some(1 << 20));
1572        assert_eq!(limits.max_tree_size, Some(1 << 20));
1573        assert_eq!(limits.max_patches, Some(1 << 16));
1574        assert_eq!(limits.max_spline_points, Some(1 << 16));
1575        assert_eq!(limits.max_reference_frames, Some(2));
1576        assert_eq!(limits.max_memory_bytes, Some(1 << 30));
1577    }
1578
1579    #[test]
1580    fn test_extra_channel_metadata() {
1581        use crate::headers::extra_channels::ExtraChannel;
1582
1583        let file = std::fs::read("resources/test/extra_channels.jxl").unwrap();
1584        let options = JxlDecoderOptions::default();
1585        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1586        let mut input = file.as_slice();
1587        let decoder = loop {
1588            match decoder.process(&mut input).unwrap() {
1589                ProcessingResult::Complete { result } => break result,
1590                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
1591            }
1592        };
1593        let info = decoder.basic_info();
1594        // extra_channels.jxl should have at least one extra channel
1595        assert!(
1596            !info.extra_channels.is_empty(),
1597            "expected at least one extra channel"
1598        );
1599
1600        // Verify all new fields are populated
1601        for ec in &info.extra_channels {
1602            // bits_per_sample should be a reasonable value
1603            assert!(
1604                ec.bits_per_sample > 0 && ec.bits_per_sample <= 32,
1605                "unexpected bits_per_sample: {}",
1606                ec.bits_per_sample
1607            );
1608            // dim_shift should be <= 3
1609            assert!(ec.dim_shift <= 3, "unexpected dim_shift: {}", ec.dim_shift);
1610        }
1611    }
1612
1613    #[test]
1614    fn test_extra_channel_alpha_with_new_fields() {
1615        use crate::headers::extra_channels::ExtraChannel;
1616
1617        // 3x3a has alpha
1618        let file = std::fs::read("resources/test/3x3a_srgb_lossless.jxl").unwrap();
1619        let options = JxlDecoderOptions::default();
1620        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1621        let mut input = file.as_slice();
1622        let decoder = loop {
1623            match decoder.process(&mut input).unwrap() {
1624                ProcessingResult::Complete { result } => break result,
1625                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
1626            }
1627        };
1628        let info = decoder.basic_info();
1629        // Should have exactly one extra channel of type Alpha
1630        assert_eq!(info.extra_channels.len(), 1);
1631        let alpha = &info.extra_channels[0];
1632        assert_eq!(alpha.ec_type, ExtraChannel::Alpha);
1633        assert!(alpha.bits_per_sample > 0);
1634        // Default alpha channels typically have dim_shift 0 (full resolution)
1635        assert_eq!(alpha.dim_shift, 0);
1636    }
1637
1638    #[test]
1639    fn test_preview_metadata_in_basic_info() {
1640        // with_preview.jxl has a preview; basic.jxl does not
1641        let file = std::fs::read("resources/test/with_preview.jxl").unwrap();
1642        let options = JxlDecoderOptions::default();
1643        let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1644        let mut input = file.as_slice();
1645        let decoder = loop {
1646            match decoder.process(&mut input).unwrap() {
1647                ProcessingResult::Complete { result } => break result,
1648                ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
1649            }
1650        };
1651        let info = decoder.basic_info();
1652        let (pw, ph) = info.preview_size.expect("expected preview_size");
1653        assert!(pw > 0 && ph > 0, "preview dimensions should be positive");
1654    }
1655
1656    #[test]
1657    fn test_stop_cancellation() {
1658        use almost_enough::Stopper;
1659        use enough::Stop;
1660
1661        let stop = Stopper::new();
1662        assert!(!stop.should_stop());
1663        stop.cancel();
1664        assert!(stop.should_stop());
1665        // Verify it integrates with our error type
1666        let result: crate::error::Result<()> = stop.check().map_err(Into::into);
1667        assert!(matches!(result, Err(crate::error::Error::Cancelled)));
1668    }
1669}