1use 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
30pub 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 #[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 #[cfg(feature = "jpeg")]
65 pub fn take_jpeg_reconstruction(&mut self) -> Option<Vec<u8>> {
66 self.inner.take_jpeg_reconstruction()
67 }
68
69 pub fn frame_index(&self) -> Option<&FrameIndexBox> {
75 self.inner.frame_index()
76 }
77
78 pub fn gain_map(&self) -> Option<&GainMapBundle> {
85 self.inner.gain_map()
86 }
87
88 pub fn take_gain_map(&mut self) -> Option<GainMapBundle> {
91 self.inner.take_gain_map()
92 }
93
94 pub fn exif(&self) -> Option<&[u8]> {
103 self.inner.exif()
104 }
105
106 pub fn take_exif(&mut self) -> Option<Vec<u8>> {
108 self.inner.take_exif()
109 }
110
111 pub fn xmp(&self) -> Option<&[u8]> {
118 self.inner.xmp()
119 }
120
121 pub fn take_xmp(&mut self) -> Option<Vec<u8>> {
123 self.inner.take_xmp()
124 }
125
126 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 pub fn basic_info(&self) -> &JxlBasicInfo {
169 self.inner.basic_info().unwrap()
170 }
171
172 pub fn embedded_color_profile(&self) -> &JxlColorProfile {
174 self.inner.embedded_color_profile().unwrap()
175 }
176
177 pub fn output_color_profile(&self) -> &JxlColorProfile {
179 self.inner.output_color_profile().unwrap()
180 }
181
182 pub fn set_output_color_profile(&mut self, profile: JxlColorProfile) -> Result<()> {
185 self.inner.set_output_color_profile(profile)
186 }
187
188 pub fn current_pixel_format(&self) -> &JxlPixelFormat {
190 self.inner.current_pixel_format().unwrap()
191 }
192
193 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 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 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 pub fn num_completed_passes(&self) -> usize {
242 self.inner.num_completed_passes().unwrap()
243 }
244
245 pub fn flush_pixels(&mut self, buffers: &mut [JxlOutputBuffer<'_>]) -> Result<()> {
249 self.inner.flush_pixels(buffers)
250 }
251
252 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 mut options = JxlDecoderOptions::default();
302 options.limits.max_memory_bytes = None;
305 let mut initialized_decoder = JxlDecoder::<states::Initialized>::new(options);
306
307 if let Some(callback) = callback {
308 initialized_decoder.set_frame_callback(callback);
309 }
310
311 let mut chunk_input = &input[0..0];
312
313 macro_rules! advance_decoder {
314 ($decoder: ident $(, $extra_arg: expr)? $(; $flush_arg: expr)?) => {
315 loop {
316 chunk_input =
317 &input[..(chunk_input.len().saturating_add(chunk_size)).min(input.len())];
318 let available_before = chunk_input.len();
319 let process_result = $decoder.process(&mut chunk_input $(, $extra_arg)?);
320 input = &input[(available_before - chunk_input.len())..];
321 match process_result.unwrap() {
322 ProcessingResult::Complete { result } => break result,
323 ProcessingResult::NeedsMoreInput { fallback, .. } => {
324 $(
325 let mut fallback = fallback;
326 if do_flush && !input.is_empty() {
327 fallback.flush_pixels($flush_arg)?;
328 }
329 )?
330 if input.is_empty() {
331 panic!("Unexpected end of input");
332 }
333 $decoder = fallback;
334 }
335 }
336 }
337 };
338 }
339
340 let mut decoder_with_image_info = advance_decoder!(initialized_decoder);
342 decoder_with_image_info.set_use_simple_pipeline(use_simple_pipeline);
343
344 let basic_info = decoder_with_image_info.basic_info().clone();
346 assert!(basic_info.bit_depth.bits_per_sample() > 0);
347
348 let (buffer_width, buffer_height) = basic_info.size;
350 assert!(buffer_width > 0);
351 assert!(buffer_height > 0);
352
353 let default_format = decoder_with_image_info.current_pixel_format();
355 let requested_format = JxlPixelFormat {
356 color_type: default_format.color_type,
357 color_data_format: Some(JxlDataFormat::f32()),
358 extra_channel_format: default_format
359 .extra_channel_format
360 .iter()
361 .map(|_| Some(JxlDataFormat::f32()))
362 .collect(),
363 };
364 decoder_with_image_info.set_pixel_format(requested_format);
365
366 let pixel_format = decoder_with_image_info.current_pixel_format().clone();
368
369 let num_channels = pixel_format.color_type.samples_per_pixel();
370 assert!(num_channels > 0);
371
372 let mut frames = vec![];
373
374 loop {
375 let mut buffers = vec![Image::new_with_value(
377 (buffer_width * num_channels, buffer_height),
378 f32::NAN,
379 )?];
380
381 for ecf in pixel_format.extra_channel_format.iter() {
382 if ecf.is_none() {
383 continue;
384 }
385 buffers.push(Image::new_with_value(
386 (buffer_width, buffer_height),
387 f32::NAN,
388 )?);
389 }
390
391 let mut api_buffers: Vec<_> = buffers
392 .iter_mut()
393 .map(|b| {
394 JxlOutputBuffer::from_image_rect_mut(
395 b.get_rect_mut(Rect {
396 origin: (0, 0),
397 size: b.size(),
398 })
399 .into_raw(),
400 )
401 })
402 .collect();
403
404 let mut decoder_with_frame_info =
406 advance_decoder!(decoder_with_image_info; &mut api_buffers);
407 decoder_with_image_info =
408 advance_decoder!(decoder_with_frame_info, &mut api_buffers; &mut api_buffers);
409
410 for buf in buffers.iter() {
412 let (xs, ys) = buf.size();
413 for y in 0..ys {
414 let row = buf.row(y);
415 for (x, v) in row.iter().enumerate() {
416 assert!(!v.is_nan(), "NaN at {x} {y} (image size {xs}x{ys})");
417 }
418 }
419 }
420
421 frames.push(buffers);
422
423 if !decoder_with_image_info.has_more_frames() {
425 let decoded_frames = decoder_with_image_info.decoded_frames();
426
427 assert!(decoded_frames > 0, "No frames were decoded");
429
430 return Ok((decoded_frames, frames));
431 }
432 }
433 }
434
435 fn decode_test_file(path: &Path) -> Result<(), Error> {
436 decode(&std::fs::read(path)?, usize::MAX, false, false, None)?;
437 Ok(())
438 }
439
440 for_each_test_file!(decode_test_file);
441
442 fn decode_test_file_chunks(path: &Path) -> Result<(), Error> {
443 decode(&std::fs::read(path)?, 1, false, false, None)?;
444 Ok(())
445 }
446
447 for_each_test_file!(decode_test_file_chunks);
448
449 #[allow(dead_code)] fn compare_frames(
451 _path: &Path,
452 fc: usize,
453 f: &[Image<f32>],
454 sf: &[Image<f32>],
455 ) -> Result<(), Error> {
456 assert_eq!(
457 f.len(),
458 sf.len(),
459 "Frame {fc} has different channels counts",
460 );
461 for (c, (b, sb)) in f.iter().zip(sf.iter()).enumerate() {
462 assert_eq!(
463 b.size(),
464 sb.size(),
465 "Channel {c} in frame {fc} has different sizes",
466 );
467 let sz = b.size();
468 for y in 0..sz.1 {
469 for x in 0..sz.0 {
470 assert_eq!(
471 b.row(y)[x],
472 sb.row(y)[x],
473 "Pixels differ at position ({x}, {y}), channel {c}"
474 );
475 }
476 }
477 }
478 Ok(())
479 }
480
481 fn hash_frames(frames: &[Vec<Image<f32>>]) -> Vec<Vec<Vec<u64>>> {
483 use std::hash::{Hash, Hasher};
484 frames
485 .iter()
486 .map(|channels| {
487 channels
488 .iter()
489 .map(|img| {
490 let (_, ys) = img.size();
491 (0..ys)
492 .map(|y| {
493 let mut h = std::hash::DefaultHasher::new();
494 for &v in img.row(y) {
495 v.to_bits().hash(&mut h);
496 }
497 h.finish()
498 })
499 .collect()
500 })
501 .collect()
502 })
503 .collect()
504 }
505
506 fn compare_pipelines(path: &Path) -> Result<(), Error> {
507 let file = std::fs::read(path)?;
508 let reference_frames = decode(&file, usize::MAX, true, false, None)?.1;
509 let reference_hashes = hash_frames(&reference_frames);
513 drop(reference_frames);
514 let frames = decode(&file, usize::MAX, false, false, None)?.1;
515 let frame_hashes = hash_frames(&frames);
516 assert_eq!(
517 reference_hashes,
518 frame_hashes,
519 "{}: pipeline outputs differ",
520 path.display()
521 );
522 Ok(())
523 }
524
525 for_each_test_file!(compare_pipelines);
526
527 fn compare_incremental(path: &Path) -> Result<(), Error> {
528 let file = std::fs::read(path).unwrap();
529 let (_, one_shot_frames) = decode(&file, usize::MAX, false, false, None)?;
531 let reference_hashes = hash_frames(&one_shot_frames);
532 drop(one_shot_frames);
533 let (_, frames) = decode(&file, 123, false, true, None)?;
535 let frame_hashes = hash_frames(&frames);
536 assert_eq!(
537 reference_hashes,
538 frame_hashes,
539 "{}: incremental vs one-shot outputs differ",
540 path.display()
541 );
542
543 Ok(())
544 }
545
546 for_each_test_file!(compare_incremental);
547
548 #[test]
549 fn test_preview_size_none_for_regular_files() {
550 let file = std::fs::read("resources/test/basic.jxl").unwrap();
551 let options = JxlDecoderOptions::default();
552 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
553 let mut input = file.as_slice();
554 let decoder = loop {
555 match decoder.process(&mut input).unwrap() {
556 ProcessingResult::Complete { result } => break result,
557 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
558 }
559 };
560 assert!(decoder.basic_info().preview_size.is_none());
561 }
562
563 #[test]
564 fn test_preview_size_some_for_preview_files() {
565 let file = std::fs::read("resources/test/with_preview.jxl").unwrap();
566 let options = JxlDecoderOptions::default();
567 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
568 let mut input = file.as_slice();
569 let decoder = loop {
570 match decoder.process(&mut input).unwrap() {
571 ProcessingResult::Complete { result } => break result,
572 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
573 }
574 };
575 assert_eq!(decoder.basic_info().preview_size, Some((16, 16)));
576 }
577
578 #[test]
579 fn test_num_completed_passes() {
580 use crate::image::{Image, Rect};
581 let file = std::fs::read("resources/test/basic.jxl").unwrap();
582 let options = JxlDecoderOptions::default();
583 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
584 let mut input = file.as_slice();
585 let mut decoder_with_info = loop {
587 match decoder.process(&mut input).unwrap() {
588 ProcessingResult::Complete { result } => break result,
589 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
590 }
591 };
592 let info = decoder_with_info.basic_info().clone();
593 let mut decoder_with_frame = loop {
594 match decoder_with_info.process(&mut input).unwrap() {
595 ProcessingResult::Complete { result } => break result,
596 ProcessingResult::NeedsMoreInput { fallback, .. } => {
597 decoder_with_info = fallback;
598 }
599 }
600 };
601 assert_eq!(decoder_with_frame.num_completed_passes(), 0);
603 let mut output = Image::<f32>::new((info.size.0 * 3, info.size.1)).unwrap();
605 let rect = Rect {
606 size: output.size(),
607 origin: (0, 0),
608 };
609 let mut bufs = [JxlOutputBuffer::from_image_rect_mut(
610 output.get_rect_mut(rect).into_raw(),
611 )];
612 loop {
613 match decoder_with_frame.process(&mut input, &mut bufs).unwrap() {
614 ProcessingResult::Complete { .. } => break,
615 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder_with_frame = fallback,
616 }
617 }
618 }
619
620 #[test]
621 fn test_set_pixel_format() {
622 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
623
624 let file = std::fs::read("resources/test/basic.jxl").unwrap();
625 let options = JxlDecoderOptions::default();
626 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
627 let mut input = file.as_slice();
628 let mut decoder = loop {
629 match decoder.process(&mut input).unwrap() {
630 ProcessingResult::Complete { result } => break result,
631 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
632 }
633 };
634 let default_format = decoder.current_pixel_format().clone();
636 assert_eq!(default_format.color_type, JxlColorType::Rgb);
637
638 let new_format = JxlPixelFormat {
640 color_type: JxlColorType::Grayscale,
641 color_data_format: Some(JxlDataFormat::U8 { bit_depth: 8 }),
642 extra_channel_format: vec![],
643 };
644 decoder.set_pixel_format(new_format.clone());
645
646 assert_eq!(decoder.current_pixel_format(), &new_format);
648 }
649
650 #[test]
651 fn test_set_output_color_profile() {
652 use crate::api::JxlColorProfile;
653
654 let file = std::fs::read("resources/test/basic.jxl").unwrap();
655 let options = JxlDecoderOptions::default();
656 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
657 let mut input = file.as_slice();
658 let mut decoder = loop {
659 match decoder.process(&mut input).unwrap() {
660 ProcessingResult::Complete { result } => break result,
661 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
662 }
663 };
664
665 let embedded = decoder.embedded_color_profile().clone();
667 let result = decoder.set_output_color_profile(embedded);
668 assert!(result.is_ok());
669
670 let icc_profile = JxlColorProfile::Icc(vec![0u8; 100]);
672 let result = decoder.set_output_color_profile(icc_profile);
673 assert!(result.is_err());
674 }
675
676 #[test]
677 fn test_default_output_tf_by_pixel_format() {
678 use crate::api::{JxlColorEncoding, JxlTransferFunction};
679
680 let file = std::fs::read("resources/test/lossy_with_icc.jxl").unwrap();
682 let options = JxlDecoderOptions::default();
683 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
684 let mut input = file.as_slice();
685 let mut decoder = loop {
686 match decoder.process(&mut input).unwrap() {
687 ProcessingResult::Complete { result } => break result,
688 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
689 }
690 };
691
692 assert_eq!(
694 *decoder.output_color_profile().transfer_function().unwrap(),
695 JxlTransferFunction::Linear,
696 );
697
698 decoder.set_pixel_format(JxlPixelFormat::rgba8(0));
700 assert_eq!(
701 *decoder.output_color_profile().transfer_function().unwrap(),
702 JxlTransferFunction::SRGB,
703 );
704
705 decoder.set_pixel_format(JxlPixelFormat::rgba_f16(0));
706 assert_eq!(
707 *decoder.output_color_profile().transfer_function().unwrap(),
708 JxlTransferFunction::Linear,
709 );
710
711 decoder.set_pixel_format(JxlPixelFormat::rgba16(0));
712 assert_eq!(
713 *decoder.output_color_profile().transfer_function().unwrap(),
714 JxlTransferFunction::SRGB,
715 );
716
717 let profile = JxlColorProfile::Simple(JxlColorEncoding::srgb(false));
720 decoder.set_output_color_profile(profile.clone()).unwrap();
721 decoder.set_pixel_format(JxlPixelFormat::rgba_f16(0));
722 assert!(decoder.output_color_profile() == &profile);
723 }
724
725 #[test]
726 fn test_fill_opaque_alpha_both_pipelines() {
727 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
728 use crate::image::{Image, Rect};
729
730 let file = std::fs::read("resources/test/basic.jxl").unwrap();
732
733 let rgba_format = JxlPixelFormat {
735 color_type: JxlColorType::Rgba,
736 color_data_format: Some(JxlDataFormat::f32()),
737 extra_channel_format: vec![],
738 };
739
740 for use_simple in [true, false] {
742 let options = JxlDecoderOptions::default();
743 let decoder = JxlDecoder::<states::Initialized>::new(options);
744 let mut input = file.as_slice();
745
746 macro_rules! advance_decoder {
748 ($decoder:expr) => {
749 loop {
750 match $decoder.process(&mut input).unwrap() {
751 ProcessingResult::Complete { result } => break result,
752 ProcessingResult::NeedsMoreInput { fallback, .. } => {
753 if input.is_empty() {
754 panic!("Unexpected end of input");
755 }
756 $decoder = fallback;
757 }
758 }
759 }
760 };
761 ($decoder:expr, $buffers:expr) => {
762 loop {
763 match $decoder.process(&mut input, $buffers).unwrap() {
764 ProcessingResult::Complete { result } => break result,
765 ProcessingResult::NeedsMoreInput { fallback, .. } => {
766 if input.is_empty() {
767 panic!("Unexpected end of input");
768 }
769 $decoder = fallback;
770 }
771 }
772 }
773 };
774 }
775
776 let mut decoder = decoder;
777 let mut decoder = advance_decoder!(decoder);
778 decoder.set_use_simple_pipeline(use_simple);
779
780 decoder.set_pixel_format(rgba_format.clone());
782
783 let basic_info = decoder.basic_info().clone();
784 let (width, height) = basic_info.size;
785
786 let mut decoder = advance_decoder!(decoder);
788
789 let mut color_buffer = Image::<f32>::new((width * 4, height)).unwrap();
791 let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
792 color_buffer
793 .get_rect_mut(Rect {
794 origin: (0, 0),
795 size: (width * 4, height),
796 })
797 .into_raw(),
798 )];
799
800 let _decoder = advance_decoder!(decoder, &mut buffers);
802
803 for y in 0..height {
805 let row = color_buffer.row(y);
806 for x in 0..width {
807 let alpha = row[x * 4 + 3];
808 assert_eq!(
809 alpha, 1.0,
810 "Alpha at ({},{}) should be 1.0, got {} (use_simple={})",
811 x, y, alpha, use_simple
812 );
813 }
814 }
815 }
816 }
817
818 #[test]
821 fn test_premultiply_output_straight_alpha() {
822 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
823
824 let file =
826 std::fs::read("resources/test/conformance_test_images/alpha_nonpremultiplied.jxl")
827 .unwrap();
828
829 let rgba_format = JxlPixelFormat {
832 color_type: JxlColorType::Rgba,
833 color_data_format: Some(JxlDataFormat::f32()),
834 extra_channel_format: vec![None],
835 };
836
837 for use_simple in [true, false] {
839 let (straight_buffer, width, height) =
840 decode_with_format::<f32>(&file, &rgba_format, use_simple, false);
841 let (premul_buffer, _, _) =
842 decode_with_format::<f32>(&file, &rgba_format, use_simple, true);
843
844 let mut found_semitransparent = false;
846 for y in 0..height {
847 let straight_row = straight_buffer.row(y);
848 let premul_row = premul_buffer.row(y);
849 for x in 0..width {
850 let sr = straight_row[x * 4];
851 let sg = straight_row[x * 4 + 1];
852 let sb = straight_row[x * 4 + 2];
853 let sa = straight_row[x * 4 + 3];
854
855 let pr = premul_row[x * 4];
856 let pg = premul_row[x * 4 + 1];
857 let pb = premul_row[x * 4 + 2];
858 let pa = premul_row[x * 4 + 3];
859
860 assert!(
862 (sa - pa).abs() < 1e-5,
863 "Alpha mismatch at ({},{}): straight={}, premul={} (use_simple={})",
864 x,
865 y,
866 sa,
867 pa,
868 use_simple
869 );
870
871 let expected_r = sr * sa;
873 let expected_g = sg * sa;
874 let expected_b = sb * sa;
875
876 let tol = 0.01;
878 assert!(
879 (expected_r - pr).abs() < tol,
880 "R mismatch at ({},{}): expected={}, got={} (use_simple={})",
881 x,
882 y,
883 expected_r,
884 pr,
885 use_simple
886 );
887 assert!(
888 (expected_g - pg).abs() < tol,
889 "G mismatch at ({},{}): expected={}, got={} (use_simple={})",
890 x,
891 y,
892 expected_g,
893 pg,
894 use_simple
895 );
896 assert!(
897 (expected_b - pb).abs() < tol,
898 "B mismatch at ({},{}): expected={}, got={} (use_simple={})",
899 x,
900 y,
901 expected_b,
902 pb,
903 use_simple
904 );
905
906 if sa > 0.01 && sa < 0.99 {
907 found_semitransparent = true;
908 }
909 }
910 }
911
912 assert!(
914 found_semitransparent,
915 "Test image should have semi-transparent pixels (use_simple={})",
916 use_simple
917 );
918 }
919 }
920
921 #[test]
924 fn test_premultiply_output_already_premultiplied() {
925 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
926
927 let file = std::fs::read("resources/test/conformance_test_images/alpha_premultiplied.jxl")
929 .unwrap();
930
931 let rgba_format = JxlPixelFormat {
933 color_type: JxlColorType::Rgba,
934 color_data_format: Some(JxlDataFormat::f32()),
935 extra_channel_format: vec![None],
936 };
937
938 for use_simple in [true, false] {
940 let (without_flag_buffer, width, height) =
941 decode_with_format::<f32>(&file, &rgba_format, use_simple, false);
942 let (with_flag_buffer, _, _) =
943 decode_with_format::<f32>(&file, &rgba_format, use_simple, true);
944
945 for y in 0..height {
948 let without_row = without_flag_buffer.row(y);
949 let with_row = with_flag_buffer.row(y);
950 for x in 0..width {
951 for c in 0..4 {
952 let without_val = without_row[x * 4 + c];
953 let with_val = with_row[x * 4 + c];
954 assert!(
955 (without_val - with_val).abs() < 1e-5,
956 "Mismatch at ({},{}) channel {}: without_flag={}, with_flag={} (use_simple={})",
957 x,
958 y,
959 c,
960 without_val,
961 with_val,
962 use_simple
963 );
964 }
965 }
966 }
967 }
968 }
969
970 #[test]
974 fn test_animation_with_reference_frames() {
975 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
976 use crate::image::{Image, Rect};
977
978 let file =
980 std::fs::read("resources/test/conformance_test_images/animation_spline.jxl").unwrap();
981
982 let options = JxlDecoderOptions::default();
983 let decoder = JxlDecoder::<states::Initialized>::new(options);
984 let mut input = file.as_slice();
985
986 let mut decoder = decoder;
988 let mut decoder = loop {
989 match decoder.process(&mut input).unwrap() {
990 ProcessingResult::Complete { result } => break result,
991 ProcessingResult::NeedsMoreInput { fallback, .. } => {
992 decoder = fallback;
993 }
994 }
995 };
996
997 let rgb_format = JxlPixelFormat {
999 color_type: JxlColorType::Rgb,
1000 color_data_format: Some(JxlDataFormat::f32()),
1001 extra_channel_format: vec![],
1002 };
1003 decoder.set_pixel_format(rgb_format);
1004
1005 let basic_info = decoder.basic_info().clone();
1006 let (width, height) = basic_info.size;
1007
1008 let mut frame_count = 0;
1009
1010 loop {
1012 let mut decoder_frame = loop {
1014 match decoder.process(&mut input).unwrap() {
1015 ProcessingResult::Complete { result } => break result,
1016 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1017 decoder = fallback;
1018 }
1019 }
1020 };
1021
1022 let mut color_buffer = Image::<f32>::new((width * 3, height)).unwrap();
1024 let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
1025 color_buffer
1026 .get_rect_mut(Rect {
1027 origin: (0, 0),
1028 size: (width * 3, height),
1029 })
1030 .into_raw(),
1031 )];
1032
1033 decoder = loop {
1036 match decoder_frame.process(&mut input, &mut buffers).unwrap() {
1037 ProcessingResult::Complete { result } => break result,
1038 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1039 decoder_frame = fallback;
1040 }
1041 }
1042 };
1043
1044 frame_count += 1;
1045
1046 if !decoder.has_more_frames() {
1048 break;
1049 }
1050 }
1051
1052 assert!(
1054 frame_count > 1,
1055 "Expected multiple frames in animation, got {}",
1056 frame_count
1057 );
1058 }
1059
1060 #[test]
1061 fn test_skip_frame_then_decode_next() {
1062 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
1063 use crate::image::{Image, Rect};
1064
1065 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 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 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 decoder_frame = loop {
1097 match decoder.process(&mut input).unwrap() {
1098 ProcessingResult::Complete { result } => break result,
1099 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1100 decoder = fallback;
1101 }
1102 }
1103 };
1104
1105 let mut decoder = loop {
1107 match decoder_frame.skip_frame(&mut input).unwrap() {
1108 ProcessingResult::Complete { result } => break result,
1109 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1110 decoder_frame = fallback;
1111 }
1112 }
1113 };
1114
1115 assert!(
1116 decoder.has_more_frames(),
1117 "Animation should have more frames"
1118 );
1119
1120 let mut decoder_frame = loop {
1123 match decoder.process(&mut input).unwrap() {
1124 ProcessingResult::Complete { result } => break result,
1125 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1126 decoder = fallback;
1127 }
1128 }
1129 };
1130
1131 let mut color_buffer = Image::<f32>::new((width * 3, height)).unwrap();
1133 let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
1134 color_buffer
1135 .get_rect_mut(Rect {
1136 origin: (0, 0),
1137 size: (width * 3, height),
1138 })
1139 .into_raw(),
1140 )];
1141
1142 let decoder = loop {
1143 match decoder_frame.process(&mut input, &mut buffers).unwrap() {
1144 ProcessingResult::Complete { result } => break result,
1145 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1146 decoder_frame = fallback;
1147 }
1148 }
1149 };
1150
1151 let _ = decoder.has_more_frames();
1154 }
1155
1156 #[test]
1160 fn test_output_format_u8_matches_f32() {
1161 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
1162
1163 let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1165
1166 for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] {
1168 let f32_format = JxlPixelFormat {
1169 color_type,
1170 color_data_format: Some(JxlDataFormat::f32()),
1171 extra_channel_format: vec![],
1172 };
1173 let u8_format = JxlPixelFormat {
1174 color_type,
1175 color_data_format: Some(JxlDataFormat::U8 { bit_depth: 8 }),
1176 extra_channel_format: vec![],
1177 };
1178
1179 for use_simple in [true, false] {
1181 let (f32_buffer, width, height) =
1182 decode_with_format::<f32>(&file, &f32_format, use_simple, false);
1183 let (u8_buffer, _, _) =
1184 decode_with_format::<u8>(&file, &u8_format, use_simple, false);
1185
1186 let tolerance = 0.003;
1189 let mut max_error: f32 = 0.0;
1190
1191 for y in 0..height {
1192 let f32_row = f32_buffer.row(y);
1193 let u8_row = u8_buffer.row(y);
1194 for x in 0..(width * num_samples) {
1195 let f32_val = f32_row[x].clamp(0.0, 1.0);
1196 let u8_val = u8_row[x] as f32 / 255.0;
1197 let error = (f32_val - u8_val).abs();
1198 max_error = max_error.max(error);
1199 assert!(
1200 error < tolerance,
1201 "{:?} u8 mismatch at ({},{}): f32={}, u8={} (scaled={}), error={} (use_simple={})",
1202 color_type,
1203 x,
1204 y,
1205 f32_val,
1206 u8_row[x],
1207 u8_val,
1208 error,
1209 use_simple
1210 );
1211 }
1212 }
1213 }
1214 }
1215 }
1216
1217 #[test]
1219 fn test_output_format_u16_matches_f32() {
1220 use crate::api::{Endianness, JxlColorType, JxlDataFormat, JxlPixelFormat};
1221
1222 let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1223
1224 for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] {
1226 let f32_format = JxlPixelFormat {
1227 color_type,
1228 color_data_format: Some(JxlDataFormat::f32()),
1229 extra_channel_format: vec![],
1230 };
1231 let u16_format = JxlPixelFormat {
1232 color_type,
1233 color_data_format: Some(JxlDataFormat::U16 {
1234 endianness: Endianness::native(),
1235 bit_depth: 16,
1236 }),
1237 extra_channel_format: vec![],
1238 };
1239
1240 for use_simple in [true, false] {
1241 let (f32_buffer, width, height) =
1242 decode_with_format::<f32>(&file, &f32_format, use_simple, false);
1243 let (u16_buffer, _, _) =
1244 decode_with_format::<u16>(&file, &u16_format, use_simple, false);
1245
1246 let tolerance = 0.0001;
1248
1249 for y in 0..height {
1250 let f32_row = f32_buffer.row(y);
1251 let u16_row = u16_buffer.row(y);
1252 for x in 0..(width * num_samples) {
1253 let f32_val = f32_row[x].clamp(0.0, 1.0);
1254 let u16_val = u16_row[x] as f32 / 65535.0;
1255 let error = (f32_val - u16_val).abs();
1256 assert!(
1257 error < tolerance,
1258 "{:?} u16 mismatch at ({},{}): f32={}, u16={} (scaled={}), error={} (use_simple={})",
1259 color_type,
1260 x,
1261 y,
1262 f32_val,
1263 u16_row[x],
1264 u16_val,
1265 error,
1266 use_simple
1267 );
1268 }
1269 }
1270 }
1271 }
1272 }
1273
1274 #[test]
1276 fn test_output_format_f16_matches_f32() {
1277 use crate::api::{Endianness, JxlColorType, JxlDataFormat, JxlPixelFormat};
1278 use crate::util::f16;
1279
1280 let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1281
1282 for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] {
1284 let f32_format = JxlPixelFormat {
1285 color_type,
1286 color_data_format: Some(JxlDataFormat::f32()),
1287 extra_channel_format: vec![],
1288 };
1289 let f16_format = JxlPixelFormat {
1290 color_type,
1291 color_data_format: Some(JxlDataFormat::F16 {
1292 endianness: Endianness::native(),
1293 }),
1294 extra_channel_format: vec![],
1295 };
1296
1297 for use_simple in [true, false] {
1298 let (f32_buffer, width, height) =
1299 decode_with_format::<f32>(&file, &f32_format, use_simple, false);
1300 let (f16_buffer, _, _) =
1301 decode_with_format::<f16>(&file, &f16_format, use_simple, false);
1302
1303 let tolerance = 0.002;
1306
1307 for y in 0..height {
1308 let f32_row = f32_buffer.row(y);
1309 let f16_row = f16_buffer.row(y);
1310 for x in 0..(width * num_samples) {
1311 let f32_val = f32_row[x];
1312 let f16_val = f16_row[x].to_f32();
1313 let error = (f32_val - f16_val).abs();
1314 assert!(
1315 error < tolerance,
1316 "{:?} f16 mismatch at ({},{}): f32={}, f16={}, error={} (use_simple={})",
1317 color_type,
1318 x,
1319 y,
1320 f32_val,
1321 f16_val,
1322 error,
1323 use_simple
1324 );
1325 }
1326 }
1327 }
1328 }
1329 }
1330
1331 fn decode_with_format<T: crate::image::ImageDataType>(
1333 file: &[u8],
1334 pixel_format: &JxlPixelFormat,
1335 use_simple: bool,
1336 premultiply: bool,
1337 ) -> (Image<T>, usize, usize) {
1338 let options = JxlDecoderOptions {
1339 premultiply_output: premultiply,
1340 ..Default::default()
1341 };
1342 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1343 let mut input = file;
1344
1345 let mut decoder = loop {
1347 match decoder.process(&mut input).unwrap() {
1348 ProcessingResult::Complete { result } => break result,
1349 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1350 if input.is_empty() {
1351 panic!("Unexpected end of input");
1352 }
1353 decoder = fallback;
1354 }
1355 }
1356 };
1357 decoder.set_use_simple_pipeline(use_simple);
1358 decoder.set_pixel_format(pixel_format.clone());
1359
1360 let basic_info = decoder.basic_info().clone();
1361 let (width, height) = basic_info.size;
1362
1363 let num_samples = pixel_format.color_type.samples_per_pixel();
1364
1365 let decoder = loop {
1367 match decoder.process(&mut input).unwrap() {
1368 ProcessingResult::Complete { result } => break result,
1369 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1370 if input.is_empty() {
1371 panic!("Unexpected end of input");
1372 }
1373 decoder = fallback;
1374 }
1375 }
1376 };
1377
1378 let mut buffer = Image::<T>::new((width * num_samples, height)).unwrap();
1379 let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
1380 buffer
1381 .get_rect_mut(Rect {
1382 origin: (0, 0),
1383 size: (width * num_samples, height),
1384 })
1385 .into_raw(),
1386 )];
1387
1388 let mut decoder = decoder;
1390 loop {
1391 match decoder.process(&mut input, &mut buffers).unwrap() {
1392 ProcessingResult::Complete { .. } => break,
1393 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1394 if input.is_empty() {
1395 panic!("Unexpected end of input");
1396 }
1397 decoder = fallback;
1398 }
1399 }
1400 }
1401
1402 (buffer, width, height)
1403 }
1404
1405 #[test]
1408 fn test_fuzzer_smallbuffer_overflow() {
1409 use std::panic;
1410
1411 let data = include_bytes!("../../tests/testdata/fuzzer_smallbuffer_overflow.jxl");
1412
1413 let result = panic::catch_unwind(|| {
1416 let _ = decode(data, 1024, false, false, None);
1417 });
1418
1419 if let Err(e) = result {
1421 let panic_msg = e
1422 .downcast_ref::<&str>()
1423 .map(|s| s.to_string())
1424 .or_else(|| e.downcast_ref::<String>().cloned())
1425 .unwrap_or_default();
1426 assert!(
1427 !panic_msg.contains("overflow"),
1428 "Unexpected overflow panic: {}",
1429 panic_msg
1430 );
1431 }
1432 }
1433
1434 fn wrap_with_frame_index(
1436 codestream: &[u8],
1437 tnum: u32,
1438 tden: u32,
1439 entries: &[(u64, u64, u64)], ) -> Vec<u8> {
1441 use crate::util::test::build_frame_index_content;
1442
1443 fn make_box(ty: &[u8; 4], content: &[u8]) -> Vec<u8> {
1444 let len = (8 + content.len()) as u32;
1445 let mut buf = Vec::new();
1446 buf.extend(len.to_be_bytes());
1447 buf.extend(ty);
1448 buf.extend(content);
1449 buf
1450 }
1451
1452 let jxli_content = build_frame_index_content(tnum, tden, entries);
1453
1454 let sig = [
1456 0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a,
1457 ];
1458 let ftyp = make_box(b"ftyp", b"jxl \x00\x00\x00\x00jxl ");
1460 let jxli = make_box(b"jxli", &jxli_content);
1461 let jxlc = make_box(b"jxlc", codestream);
1462
1463 let mut container = Vec::new();
1464 container.extend(&sig);
1465 container.extend(&ftyp);
1466 container.extend(&jxli);
1467 container.extend(&jxlc);
1468 container
1469 }
1470
1471 #[test]
1472 fn test_frame_index_parsed_from_container() {
1473 let codestream =
1475 std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
1476
1477 let entries = vec![
1480 (0u64, 100u64, 1u64), (500, 100, 1), (600, 100, 1), ];
1484
1485 let container = wrap_with_frame_index(&codestream, 1, 1000, &entries);
1486
1487 let options = JxlDecoderOptions::default();
1489 let mut dec = JxlDecoder::<states::Initialized>::new(options);
1490 let mut input: &[u8] = &container;
1491 let dec = loop {
1492 match dec.process(&mut input).unwrap() {
1493 ProcessingResult::Complete { result } => break result,
1494 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1495 if input.is_empty() {
1496 panic!("Unexpected end of input");
1497 }
1498 dec = fallback;
1499 }
1500 }
1501 };
1502
1503 let fi = dec.frame_index().expect("frame_index should be Some");
1505 assert_eq!(fi.num_frames(), 3);
1506 assert_eq!(fi.tnum, 1);
1507 assert_eq!(fi.tden.get(), 1000);
1508 assert_eq!(fi.entries[0].codestream_offset, 0);
1510 assert_eq!(fi.entries[1].codestream_offset, 500);
1511 assert_eq!(fi.entries[2].codestream_offset, 1100);
1512 assert_eq!(fi.entries[0].duration_ticks, 100);
1513 assert_eq!(fi.entries[2].frame_count, 1);
1514 }
1515
1516 #[test]
1517 fn test_frame_index_none_for_bare_codestream() {
1518 let data =
1520 std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
1521 let options = JxlDecoderOptions::default();
1522 let mut dec = JxlDecoder::<states::Initialized>::new(options);
1523 let mut input: &[u8] = &data;
1524 let dec = loop {
1525 match dec.process(&mut input).unwrap() {
1526 ProcessingResult::Complete { result } => break result,
1527 ProcessingResult::NeedsMoreInput { fallback, .. } => {
1528 if input.is_empty() {
1529 panic!("Unexpected end of input");
1530 }
1531 dec = fallback;
1532 }
1533 }
1534 };
1535 assert!(dec.frame_index().is_none());
1536 }
1537
1538 #[test]
1540 fn test_fuzzer_xyb_icc_no_panic() {
1541 use crate::api::ProcessingResult;
1542
1543 #[rustfmt::skip]
1544 let data: &[u8] = &[
1545 0xff, 0x0a, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00,
1546 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x25, 0x00,
1547 ];
1548
1549 let opts = JxlDecoderOptions::default();
1550 let mut decoder = JxlDecoderInner::new(opts);
1551 let mut input = data;
1552
1553 if let Ok(ProcessingResult::Complete { .. }) = decoder.process(&mut input, None)
1554 && let Some(profile) = decoder.output_color_profile()
1555 {
1556 let _ = profile.try_as_icc();
1557 }
1558 }
1559
1560 #[test]
1561 fn test_pixel_limit_enforcement() {
1562 let input = std::fs::read("resources/test/green_queen_vardct_e3.jxl").unwrap();
1564
1565 let mut options = JxlDecoderOptions::default();
1567 options.limits.max_pixels = Some(100); let decoder = JxlDecoder::<states::Initialized>::new(options);
1570 let mut input_slice = &input[..];
1571
1572 let result = decoder.process(&mut input_slice);
1574 match result {
1575 Err(err) => {
1576 assert!(
1577 matches!(
1578 err,
1579 Error::LimitExceeded {
1580 resource: "pixels",
1581 ..
1582 }
1583 ),
1584 "Expected LimitExceeded for pixels, got {:?}",
1585 err
1586 );
1587 }
1588 Ok(ProcessingResult::NeedsMoreInput { .. }) => {
1589 panic!("Expected error, got needs more input");
1590 }
1591 Ok(ProcessingResult::Complete { .. }) => {
1592 panic!("Expected error, got success");
1593 }
1594 }
1595 }
1596
1597 #[test]
1598 fn test_restrictive_limits_preset() {
1599 let limits = crate::api::JxlDecoderLimits::restrictive();
1601 assert_eq!(limits.max_pixels, Some(100_000_000));
1602 assert_eq!(limits.max_extra_channels, Some(16));
1603 assert_eq!(limits.max_icc_size, Some(1 << 20));
1604 assert_eq!(limits.max_tree_size, Some(1 << 20));
1605 assert_eq!(limits.max_patches, Some(1 << 16));
1606 assert_eq!(limits.max_spline_points, Some(1 << 16));
1607 assert_eq!(limits.max_reference_frames, Some(2));
1608 assert_eq!(limits.max_memory_bytes, Some(1 << 30));
1609 }
1610
1611 #[test]
1612 fn test_extra_channel_metadata() {
1613 let file = std::fs::read("resources/test/extra_channels.jxl").unwrap();
1614 let options = JxlDecoderOptions::default();
1615 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1616 let mut input = file.as_slice();
1617 let decoder = loop {
1618 match decoder.process(&mut input).unwrap() {
1619 ProcessingResult::Complete { result } => break result,
1620 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
1621 }
1622 };
1623 let info = decoder.basic_info();
1624 assert!(
1626 !info.extra_channels.is_empty(),
1627 "expected at least one extra channel"
1628 );
1629
1630 for ec in &info.extra_channels {
1632 assert!(
1634 ec.bits_per_sample > 0 && ec.bits_per_sample <= 32,
1635 "unexpected bits_per_sample: {}",
1636 ec.bits_per_sample
1637 );
1638 assert!(ec.dim_shift <= 3, "unexpected dim_shift: {}", ec.dim_shift);
1640 }
1641 }
1642
1643 #[test]
1644 fn test_extra_channel_alpha_with_new_fields() {
1645 use crate::headers::extra_channels::ExtraChannel;
1646
1647 let file = std::fs::read("resources/test/3x3a_srgb_lossless.jxl").unwrap();
1649 let options = JxlDecoderOptions::default();
1650 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1651 let mut input = file.as_slice();
1652 let decoder = loop {
1653 match decoder.process(&mut input).unwrap() {
1654 ProcessingResult::Complete { result } => break result,
1655 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
1656 }
1657 };
1658 let info = decoder.basic_info();
1659 assert_eq!(info.extra_channels.len(), 1);
1661 let alpha = &info.extra_channels[0];
1662 assert_eq!(alpha.ec_type, ExtraChannel::Alpha);
1663 assert!(alpha.bits_per_sample > 0);
1664 assert_eq!(alpha.dim_shift, 0);
1666 }
1667
1668 #[test]
1669 fn test_preview_metadata_in_basic_info() {
1670 let file = std::fs::read("resources/test/with_preview.jxl").unwrap();
1672 let options = JxlDecoderOptions::default();
1673 let mut decoder = JxlDecoder::<states::Initialized>::new(options);
1674 let mut input = file.as_slice();
1675 let decoder = loop {
1676 match decoder.process(&mut input).unwrap() {
1677 ProcessingResult::Complete { result } => break result,
1678 ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
1679 }
1680 };
1681 let info = decoder.basic_info();
1682 let (pw, ph) = info.preview_size.expect("expected preview_size");
1683 assert!(pw > 0 && ph > 0, "preview dimensions should be positive");
1684 }
1685
1686 #[test]
1687 fn test_stop_cancellation() {
1688 use almost_enough::Stopper;
1689 use enough::Stop;
1690
1691 let stop = Stopper::new();
1692 assert!(!stop.should_stop());
1693 stop.cancel();
1694 assert!(stop.should_stop());
1695 let result: crate::error::Result<()> = stop.check().map_err(Into::into);
1697 assert!(matches!(result, Err(crate::error::Error::Cancelled)));
1698 }
1699
1700 #[test]
1721 #[allow(clippy::field_reassign_with_default)]
1722 fn test_preview_recovery_preserves_decoder_options() {
1723 let data = std::fs::read("resources/test/with_preview.jxl")
1724 .expect("with_preview.jxl test fixture should exist");
1725
1726 let mut options = JxlDecoderOptions::default();
1734 options.high_precision = true;
1735 options.premultiply_output = true;
1736 options.parallel = false;
1737 options.render_spot_colors = false;
1738 options.limits.max_memory_bytes = Some(64 * 1024 * 1024);
1741
1742 let mut decoder = JxlDecoderInner::new(options);
1743 let mut input = data.as_slice();
1744
1745 match decoder.process(&mut input, None) {
1747 Ok(ProcessingResult::Complete { .. }) => {}
1748 other => panic!("expected image-info Complete, got {other:?}"),
1749 }
1750 assert!(decoder.basic_info().is_some());
1751
1752 match decoder.process(&mut input, None) {
1760 Ok(ProcessingResult::Complete { .. }) => {}
1761 other => panic!("expected frame-info Complete, got {other:?}"),
1762 }
1763 assert!(decoder.frame_header().is_some());
1764
1765 let state = decoder
1768 .decoder_state_for_test()
1769 .expect("decoder_state must exist inside the active main frame");
1770
1771 assert!(
1775 state.high_precision,
1776 "high_precision should survive preview-frame recovery"
1777 );
1778 assert!(
1779 state.premultiply_output,
1780 "premultiply_output should survive preview-frame recovery"
1781 );
1782 assert!(
1783 !state.parallel,
1784 "parallel=false should survive preview-frame recovery (was silently flipped back to DecoderState::new default)"
1785 );
1786 assert!(
1787 !state.render_spotcolors,
1788 "render_spotcolors=false should survive preview-frame recovery"
1789 );
1790 assert!(
1791 state.memory_tracker.has_limit(),
1792 "memory_tracker should carry the configured limit after preview-frame recovery, not revert to unlimited"
1793 );
1794 assert_eq!(
1795 state.memory_tracker.limit(),
1796 Some(64 * 1024 * 1024),
1797 "memory_tracker limit should equal configured max_memory_bytes"
1798 );
1799 assert!(
1800 state.embedded_color_profile.is_some(),
1801 "embedded_color_profile must be propagated so CMYK ICC and similar code paths work after preview recovery"
1802 );
1803 assert_eq!(
1804 state.limits.max_memory_bytes,
1805 Some(64 * 1024 * 1024),
1806 "limits.max_memory_bytes on DecoderState must match the configured options"
1807 );
1808 }
1809
1810 #[test]
1816 fn test_chunked_drip_decode_animation_newtons_cradle() {
1817 let data =
1818 std::fs::read("resources/test/conformance_test_images/animation_newtons_cradle.jxl")
1819 .expect("animation_newtons_cradle.jxl test fixture should exist");
1820
1821 let options = JxlDecoderOptions::default();
1822 let mut decoder = JxlDecoderInner::new(options);
1823 const CHUNK: usize = 1024;
1824 let mut fed = 0usize;
1825
1826 while fed < data.len() {
1827 let end = (fed + CHUNK).min(data.len());
1828 let mut chunk = &data[fed..end];
1829 let before = chunk.len();
1830 match decoder.process(&mut chunk, None) {
1831 Ok(_) => {}
1832 Err(e) => panic!("decoder errored on chunk [{fed}..{end}]: {e:?}"),
1833 }
1834 let consumed = before - chunk.len();
1835 fed += consumed;
1836 if consumed == 0 {
1837 fed = end;
1839 }
1840 }
1841 }
1842}