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, 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
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 vardct_quantizer(&self) -> Option<VardctQuantizer> {
87 self.inner.vardct_quantizer()
88 }
89
90 pub fn gain_map(&self) -> Option<&GainMapBundle> {
101 self.inner.gain_map()
102 }
103
104 pub fn take_gain_map(&mut self) -> Option<GainMapBundle> {
107 self.inner.take_gain_map()
108 }
109
110 pub fn exif(&self) -> Option<&[u8]> {
120 self.inner.exif()
121 }
122
123 pub fn take_exif(&mut self) -> Option<Vec<u8>> {
125 self.inner.take_exif()
126 }
127
128 pub fn xmp(&self) -> Option<&[u8]> {
136 self.inner.xmp()
137 }
138
139 pub fn take_xmp(&mut self) -> Option<Vec<u8>> {
141 self.inner.take_xmp()
142 }
143
144 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 pub fn basic_info(&self) -> &JxlBasicInfo {
187 self.inner.basic_info().unwrap()
188 }
189
190 pub fn embedded_color_profile(&self) -> &JxlColorProfile {
192 self.inner.embedded_color_profile().unwrap()
193 }
194
195 pub fn output_color_profile(&self) -> &JxlColorProfile {
197 self.inner.output_color_profile().unwrap()
198 }
199
200 pub fn set_output_color_profile(&mut self, profile: JxlColorProfile) -> Result<()> {
203 self.inner.set_output_color_profile(profile)
204 }
205
206 pub fn current_pixel_format(&self) -> &JxlPixelFormat {
208 self.inner.current_pixel_format().unwrap()
209 }
210
211 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 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 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 pub fn num_completed_passes(&self) -> usize {
260 self.inner.num_completed_passes().unwrap()
261 }
262
263 pub fn flush_pixels(&mut self, buffers: &mut [JxlOutputBuffer<'_>]) -> Result<()> {
267 self.inner.flush_pixels(buffers)
268 }
269
270 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 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 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 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 let mut decoder_with_image_info = advance_decoder!(initialized_decoder);
429 decoder_with_image_info.set_use_simple_pipeline(use_simple_pipeline);
430
431 let basic_info = decoder_with_image_info.basic_info().clone();
433 assert!(basic_info.bit_depth.bits_per_sample() > 0);
434
435 let (buffer_width, buffer_height) = basic_info.size;
437 assert!(buffer_width > 0);
438 assert!(buffer_height > 0);
439
440 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 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 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 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 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 if !decoder_with_image_info.has_more_frames() {
512 let decoded_frames = decoder_with_image_info.decoded_frames();
513
514 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)] 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 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 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 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 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 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 assert_eq!(decoder_with_frame.num_completed_passes(), 0);
690 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 let default_format = decoder.current_pixel_format().clone();
723 assert_eq!(default_format.color_type, JxlColorType::Rgb);
724
725 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 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 let embedded = decoder.embedded_color_profile().clone();
754 let result = decoder.set_output_color_profile(embedded);
755 assert!(result.is_ok());
756
757 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 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 assert_eq!(
781 *decoder.output_color_profile().transfer_function().unwrap(),
782 JxlTransferFunction::Linear,
783 );
784
785 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 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 let file = std::fs::read("resources/test/basic.jxl").unwrap();
819
820 let rgba_format = JxlPixelFormat {
822 color_type: JxlColorType::Rgba,
823 color_data_format: Some(JxlDataFormat::f32()),
824 extra_channel_format: vec![],
825 };
826
827 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 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 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 let mut decoder = advance_decoder!(decoder);
875
876 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 let _decoder = advance_decoder!(decoder, &mut buffers);
889
890 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]
908 fn test_premultiply_output_straight_alpha() {
909 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
910
911 let file =
913 std::fs::read("resources/test/conformance_test_images/alpha_nonpremultiplied.jxl")
914 .unwrap();
915
916 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 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 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 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 let expected_r = sr * sa;
960 let expected_g = sg * sa;
961 let expected_b = sb * sa;
962
963 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 assert!(
1001 found_semitransparent,
1002 "Test image should have semi-transparent pixels (use_simple={})",
1003 use_simple
1004 );
1005 }
1006 }
1007
1008 #[test]
1011 fn test_premultiply_output_already_premultiplied() {
1012 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
1013
1014 let file = std::fs::read("resources/test/conformance_test_images/alpha_premultiplied.jxl")
1016 .unwrap();
1017
1018 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 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 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]
1061 fn test_animation_with_reference_frames() {
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 frame_count = 0;
1096
1097 loop {
1099 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 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 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 if !decoder.has_more_frames() {
1135 break;
1136 }
1137 }
1138
1139 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 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 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 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 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 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 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 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 let _ = decoder.has_more_frames();
1241 }
1242
1243 #[test]
1247 fn test_output_format_u8_matches_f32() {
1248 use crate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
1249
1250 let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
1252
1253 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 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 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]
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 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 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]
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 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 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 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 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 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 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 #[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 let result = panic::catch_unwind(|| {
1503 let _ = decode(data, 1024, false, false, None);
1504 });
1505
1506 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 fn wrap_with_frame_index(
1523 codestream: &[u8],
1524 tnum: u32,
1525 tden: u32,
1526 entries: &[(u64, u64, u64)], ) -> 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 let sig = [
1543 0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a,
1544 ];
1545 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 let codestream =
1562 std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
1563
1564 let entries = vec![
1567 (0u64, 100u64, 1u64), (500, 100, 1), (600, 100, 1), ];
1571
1572 let container = wrap_with_frame_index(&codestream, 1, 1000, &entries);
1573
1574 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 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 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 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 #[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 let input = std::fs::read("resources/test/green_queen_vardct_e3.jxl").unwrap();
1651
1652 let mut options = JxlDecoderOptions::default();
1654 options.limits.max_pixels = Some(100); let decoder = JxlDecoder::<states::Initialized>::new(options);
1657 let mut input_slice = &input[..];
1658
1659 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 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 assert!(
1713 !info.extra_channels.is_empty(),
1714 "expected at least one extra channel"
1715 );
1716
1717 for ec in &info.extra_channels {
1719 assert!(
1721 ec.bits_per_sample > 0 && ec.bits_per_sample <= 32,
1722 "unexpected bits_per_sample: {}",
1723 ec.bits_per_sample
1724 );
1725 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 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 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 assert_eq!(alpha.dim_shift, 0);
1753 }
1754
1755 #[test]
1756 fn test_preview_metadata_in_basic_info() {
1757 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 let result: crate::error::Result<()> = stop.check().map_err(Into::into);
1784 assert!(matches!(result, Err(crate::error::Error::Cancelled)));
1785 }
1786
1787 #[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 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 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 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 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 let state = decoder
1855 .decoder_state_for_test()
1856 .expect("decoder_state must exist inside the active main frame");
1857
1858 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 #[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 fed = end;
1926 }
1927 }
1928 }
1929}