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