// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file.
usesuper::{
JxlBasicInfo, JxlBitstreamInput, JxlColorProfile, JxlDecoderInner, JxlDecoderOptions,
JxlOutputBuffer, JxlPixelFormat, ProcessingResult,
}; #[cfg(test)] usecrate::frame::Frame; usecrate::{api::JxlFrameHeader, container::frame_index::FrameIndexBox, error::Result}; use states::*; use std::marker::PhantomData;
pubmod states { pubtrait JxlState {} pubstruct Initialized; pubstruct WithImageInfo; pubstruct WithFrameInfo; impl JxlState for Initialized {} impl JxlState for WithImageInfo {} impl JxlState for WithFrameInfo {}
}
// Q: do we plan to add support for box decoding? // If we do, one way is to take a callback &[u8; 4] -> Box<dyn Write>.
/// High level API using the typestate pattern to forbid invalid usage. pubstruct JxlDecoder<State: JxlState> {
inner: Box<JxlDecoderInner>,
_state: PhantomData<State>,
}
/// Information about a single visible frame discovered while decoding. #[derive(Debug, Clone, PartialEq)] pubstruct VisibleFrameInfo { /// Zero-based index among visible frames. pub index: usize, /// Duration in milliseconds (0 for still images or the last frame). pub duration_ms: f64, /// Duration in raw ticks from the animation header. pub duration_ticks: u32, /// Byte offset of this frame's header in the input file. pub(crate) file_offset: usize, /// Whether this is the last frame in the codestream. pub is_last: bool, /// Whether this frame is a seek-keyframe for visible-frame playback. /// /// This is equivalent to `seek_target.visible_frames_to_skip == 0`. pub is_keyframe: bool, /// Precomputed seek inputs for this visible frame. pub seek_target: VisibleFrameSeekTarget, /// Frame name, if any. pub name: String,
}
/// Computed seek inputs for a target visible frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pubstruct VisibleFrameSeekTarget { /// File byte offset to start feeding input from. pub decode_start_file_offset: u64, /// Remaining codestream bytes in the current container box at the seek /// point. Pass this to [`JxlDecoder::start_new_frame`]. pub remaining_in_box: u64, /// Number of visible frames to skip after seek-start before decoding the /// requested target frame. pub visible_frames_to_skip: usize,
}
/// Sets a callback that processes all frames by calling `callback(frame, frame_index)`. #[cfg(test)] pubfn set_frame_callback(&mutself, callback: Box<FrameCallback>) { self.inner.set_frame_callback(callback);
}
/// Returns the parsed frame index box, if the file contained one. /// /// The frame index box (`jxli`) is an optional part of the JXL container /// format that provides a seek table for animated files, listing keyframe /// byte offsets, timestamps, and frame counts. /// /// TODO(veluca): Provide a higher-level frame-index API aligned with /// `scanned_frames()` / `VisibleFrameInfo` seek metadata. pubfn frame_index(&self) -> Option<&FrameIndexBox> { self.inner.frame_index()
}
/// Returns visible frame info entries collected so far. /// /// When `JxlDecoderOptions::scan_frames_only` is enabled this is the /// primary output of decoding. pubfn scanned_frames(&self) -> &[VisibleFrameInfo] { self.inner.scanned_frames()
}
/// Rewinds a decoder to the start of the file, allowing past frames to be displayed again. pubfn rewind(mutself) -> JxlDecoder<Initialized> { self.inner.rewind();
JxlDecoder::wrap_inner(self.inner)
}
/// Retrieves the file's color profile. pubfn embedded_color_profile(&self) -> &JxlColorProfile { self.inner.embedded_color_profile().unwrap()
}
/// Retrieves the current output color profile. pubfn output_color_profile(&self) -> &JxlColorProfile { self.inner.output_color_profile().unwrap()
}
/// Specifies the preferred color profile to be used for outputting data. /// Same semantics as JxlDecoderSetOutputColorProfile. pubfn set_output_color_profile(&mutself, profile: JxlColorProfile) -> Result<()> { self.inner.set_output_color_profile(profile)
}
/// Retrieves the current pixel format for output buffers. pubfn current_pixel_format(&self) -> &JxlPixelFormat { self.inner.current_pixel_format().unwrap()
}
/// Specifies pixel format for output buffers. /// /// Setting this may also change output color profile in some cases, if the profile was not set /// manually before. pubfn set_pixel_format(&mutself, pixel_format: JxlPixelFormat) { self.inner.set_pixel_format(pixel_format);
}
/// Draws all the pixels we have data for. This is useful for i.e. previewing LF frames. /// /// Returns `true` if any new pixels were written to `buffers` since the /// previous call to `flush_pixels`; `false` if nothing new was rendered. /// /// Note: see `process` for alignment requirements for the buffer data. pubfn flush_pixels(&mutself, buffers: &mut [JxlOutputBuffer<'_>]) -> Result<bool> { self.inner.flush_pixels(buffers)
}
/// Resets frame-level decoder state to prepare for decoding a new frame. /// /// This clears intermediate buffers (frame header, TOC, section data) while /// preserving image-level state (file header, color profiles, pixel format, /// reference frames). The box parser is restored to the correct /// mid-codestream state using `remaining_in_box`, so the next `process()` /// call correctly parses a new frame header from the input. /// /// # Arguments /// /// * `seek_target` -- from `VisibleFrameInfo::seek_target`. /// Includes both the box-parser state (`remaining_in_box`) and the input /// resume offset (`decode_start_file_offset`). /// /// After calling this, provide raw file input starting from /// `seek_target.decode_start_file_offset`. /// /// # Example /// /// ```rust,ignore /// // 1. Scan frame info using the regular decoder API. /// let options = JxlDecoderOptions { /// scan_frames_only: true, /// ..Default::default() /// }; /// let decoder = JxlDecoder::<states::Initialized>::new(options); /// // ...advance decoder and call `scanned_frames()`... /// /// // 2. Seek to frame N (bare codestream). /// let target = &frames[n]; /// decoder.start_new_frame(target.seek_target); /// // 3. Provide input from target.seek_target.decode_start_file_offset and process(). /// ``` pubfn start_new_frame(&mutself, seek_target: VisibleFrameSeekTarget) { self.inner.start_new_frame(seek_target);
}
impl JxlDecoder<WithFrameInfo> { /// Skip the current frame without decoding pixels. /// /// This reads section data from the input to advance past the frame, but /// does not render pixels. Reference frames that may be needed by later /// frames are still decoded internally. /// /// For efficient frame seeking in animations, enable /// `JxlDecoderOptions::scan_frames_only` and use /// [`scanned_frames`](JxlDecoder::scanned_frames), then /// [`start_new_frame`](JxlDecoder::start_new_frame) to jump directly to a /// target frame. pubfn skip_frame( mutself,
input: &mutimpl JxlBitstreamInput,
) -> Result<ProcessingResult<JxlDecoder<WithImageInfo>, Self>> { let inner_result = self.inner.process(input, None)?;
Ok(self.map_inner_processing_result(inner_result))
}
/// Number of passes we have full data for. pubfn num_completed_passes(&self) -> usize { self.inner.num_completed_passes().unwrap()
}
/// Draws all the pixels we have data for. /// /// Returns `true` if any new pixels were written to `buffers` since the /// previous call to `flush_pixels`; `false` if nothing new was rendered. /// /// Note: see `process` for alignment requirements for the buffer data. pubfn flush_pixels(&mutself, buffers: &mut [JxlOutputBuffer<'_>]) -> Result<bool> { self.inner.flush_pixels(buffers)
}
/// Guarantees to populate exactly the appropriate part of the buffers. /// Wants one buffer for each non-ignored pixel type, i.e. color channels and each extra channel. /// /// Note: the data in `buffers` should have alignment requirements that are compatible with the /// requested pixel format. This means that, if we are asking for 2-byte or 4-byte output (i.e. /// u16/f16 and f32 respectively), each row in the provided buffers must be aligned to 2 or 4 /// bytes respectively. If that is not the case, the library may panic. pubfn process<In: JxlBitstreamInput>( mutself,
input: &mutIn,
buffers: &mut [JxlOutputBuffer<'_>],
) -> Result<ProcessingResult<JxlDecoder<WithImageInfo>, Self>> { let inner_result = self.inner.process(input, Some(buffers))?;
Ok(self.map_inner_processing_result(inner_result))
}
}
#[cfg(test)] pub(crate) mod tests { usesuper::*; usecrate::api::{JxlDataFormat, JxlDecoderOptions}; usecrate::error::Error; usecrate::image::{Image, Rect}; use jxl_macros::for_each_test_file; use std::path::Path;
macro_rules! advance_decoder {
($decoder: ident $(, $extra_arg: expr)? $(; $flush_arg: expr)?) => { loop {
chunk_input =
&input[..(chunk_input.len().saturating_add(chunk_size)).min(input.len())]; let available_before = chunk_input.len(); let process_result = $decoder.process(&mut chunk_input $(, $extra_arg)?);
input = &input[(available_before - chunk_input.len())..]; match process_result.unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
$( letmut fallback = fallback; if do_flush && !input.is_empty() {
fallback.flush_pixels($flush_arg)?;
}
)? if input.is_empty() {
panic!("Unexpected end of input");
}
$decoder = fallback;
}
}
}
};
}
// Process until we have image info letmut decoder_with_image_info = advance_decoder!(initialized_decoder);
decoder_with_image_info.set_use_simple_pipeline(use_simple_pipeline);
// Get basic info let basic_info = decoder_with_image_info.basic_info().clone();
assert!(basic_info.bit_depth.bits_per_sample() > 0);
// Get image dimensions (after upsampling, which is the actual output size) let (buffer_width, buffer_height) = basic_info.size;
assert!(buffer_width > 0);
assert!(buffer_height > 0);
// Process until we have frame info letmut decoder_with_frame_info =
advance_decoder!(decoder_with_image_info; &mut api_buffers);
decoder_with_image_info =
advance_decoder!(decoder_with_frame_info, &mut api_buffers; &pan style='color:red'>mut api_buffers);
// All pixels should have been overwritten, so they should no longer be NaNs. for buf in buffers.iter() { let (xs, ys) = buf.size(); for y in0..ys { let row = buf.row(y); for (x, v) in row.iter().enumerate() {
assert!(!v.is_nan(), "NaN at {x} {y} (image size {xs}x{ys})");
}
}
}
frames.push(buffers);
// Check if there are more frames if !decoder_with_image_info.has_more_frames() { let decoded_frames = decoder_with_image_info.decoded_frames();
// Ensure we decoded at least one frame
assert!(decoded_frames > 0, "No frames were decoded");
let file = std::fs::read("resources/test/basic.jxl").unwrap(); let options = JxlDecoderOptions::default(); letmut decoder = JxlDecoder::<states::Initialized>::new(options); letmut input = file.as_slice(); letmut decoder = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
}
}; // Check default pixel format let default_format = decoder.current_pixel_format().clone();
assert_eq!(default_format.color_type, JxlColorType::Rgb);
// Set a new pixel format let new_format = JxlPixelFormat {
color_type: JxlColorType::Grayscale,
color_data_format: Some(JxlDataFormat::U8 { bit_depth: 8 }),
extra_channel_format: vec![],
};
decoder.set_pixel_format(new_format.clone());
// Verify it was set
assert_eq!(decoder.current_pixel_format(), &new_format);
}
// Get the embedded profile and set it as output (should work) let embedded = decoder.embedded_color_profile().clone(); let result = decoder.set_output_color_profile(embedded);
assert!(result.is_ok());
// Setting an ICC profile without CMS should fail let icc_profile = JxlColorProfile::Icc(vec![0u8; 100]); let result = decoder.set_output_color_profile(icc_profile);
assert!(result.is_err());
}
// Using test image with ICC profile to trigger default transfer function path let file = std::fs::read("resources/test/lossy_with_icc.jxl").unwrap(); let options = JxlDecoderOptions::default(); letmut decoder = JxlDecoder::<states::Initialized>::new(options); letmut input = file.as_slice(); letmut decoder = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => decoder = fallback,
}
};
// Output data format will default to F32, so output color profile will be linear sRGB
assert_eq!(
*decoder.output_color_profile().transfer_function().unwrap(),
JxlTransferFunction::Linear,
);
// Integer data format will set output color profile to sRGB
decoder.set_pixel_format(JxlPixelFormat::rgba8(0));
assert_eq!(
*decoder.output_color_profile().transfer_function().unwrap(),
JxlTransferFunction::SRGB,
);
// Once output color profile is set by user, it will remain as is regardless of what pixel // format is set let profile = JxlColorProfile::Simple(JxlColorEncoding::srgb(false));
decoder.set_output_color_profile(profile.clone()).unwrap();
decoder.set_pixel_format(JxlPixelFormat::rgba_f16(0));
assert!(decoder.output_color_profile() == &profile);
}
// Use basic.jxl which has no alpha channel let file = std::fs::read("resources/test/basic.jxl").unwrap();
// Request RGBA format even though image has no alpha let rgba_format = JxlPixelFormat {
color_type: JxlColorType::Rgba,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
};
// Test both pipelines (simple and low-memory) for use_simple in [true, false] { let options = JxlDecoderOptions::default(); let decoder = JxlDecoder::<states::Initialized>::new(options); letmut input = file.as_slice();
// Advance to image info
macro_rules! advance_decoder {
($decoder:expr) => { loop { match $decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => { if input.is_empty() {
panic!("Unexpected end of input");
}
$decoder = fallback;
}
}
}
};
($decoder:expr, $buffers:expr) => { loop { match $decoder.process(&mut input, $buffers).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => { if input.is_empty() {
panic!("Unexpected end of input");
}
$decoder = fallback;
}
}
}
};
}
// Decode frame let _decoder = advance_decoder!(decoder, &mut buffers);
// Verify all alpha values are 1.0 (opaque) for y in0..height { let row = color_buffer.row(y); for x in0..width { let alpha = row[x * 4 + 3];
assert_eq!(
alpha, 1.0, "Alpha at ({},{}) should be 1.0, got {} (use_simple={})",
x, y, alpha, use_simple
);
}
}
}
}
/// Test that premultiply_output=true produces premultiplied alpha output /// from a source with straight (non-premultiplied) alpha. #[test] fn test_premultiply_output_straight_alpha() { usecrate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
// Use alpha_nonpremultiplied.jxl which has straight alpha (alpha_associated=false) let file =
std::fs::read("resources/test/conformance_test_images/alpha_nonpremultiplied.jxl")
.unwrap();
// Alpha is included in RGBA, so we set extra_channel_format to None // to indicate no separate buffer for the alpha extra channel let rgba_format = JxlPixelFormat {
color_type: JxlColorType::Rgba,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![None],
};
// Test both pipelines for use_simple in [true, false] { let (straight_buffer, width, height) =
decode_with_format::<f32>(&file, &rgba_format, use_simple, false); let (premul_buffer, _, _) =
decode_with_format::<f32>(&file, &rgba_format, use_simple, true);
// Verify premultiplied values: premul_rgb should equal straight_rgb * alpha letmut found_semitransparent = false; for y in0..height { let straight_row = straight_buffer.row(y); let premul_row = premul_buffer.row(y); for x in0..width { let sr = straight_row[x * 4]; let sg = straight_row[x * 4 + 1]; let sb = straight_row[x * 4 + 2]; let sa = straight_row[x * 4 + 3];
let pr = premul_row[x * 4]; let pg = premul_row[x * 4 + 1]; let pb = premul_row[x * 4 + 2]; let pa = premul_row[x * 4 + 3];
// Alpha should be unchanged
assert!(
(sa - pa).abs() < 1e-5, "Alpha mismatch at ({},{}): straight={}, premul={} (use_simple={})",
x,
y,
sa,
pa,
use_simple
);
// Check premultiplication: premul = straight * alpha let expected_r = sr * sa; let expected_g = sg * sa; let expected_b = sb * sa;
// Allow 1% tolerance for precision differences between pipelines let tol = 0.01;
assert!(
(expected_r - pr).abs() < tol, "R mismatch at ({},{}): expected={}, got={} (use_simple={})",
x,
y,
expected_r,
pr,
use_simple
);
assert!(
(expected_g - pg).abs() < tol, "G mismatch at ({},{}): expected={}, got={} (use_simple={})",
x,
y,
expected_g,
pg,
use_simple
);
assert!(
(expected_b - pb).abs() < tol, "B mismatch at ({},{}): expected={}, got={} (use_simple={})",
x,
y,
expected_b,
pb,
use_simple
);
if sa > 0.01 && sa < 0.99 {
found_semitransparent = true;
}
}
}
// Ensure the test image actually has some semi-transparent pixels
assert!(
found_semitransparent, "Test image should have semi-transparent pixels (use_simple={})",
use_simple
);
}
}
/// Test that premultiply_output=true doesn't double-premultiply /// when the source already has premultiplied alpha (alpha_associated=true). #[test] fn test_premultiply_output_already_premultiplied() { usecrate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
// Use alpha_premultiplied.jxl which has alpha_associated=true let file = std::fs::read("resources/test/conformance_test_images/alpha_premultiplied.jxl")
.unwrap();
// Alpha is included in RGBA, so we set extra_channel_format to None let rgba_format = JxlPixelFormat {
color_type: JxlColorType::Rgba,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![None],
};
// Test both pipelines for use_simple in [true, false] { let (without_flag_buffer, width, height) =
decode_with_format::<f32>(&file, &rgba_format, use_simple, false); let (with_flag_buffer, _, _) =
decode_with_format::<f32>(&file, &rgba_format, use_simple, true);
// Both outputs should be identical since source is already premultiplied // and we shouldn't double-premultiply for y in0..height { let without_row = without_flag_buffer.row(y); let with_row = with_flag_buffer.row(y); for x in0..width { for c in0..4 { let without_val = without_row[x * 4 + c]; let with_val = with_row[x * 4 + c];
assert!(
(without_val - with_val).abs() < 1e-5, "Mismatch at ({},{}) channel {}: without_flag={}, with_flag={} (use_simple={})",
x,
y,
c,
without_val,
with_val,
use_simple
);
}
}
}
}
}
/// Test that animations with reference frames work correctly. /// This exercises the buffer index calculation fix where reference frame /// save stages use indices beyond the API-provided buffer array. #[test] fn test_animation_with_reference_frames() { usecrate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat}; usecrate::image::{Image, Rect};
// Use animation_spline.jxl which has multiple frames with references let file =
std::fs::read("resources/test/conformance_test_images/animation_spline.jxl").unwrap();
let options = JxlDecoderOptions::default(); let decoder = JxlDecoder::<states::Initialized>::new(options); letmut input = file.as_slice();
// Advance to image info letmut decoder = decoder; letmut decoder = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
decoder = fallback;
}
}
};
// Set RGB format with no extra channels let rgb_format = JxlPixelFormat {
color_type: JxlColorType::Rgb,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
};
decoder.set_pixel_format(rgb_format);
let basic_info = decoder.basic_info().clone(); let (width, height) = basic_info.size;
letmut frame_count = 0;
// Decode all frames loop { // Advance to frame info letmut decoder_frame = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
decoder = fallback;
}
}
};
// Use animation_spline.jxl which has multiple frames let file =
std::fs::read("resources/test/conformance_test_images/animation_spline.jxl").unwrap();
let options = JxlDecoderOptions::default(); let decoder = JxlDecoder::<states::Initialized>::new(options); letmut input = file.as_slice();
// Advance to image info letmut decoder = decoder; letmut decoder = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
decoder = fallback;
}
}
};
// Set RGB format let rgb_format = JxlPixelFormat {
color_type: JxlColorType::Rgb,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
};
decoder.set_pixel_format(rgb_format);
let basic_info = decoder.basic_info().clone(); let (width, height) = basic_info.size;
// Advance to frame info for first frame letmut decoder_frame = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
decoder = fallback;
}
}
};
// Skip the first frame (this is where the bug would leave stale frame state) letmut decoder = loop { match decoder_frame.skip_frame(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
decoder_frame = fallback;
}
}
};
assert!(
decoder.has_more_frames(), "Animation should have more frames"
);
// Advance to frame info for second frame // Without the fix, this would panic at assert!(self.frame.is_none()) letmut decoder_frame = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
decoder = fallback;
}
}
};
// Decode the second frame to verify everything works letmut color_buffer = Image::<f32>::new((width * 3, height)).unwrap(); letmut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
color_buffer
.get_rect_mut(Rect {
origin: (0, 0),
size: (width * 3, height),
})
.into_raw(),
)];
// If we got here without panicking, the fix works // Optionally verify we can continue with more frames let _ = decoder.has_more_frames();
}
/// Test that u8 output matches f32 output within quantization tolerance. /// This test would catch bugs like the offset miscalculation in PR #586 /// that caused black bars in u8 output. #[test] fn test_output_format_u8_matches_f32() { usecrate::api::{JxlColorType, JxlDataFormat, JxlPixelFormat};
// Use bicycles.jxl - a larger image that exercises offset calculations let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
// Test both RGB and BGRA to catch channel reordering bugs for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] { let f32_format = JxlPixelFormat {
color_type,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
}; let u8_format = JxlPixelFormat {
color_type,
color_data_format: Some(JxlDataFormat::U8 { bit_depth: 8 }),
extra_channel_format: vec![],
};
// Test both pipelines for use_simple in [true, false] { let (f32_buffer, width, height) =
decode_with_format::<f32>(&file, &f32_format, use_simple, false); let (u8_buffer, _, _) =
decode_with_format::<u8>(&file, &u8_format, use_simple, false);
// Compare values: u8 / 255.0 should match f32 // Tolerance: quantization error of ±0.5/255 ≈ 0.00196 plus small rounding let tolerance = 0.003; letmut max_error: f32 = 0.0;
for y in0..height { let f32_row = f32_buffer.row(y); let u8_row = u8_buffer.row(y); for x in0..(width * num_samples) { let f32_val = f32_row[x].clamp(0.0, 1.0); let u8_val = u8_row[x] as f32 / 255.0; let error = (f32_val - u8_val).abs();
max_error = max_error.max(error);
assert!(
error < tolerance, "{:?} u8 mismatch at ({},{}): f32={}, u8={} (scaled={}), error={} (use_simple={})",
color_type,
x,
y,
f32_val,
u8_row[x],
u8_val,
error,
use_simple
);
}
}
}
}
}
/// Test that u16 output matches f32 output within quantization tolerance. #[test] fn test_output_format_u16_matches_f32() { usecrate::api::{Endianness, JxlColorType, JxlDataFormat, JxlPixelFormat};
let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
// Test both RGB and BGRA for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] { let f32_format = JxlPixelFormat {
color_type,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
}; let u16_format = JxlPixelFormat {
color_type,
color_data_format: Some(JxlDataFormat::U16 {
endianness: Endianness::native(),
bit_depth: 16,
}),
extra_channel_format: vec![],
};
for use_simple in [true, false] { let (f32_buffer, width, height) =
decode_with_format::<f32>(&file, &f32_format, use_simple, false); let (u16_buffer, _, _) =
decode_with_format::<u16>(&file, &u16_format, use_simple, false);
// Tolerance: quantization error of ±0.5/65535 plus small rounding let tolerance = 0.0001;
for y in0..height { let f32_row = f32_buffer.row(y); let u16_row = u16_buffer.row(y); for x in0..(width * num_samples) { let f32_val = f32_row[x].clamp(0.0, 1.0); let u16_val = u16_row[x] as f32 / 65535.0; let error = (f32_val - u16_val).abs();
assert!(
error < tolerance, "{:?} u16 mismatch at ({},{}): f32={}, u16={} (scaled={}), error={} (use_simple={})",
color_type,
x,
y,
f32_val,
u16_row[x],
u16_val,
error,
use_simple
);
}
}
}
}
}
/// Test that f16 output matches f32 output within f16 precision tolerance. #[test] fn test_output_format_f16_matches_f32() { usecrate::api::{Endianness, JxlColorType, JxlDataFormat, JxlPixelFormat}; usecrate::util::f16;
let file = std::fs::read("resources/test/conformance_test_images/bicycles.jxl").unwrap();
// Test both RGB and BGRA for (color_type, num_samples) in [(JxlColorType::Rgb, 3), (JxlColorType::Bgra, 4)] { let f32_format = JxlPixelFormat {
color_type,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
}; let f16_format = JxlPixelFormat {
color_type,
color_data_format: Some(JxlDataFormat::F16 {
endianness: Endianness::native(),
}),
extra_channel_format: vec![],
};
for use_simple in [true, false] { let (f32_buffer, width, height) =
decode_with_format::<f32>(&file, &f32_format, use_simple, false); let (f16_buffer, _, _) =
decode_with_format::<f16>(&file, &f16_format, use_simple, false);
// f16 has about 3 decimal digits of precision // For values in [0,1], the relative error is about 0.001 let tolerance = 0.002;
for y in0..height { let f32_row = f32_buffer.row(y); let f16_row = f16_buffer.row(y); for x in0..(width * num_samples) { let f32_val = f32_row[x]; let f16_val = f16_row[x].to_f32(); let error = (f32_val - f16_val).abs();
assert!(
error < tolerance, "{:?} f16 mismatch at ({},{}): f32={}, f16={}, error={} (use_simple={})",
color_type,
x,
y,
f32_val,
f16_val,
error,
use_simple
);
}
}
}
}
}
/// Helper function to decode an image with a specific format. fn decode_with_format<T: crate::image::ImageDataType>(
file: &[u8],
pixel_format: &JxlPixelFormat,
use_simple: bool,
premultiply: bool,
) -> (Image<T>, usize, usize) { let options = JxlDecoderOptions {
premultiply_output: premultiply,
..Default::default()
}; letmut decoder = JxlDecoder::<states::Initialized>::new(options); letmut input = file;
// Advance to image info letmut decoder = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => { if input.is_empty() {
panic!("Unexpected end of input");
}
decoder = fallback;
}
}
};
decoder.set_use_simple_pipeline(use_simple);
decoder.set_pixel_format(pixel_format.clone());
let basic_info = decoder.basic_info().clone(); let (width, height) = basic_info.size;
let num_samples = pixel_format.color_type.samples_per_pixel();
// Advance to frame info let decoder = loop { match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => { if input.is_empty() {
panic!("Unexpected end of input");
}
decoder = fallback;
}
}
};
/// Regression test for ClusterFuzz issue 5342436251336704 /// Tests that malformed JXL files with overflow-inducing data don't panic #[test] fn test_fuzzer_smallbuffer_overflow() { use std::panic;
let data = include_bytes!("../../tests/testdata/fuzzer_smallbuffer_overflow.jxl");
// The test passes if it doesn't panic with "attempt to add with overflow" // It's OK if it returns an error or panics with "Unexpected end of input" let result = panic::catch_unwind(|| { let _ = decode(data, 1024, false, false, None);
});
// If it panicked, make sure it wasn't an overflow panic iflet Err(e) = result { let panic_msg = e
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| e.downcast_ref::<String>().cloned())
.unwrap_or_default();
assert!(
!panic_msg.contains("overflow"), "Unexpected overflow panic: {}",
panic_msg
);
}
}
fn make_box(ty: &[u8; 4], content: &[u8]) -> Vec<u8> { let len = (8 + content.len()) as u32; letmut buf = Vec::new();
buf.extend(len.to_be_bytes());
buf.extend(ty);
buf.extend(content);
buf
}
/// Helper to wrap a bare codestream in a JXL container with a jxli frame index box. fn wrap_with_frame_index(
codestream: &[u8],
tnum: u32,
tden: u32,
entries: &[(u64, u64, u64)], // (OFF_delta, T, F)
) -> Vec<u8> { usecrate::util::test::build_frame_index_content;
let jxli_content = build_frame_index_content(tnum, tden, entries);
let jxli = make_box(b"jxli", &jxli_content); let jxlc = make_box(b"jxlc", codestream);
/// Helper to wrap a bare codestream in a container split across jxlp boxes. /// /// `chunk_starts` are codestream offsets where each new jxlp chunk begins. fn wrap_with_jxlp_chunks(codestream: &[u8], chunk_starts: &[usize]) -> Vec<u8> { letmut starts = chunk_starts.to_vec();
starts.sort_unstable();
starts.dedup(); if starts.first().copied() != Some(0) {
starts.insert(0, 0);
} if starts.last().copied() != Some(codestream.len()) {
starts.push(codestream.len());
}
assert!(starts.len() >= 2);
let num_chunks = starts.len() - 1; for i in0..num_chunks { let begin = starts[i]; let end = starts[i + 1];
assert!(begin <= end && end <= codestream.len());
letmut payload = Vec::with_capacity(4 + (end - begin)); letmut index = i as u32; if i + 1 == num_chunks {
index |= 0x8000_0000;
}
payload.extend(index.to_be_bytes());
payload.extend(&codestream[begin..end]);
container.extend(make_box(b"jxlp", &payload));
}
container
}
#[test] fn test_frame_index_parsed_from_container() { // Read a bare animation codestream and wrap it in a container with a jxli box. let codestream =
std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
// Create synthetic frame index entries (delta offsets). // These are synthetic -- we don't know real frame offsets, but we can verify parsing. let entries = vec![
(0u64, 100u64, 1u64), // Frame 0 at offset 0
(500, 100, 1), // Frame 1 at offset 500
(600, 100, 1), // Frame 2 at offset 1100
];
let container = wrap_with_frame_index(&codestream, 1, 1000, &entries);
// Decode with a large chunk size so the jxli box is fully consumed. let options = JxlDecoderOptions::default(); letmut dec = JxlDecoder::<states::Initialized>::new(options); letmut input: &[u8] = &container; let dec = loop { match dec.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => { if input.is_empty() {
panic!("Unexpected end of input");
}
dec = fallback;
}
}
};
// Check that frame index was parsed. let fi = dec.frame_index().expect("frame_index should be Some");
assert_eq!(fi.num_frames(), 3);
assert_eq!(fi.tnum, 1);
assert_eq!(fi.tden.get(), 1000); // Verify absolute offsets (accumulated from deltas)
assert_eq!(fi.entries[0].codestream_offset, 0);
assert_eq!(fi.entries[1].codestream_offset, 500);
assert_eq!(fi.entries[2].codestream_offset, 1100);
assert_eq!(fi.entries[0].duration_ticks, 100);
assert_eq!(fi.entries[2].frame_count, 1);
}
#[test] fn test_frame_index_none_for_bare_codestream() { // A bare codestream has no container, so no frame index. let data =
std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap(); let options = JxlDecoderOptions::default(); letmut dec = JxlDecoder::<states::Initialized>::new(options); letmut input: &[u8] = &data; let dec = loop { match dec.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => { if input.is_empty() {
panic!("Unexpected end of input");
}
dec = fallback;
}
}
};
assert!(dec.frame_index().is_none());
}
// 1. Scan frame info to get seek offsets. let scanned_frames = scan_frames_with_decoder(data, usize::MAX);
// 2. Decode all frames sequentially and keep the reference frame. let (_n, sequential_frames) = decode(data, usize::MAX, false, false, None).unwrap();
arbtest::arbtest(|u| { // 3. Pick a random initial offset to ensure we can seek from intermediate states let initial_offset =
u.int_in_range(scanned_frames[0].file_offset as u64..=data.len() as u64)? as usize;
let num_seeks = u.int_in_range(1..=3)?; for _ in0..num_seeks { let target_visible_index =
u.int_in_range(0..=scanned_frames.len() as u64 - 1)? as usize; let seek_target = scanned_frames[target_visible_index].seek_target;
let expected = &sequential_frames[target_visible_index];
// 4. Seek to decode-start.
decoder.start_new_frame(seek_target); letmut input = &data[seek_target.decode_start_file_offset as usize..];
let basic_info = decoder.basic_info().unwrap().clone(); let (width, height) = basic_info.size;
// Match the same requested output format as the sequential helper. let default_format = decoder.current_pixel_format().unwrap().clone(); let requested_format = JxlPixelFormat {
color_type: default_format.color_type,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: default_format
.extra_channel_format
.iter()
.map(|_| Some(JxlDataFormat::f32()))
.collect(),
};
decoder.set_pixel_format(requested_format.clone());
let channels = requested_format.color_type.samples_per_pixel(); let num_ec = requested_format.extra_channel_format.len();
// Test intermediate states let available_bytes = input.len(); let extra_bytes = u.int_in_range(0..=available_bytes as u64)? as usize; if extra_bytes == 0 { continue;
} letmut extra_input = &input[..extra_bytes];
/// Test that `start_new_frame()` + scanner seek info decodes the same /// frame as regular sequential decode for bare codestream input. #[test] fn test_start_new_frame_bare_codestream() { let data =
std::fs::read("resources/test/conformance_test_images/animation_icos4d.jxl").unwrap();
assert_start_new_frame_matches_sequential(&data);
}
/// Test that `start_new_frame()` + scanner seek info also works for boxed input. #[test] fn test_start_new_frame_boxed_codestream() { let codestream =
std::fs::read("resources/test/conformance_test_images/animation_icos4d.jxl").unwrap(); let entries = vec![(0u64, 100u64, 1u64), (500, 100, 1), (600, 100, 1)]; let container = wrap_with_frame_index(&codestream, 1, 1000, &entries);
assert_start_new_frame_matches_sequential(&container);
}
/// Test seek/scanner behavior when codestream data is split across jxlp boxes, /// with each visible frame starting in its own chunk. #[test] fn test_start_new_frame_boxed_jxlp_per_visible_frame() { let codestream =
std::fs::read("resources/test/conformance_test_images/animation_icos4d.jxl").unwrap();
let (decoded_frames, _) = decode(&codestream, usize::MAX, false, false, None).unwrap();
assert_eq!(
decoded_frames,
scanned_frames.len(), "test file should have one codestream frame per visible frame",
);
let container = wrap_with_jxlp_chunks(&codestream, &chunk_starts);
assert_start_new_frame_matches_sequential(&container);
}
#[test] fn test_start_new_frame_cropped_traffic_light() { let data = std::fs::read("resources/test/cropped_traffic_light.jxl").unwrap();
assert_start_new_frame_matches_sequential(&data);
}
#[test] fn test_scan_still_image() { let data = std::fs::read("resources/test/green_queen_vardct_e3.jxl").unwrap(); let frames = scan_frames_with_decoder(&data, usize::MAX);
#[test] fn test_scan_bare_animation() { let data =
std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap(); let frames = scan_frames_with_decoder(&data, usize::MAX);
for (i, frame) in frames.iter().enumerate() {
assert_eq!(frame.index, i);
}
assert!(frames.last().unwrap().is_last);
assert!(frames[0].is_keyframe);
assert_eq!(
frames[0].seek_target.decode_start_file_offset,
frames[0].file_offset as u64
);
}
#[test] fn test_scan_animation_offsets_increase() { let data =
std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap(); let frames = scan_frames_with_decoder(&data, usize::MAX);
for i in1..frames.len() {
assert!(
frames[i].file_offset > frames[i - 1].file_offset, "frame {} offset {} should be > frame {} offset {}",
i,
frames[i].file_offset,
i - 1,
frames[i - 1].file_offset,
);
}
}
#[test] fn test_scan_incremental() { let data =
std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
let frames = scan_frames_with_decoder(&data, 128);
assert!(frames.len() > 1);
assert!(frames.last().unwrap().is_last);
}
#[test] fn test_scan_keyframe_detection_still() { let data = std::fs::read("resources/test/green_queen_vardct_e3.jxl").unwrap(); let frames = scan_frames_with_decoder(&data, usize::MAX);
assert_eq!(frames.len(), 1); let f = &frames[0];
assert!(f.is_keyframe);
assert_eq!(f.seek_target.decode_start_file_offset, f.file_offset as u64);
assert_eq!(f.seek_target.visible_frames_to_skip, 0);
}
#[test] fn test_scan_decode_start_file_offset_consistency() { let data =
std::fs::read("resources/test/conformance_test_images/animation_icos4d_5.jxl").unwrap();
let frames = scan_frames_with_decoder(&data, usize::MAX);
for frame in &frames {
assert!(
frame.seek_target.decode_start_file_offset <= frame.file_offset as u64, "frame {}: decode_start_file_offset {} > file_offset {}",
frame.index,
frame.seek_target.decode_start_file_offset,
frame.file_offset,
);
assert_eq!(
frame.is_keyframe,
frame.seek_target.visible_frames_to_skip == 0, "frame {}: keyframe flag should match visible_frames_to_skip",
frame.index,
);
}
}
#[test] fn test_scan_with_preview() { let data = std::fs::read("resources/test/with_preview.jxl"); if data.is_err() { return;
} let data = data.unwrap(); let frames = scan_frames_with_decoder(&data, usize::MAX);
assert!(frames.len() <= 1);
}
#[test] fn test_scan_patches_not_keyframe() { let data = std::fs::read("resources/test/grayscale_patches_var_dct.jxl"); if data.is_err() { return;
} let data = data.unwrap(); let frames = scan_frames_with_decoder(&data, usize::MAX);
assert!(!frames.is_empty());
}
/// Regression test for Chromium ClusterFuzz issue 474401148. #[test] fn test_fuzzer_xyb_icc_no_panic() { usecrate::api::ProcessingResult;
/// Regression test for Chromium ClusterFuzz issue 502853162. /// /// Scan-only decoding may consume all external input in one `process()` /// call while still having buffered frame data to finalize internally. /// A subsequent empty-input `process()` call must not panic. #[test] fn test_scan_frames_only_empty_followup_no_panic_502853162() { #[rustfmt::skip] let data: &[u8] = &[ 0xff, 0x0a, 0x31, 0xbd, 0xa2, 0xd0, 0x2a, 0x18, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xa0, 0x26, 0x00, 0xff,
];
letmut input = data; while decoder.has_more_frames() { let _ = decoder.process(&mut input, None).unwrap();
}
}
/// Small regression test for issue #728: squeeze transform boundary bug. #[test] fn test_squeeze_boundary_minimal() { let (_, frames) = decode(
&std::fs::read("resources/test/issue728_minimal.jxl").unwrap(),
usize::MAX, false, false,
None,
)
.unwrap();
assert_eq!(frames.len(), 1); let frame = &frames[0]; let buf = &frame[0]; let (xs, ys) = buf.size(); for y in0..ys { let row = buf.row(y); for (x, &v) in row.iter().enumerate().take(xs) {
assert!(
v == 0.0 || v == 1.0, "pixel ({}, {}) has value {v}, expected 0.0 or 1.0 \
(issue #728 squeeze boundary bug - minimal test)",
x / 3,
y,
);
}
}
}
/// Regression test for grid boundary bug with odd-width images (issue #728 variant). /// /// This test image is 257x256 solid blue (RGB 0, 0, 255). The width has a tail pixel /// which triggers a bug in grid-based decoding where hsqueeze is called with a /// 0-width residual rectangle for the rightmost grid cell. /// /// The bug was in the w==0 shortcut in do_hsqueeze_step() which looped over in_res /// height (0) instead of output height, so no pixels were copied. The fix ensures /// we loop over output dimensions to copy all pixels from in_avg. /// /// Before the fix: 512 pixels corrupted (2 rightmost columns × 256 rows) /// After the fix: All pixels decode correctly as blue (0.0, 0.0, 1.0) #[test] fn decode_test_strategic_solid_blue_grid_boundary() { let (_, frames) = decode(
&std::fs::read("resources/test/strategic_solid_blue.jxl").unwrap(),
usize::MAX, false, false,
None,
)
.unwrap();
assert_eq!(frames.len(), 1); let frame = &frames[0];
// First buffer contains interleaved RGB channels (3 channels) let buf = &frame[0]; let (xs, ys) = buf.size();
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.