use super::*;
use crate::j2c::ComponentData;
mod banded_tier1;
mod component_layout;
mod fused_last_level;
mod ht_refinement;
mod idwt_into_channels;
mod idwt_window_storage;
mod line_region_idwt;
#[cfg(feature = "std")]
mod packet_directory;
mod progression_matrix;
mod row_sink;
mod workspace_peak;
mod workspace_reuse;
#[test]
fn image_reports_high_throughput_block_coding_from_its_header() {
let pixels = vec![17_u8; 32 * 24];
let options = EncodeOptions::default();
let ht = encode_htj2k(&pixels, 32, 24, 1, 8, false, &options).expect("encode HTJ2K");
let classic = encode(&pixels, 32, 24, 1, 8, false, &options).expect("encode J2K");
let settings = DecodeSettings::default();
assert!(Image::new(&ht, &settings)
.expect("parse HTJ2K")
.uses_high_throughput_block_coding());
assert!(!Image::new(&classic, &settings)
.expect("parse J2K")
.uses_high_throughput_block_coding());
}
#[test]
fn direct_grayscale_plan_rejects_rgb_image_with_typed_reason() {
let pixels = vec![0, 16, 32, 64, 80, 96, 128, 144, 160, 192, 208, 224];
let bytes =
encode(&pixels, 2, 2, 3, 8, false, &EncodeOptions::default()).expect("encode rgb j2k");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let error = image
.build_direct_grayscale_plan_with_context(&mut context)
.expect_err("rgb image should not build a grayscale direct plan");
assert_eq!(
error,
DecodeError::Decoding(DecodingError::DirectPlanUnsupported(
DirectPlanUnsupportedReason::GrayscaleImageWithoutAlpha
))
);
}
#[test]
fn retained_parse_baseline_is_carried_into_direct_plan_allocation() {
let pixels = (0..64u8).collect::<Vec<_>>();
let bytes = encode(
&pixels,
8,
8,
1,
8,
false,
&EncodeOptions {
use_ht_block_coding: true,
num_decomposition_levels: 1,
..EncodeOptions::default()
},
)
.expect("encode bounded direct-plan fixture");
let settings = DecodeSettings::default();
Image::new(&bytes, &settings)
.expect("unbounded image")
.build_direct_grayscale_plan_with_context(&mut DecoderContext::default())
.expect("the fixture plans without a retained baseline");
let bounded =
Image::new_with_retained_baseline(&bytes, &settings, largest_parse_baseline(&bytes))
.expect("metadata exactly fits remaining parser budget");
let error = bounded
.build_direct_grayscale_plan_with_context(&mut DecoderContext::default())
.expect_err("direct-plan owners have no remaining aggregate headroom");
assert!(is_allocation_limit(&error), "{error:?}");
}
#[test]
fn retained_parse_baseline_is_carried_into_referenced_htj2k_plans() {
let settings = DecodeSettings::default();
for components in [1u16, 3] {
let pixels = (0..=u8::MAX)
.cycle()
.take(64 * usize::from(components))
.collect::<Vec<_>>();
let bytes = encode(
&pixels,
8,
8,
components,
8,
false,
&EncodeOptions {
use_ht_block_coding: true,
num_decomposition_levels: 1,
..EncodeOptions::default()
},
)
.expect("encode bounded referenced-plan fixture");
let region = (0, 0, 8, 8);
Image::new(&bytes, &settings)
.expect("unbounded image")
.build_referenced_htj2k_plan_region_with_context(&mut DecoderContext::default(), region)
.expect("the fixture plans without a retained baseline");
let bounded =
Image::new_with_retained_baseline(&bytes, &settings, largest_parse_baseline(&bytes))
.expect("metadata exactly fits remaining parser budget");
let error = bounded
.build_referenced_htj2k_plan_region_with_context(&mut DecoderContext::default(), region)
.expect_err("referenced-plan owners have no remaining aggregate headroom");
assert!(
is_allocation_limit(&error),
"{components} components: {error:?}"
);
}
}
fn largest_parse_baseline(bytes: &[u8]) -> usize {
let settings = DecodeSettings::default();
let mut accepted = 0usize;
let mut rejected = DEFAULT_MAX_DECODE_BYTES + 1;
while rejected - accepted > 1 {
let candidate = usize::midpoint(accepted, rejected);
if Image::new_with_retained_baseline(bytes, &settings, candidate).is_ok() {
accepted = candidate;
} else {
rejected = candidate;
}
}
accepted
}
fn is_allocation_limit(error: &DecodeError) -> bool {
matches!(
error,
DecodeError::Validation(ValidationError::ImageTooLarge)
| DecodeError::AllocationTooLarge { .. }
)
}
#[test]
fn ht_uvlc_encode_table_bytes_match_entry_packing_order() {
let entries = ht_uvlc_encode_table();
let bytes = ht_uvlc_encode_table_bytes();
assert_eq!(bytes.len(), entries.len() * 6);
for (index, entry) in entries.iter().enumerate() {
let offset = index * 6;
assert_eq!(
&bytes[offset..offset + 6],
&[
entry.pre,
entry.pre_len,
entry.suf,
entry.suf_len,
entry.ext,
entry.ext_len
],
);
}
}
#[test]
fn roi_maxshift_inverse_preserves_background_and_unshifts_roi_coefficients() {
assert_eq!(apply_roi_maxshift_inverse_i32(127, 7), 127);
assert_eq!(apply_roi_maxshift_inverse_i32(-127, 7), -127);
assert_eq!(apply_roi_maxshift_inverse_i32(128, 7), 1);
assert_eq!(apply_roi_maxshift_inverse_i32(-128, 7), -1);
assert_eq!(apply_roi_maxshift_inverse_i32(255, 7), 1);
assert_eq!(apply_roi_maxshift_inverse_i32(-255, 7), -1);
assert_eq!(apply_roi_maxshift_inverse_i32(256, 7), 2);
assert_eq!(apply_roi_maxshift_inverse_i32(-256, 7), -2);
assert_eq!(apply_roi_maxshift_inverse_i32(42, 0), 42);
assert_eq!(apply_roi_maxshift_inverse_i64(1_i64 << 38, 7), 1_i64 << 31);
assert_eq!(
apply_roi_maxshift_inverse_i64(-(1_i64 << 38), 7),
-(1_i64 << 31)
);
assert_eq!(
apply_roi_maxshift_inverse_f32(127.5, 7).to_bits(),
127.5_f32.to_bits()
);
assert_eq!(
apply_roi_maxshift_inverse_f32(128.5, 7).to_bits(),
(128.5_f32 / 128.0).to_bits()
);
assert_eq!(
apply_roi_maxshift_inverse_f32(-128.5, 7).to_bits(),
(-128.5_f32 / 128.0).to_bits()
);
}
#[test]
fn cielab_conversion_uses_b_range_independently_from_a_range() {
let mut components = vec![
lab_component(vec![0.0]),
lab_component(vec![0.0]),
lab_component(vec![255.0]),
];
let lab = CieLab {
rl: Some(100),
ol: Some(0),
ra: Some(100),
oa: Some(0),
rb: Some(220),
ob: Some(0),
};
dispatch!(Level::new(), simd => {
cielab_to_rgb(simd, &mut components, 8, &lab)
})
.expect("CIELab conversion succeeds");
let b = components[2].container.truncated()[0];
assert!(
(b - 348.0).abs() < 0.001,
"b channel must use rb=220, not ra=100; got {b}"
);
}
#[test]
fn cielab_conversion_honors_b_range_when_a_range_is_missing() {
let mut components = vec![
lab_component(vec![0.0]),
lab_component(vec![0.0]),
lab_component(vec![255.0]),
];
let lab = CieLab {
rl: Some(100),
ol: Some(0),
ra: None,
oa: Some(0),
rb: Some(220),
ob: Some(0),
};
dispatch!(Level::new(), simd => {
cielab_to_rgb(simd, &mut components, 8, &lab)
})
.expect("CIELab conversion succeeds");
let b = components[2].container.truncated()[0];
assert!(
(b - 348.0).abs() < 0.001,
"b channel must use explicit rb even when ra is absent; got {b}"
);
}
fn lab_component(samples: Vec<f32>) -> ComponentData {
ComponentData {
container: math::SimdBuffer::new(samples),
integer_container: None,
bit_depth: 8,
signed: false,
}
}
#[test]
fn classic_decode_adapter_accepts_legal_38_bit_roi_bitplane_count() {
assert_eq!(
add_roi_shift_to_bitplanes(38, 0, MAX_CLASSIC_DECODE_BITPLANES).unwrap(),
38
);
assert_eq!(
add_roi_shift_to_bitplanes(37, 1, MAX_CLASSIC_DECODE_BITPLANES).unwrap(),
38
);
}
#[test]
fn classic_scalar_decode_applies_nonzero_roi_maxshift() {
let roi_shift = 3;
let total_bitplanes = 3;
let style = J2kCodeBlockStyle {
selective_arithmetic_coding_bypass: false,
reset_context_probabilities: false,
termination_on_each_pass: false,
vertically_causal_context: false,
segmentation_symbols: false,
};
let coded_coefficients = [0, 5, 1 << roi_shift, -(2 << roi_shift)];
let encoded = encode_j2k_code_block_scalar_with_style(
&coded_coefficients,
2,
2,
J2kSubBandType::LowLow,
total_bitplanes + roi_shift,
style,
)
.expect("encode ROI-shifted code block");
let job = J2kCodeBlockDecodeJob {
data: &encoded.data,
segments: &encoded.segments,
width: 2,
height: 2,
output_stride: 2,
missing_bit_planes: encoded.missing_bit_planes,
number_of_coding_passes: encoded.number_of_coding_passes,
total_bitplanes,
roi_shift,
sub_band_type: J2kSubBandType::LowLow,
style,
strict: true,
dequantization_step: 1.0,
};
let mut output = [0.0; 4];
decode_j2k_code_block_scalar(job, &mut output).expect("decode ROI-shifted code block");
assert_eq!(output, [0.0, 5.0, 1.0, -2.0]);
}
#[test]
fn classic_scalar_token_pack_matches_scalar_single_cleanup_block() {
let style = J2kCodeBlockStyle {
selective_arithmetic_coding_bypass: true,
reset_context_probabilities: false,
termination_on_each_pass: false,
vertically_causal_context: false,
segmentation_symbols: false,
};
let scalar =
encode_j2k_code_block_scalar_with_style(&[1], 1, 1, J2kSubBandType::LowLow, 1, style)
.expect("encode scalar");
let token_bytes = pack_mq_test_tokens(&[(0, 1), (9, 0)]);
let packed = pack_j2k_code_block_scalar_from_tier1_tokens(
&token_bytes,
&[J2kTier1TokenSegment {
token_bit_offset: 0,
token_bit_count: 12,
start_coding_pass: 0,
end_coding_pass: 1,
use_arithmetic: true,
}],
scalar.number_of_coding_passes,
scalar.missing_bit_planes,
)
.expect("pack tokens");
assert_eq!(packed.data, scalar.data);
assert_eq!(packed.segments, scalar.segments);
assert_eq!(
packed.number_of_coding_passes,
scalar.number_of_coding_passes
);
assert_eq!(packed.missing_bit_planes, scalar.missing_bit_planes);
}
#[test]
fn scalar_encode_adapters_preserve_typed_input_and_cap_categories() {
let style = J2kCodeBlockStyle {
selective_arithmetic_coding_bypass: false,
reset_context_probabilities: false,
termination_on_each_pass: false,
vertically_causal_context: false,
segmentation_symbols: false,
};
assert!(matches!(
encode_j2k_code_block_scalar_with_style(&[1], 2, 2, J2kSubBandType::LowLow, 1, style,),
Err(EncodeError::InvalidInput {
what: "contiguous coefficient block length mismatch",
})
));
assert!(matches!(
encode_ht_code_block_scalar(&[0], 1, 1, 0),
Err(EncodeError::InvalidInput {
what: "HTJ2K scalar encoder currently supports 1..=31 bitplanes",
})
));
assert!(matches!(
encode_ht_code_block_scalar_with_passes(&[0], 1, 1, 1, 4),
Err(EncodeError::InvalidInput {
what: "HTJ2K scalar encoder currently supports cleanup, sigprop, and one magref refinement pass",
})
));
let error = pack_j2k_code_block_scalar_from_tier1_tokens(
&[],
&[J2kTier1TokenSegment {
token_bit_offset: 0,
token_bit_count: u32::MAX - 3,
start_coding_pass: 0,
end_coding_pass: 1,
use_arithmetic: true,
}],
1,
0,
)
.expect_err("oversized token payload must fail before reading token bytes");
assert!(matches!(
error,
EncodeError::AllocationTooLarge {
what: "classic Tier-1 token worker allocation",
requested,
cap: DEFAULT_MAX_CODEC_BYTES,
} if requested > DEFAULT_MAX_CODEC_BYTES
));
}
fn pack_mq_test_tokens(tokens: &[(u8, u8)]) -> Vec<u8> {
let mut bytes = Vec::new();
let mut current = 0u8;
let mut bits = 0u8;
for &(ctx, bit) in tokens {
let value = (ctx & 0x1F) | ((bit & 1) << 5);
for shift in (0..6).rev() {
current = (current << 1) | ((value >> shift) & 1);
bits += 1;
if bits == 8 {
bytes.push(current);
current = 0;
bits = 0;
}
}
}
if bits != 0 {
bytes.push(current << (8 - bits));
}
bytes
}
#[test]
fn classic_scalar_profiled_decode_matches_unprofiled_decode() {
let width = 64u32;
let height = 64u32;
let sample_count = width as usize * height as usize;
let total_bitplanes = 12;
let style = default_classic_test_style();
let coefficients = (0..sample_count)
.map(|idx| {
let value = i32::try_from((idx * 37) % 4095).expect("sample value fits i32") - 2048;
if idx % 17 == 0 {
0
} else {
value
}
})
.collect::<Vec<_>>();
let encoded = encode_j2k_code_block_scalar_with_style(
&coefficients,
width,
height,
J2kSubBandType::LowLow,
total_bitplanes,
style,
)
.expect("encode classic block");
let job = classic_low_low_decode_job(&encoded, width, height, total_bitplanes, style);
let mut expected = vec![0.0_f32; sample_count];
let mut actual = vec![0.0_f32; sample_count];
let mut profile = J2kCodeBlockDecodeProfile::default();
decode_j2k_code_block_scalar(job, &mut expected).expect("unprofiled classic decode");
decode_j2k_code_block_scalar_profiled(job, &mut actual, &mut profile)
.expect("profiled classic decode");
assert_eq!(actual, expected);
#[cfg(feature = "profile")]
assert!(profile.cleanup_us > 0);
#[cfg(not(feature = "profile"))]
assert_eq!(profile.cleanup_us, 0);
}
#[test]
fn classic_scalar_workspace_reuse_matches_fresh_decode() {
let total_bitplanes = 6;
let style = default_classic_test_style();
let mut workspace = J2kCodeBlockDecodeWorkspace::default();
for (width, height, seed) in [(8, 8, 0x31), (4, 16, 0x47)] {
let coefficients = (0..width * height)
.map(|idx| {
let idx = i32::try_from(idx).expect("small test index fits i32");
let value = ((idx * seed) % 23) - 11;
if idx % 7 == 0 {
0
} else {
value
}
})
.collect::<Vec<_>>();
let encoded = encode_j2k_code_block_scalar_with_style(
&coefficients,
width,
height,
J2kSubBandType::LowLow,
total_bitplanes,
style,
)
.expect("encode classic block");
let job = classic_low_low_decode_job(&encoded, width, height, total_bitplanes, style);
let mut fresh = vec![0.0_f32; width as usize * height as usize];
let mut reused = vec![0.0_f32; width as usize * height as usize];
decode_j2k_code_block_scalar(job, &mut fresh).expect("fresh classic decode");
decode_j2k_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
.expect("workspace classic decode");
assert_eq!(reused, fresh);
}
}
fn default_classic_test_style() -> J2kCodeBlockStyle {
J2kCodeBlockStyle {
selective_arithmetic_coding_bypass: false,
reset_context_probabilities: false,
termination_on_each_pass: false,
vertically_causal_context: false,
segmentation_symbols: false,
}
}
fn classic_low_low_decode_job(
encoded: &EncodedJ2kCodeBlock,
width: u32,
height: u32,
total_bitplanes: u8,
style: J2kCodeBlockStyle,
) -> J2kCodeBlockDecodeJob<'_> {
J2kCodeBlockDecodeJob {
data: &encoded.data,
segments: &encoded.segments,
width,
height,
output_stride: width as usize,
missing_bit_planes: encoded.missing_bit_planes,
number_of_coding_passes: encoded.number_of_coding_passes,
total_bitplanes,
roi_shift: 0,
sub_band_type: J2kSubBandType::LowLow,
style,
strict: true,
dequantization_step: 1.0,
}
}
#[test]
fn scalar_packetization_rejects_overflowing_ht_refinement_lengths_without_panic() {
let payload = [0x12];
let block = J2kPacketizationCodeBlock {
data: &payload,
ht_cleanup_length: u32::MAX,
ht_refinement_length: 1,
num_coding_passes: 3,
num_zero_bitplanes: 2,
previously_included: false,
l_block: 3,
block_coding_mode: J2kPacketizationBlockCodingMode::HighThroughput,
};
let subband = J2kPacketizationSubband {
code_blocks: vec![block],
num_cbs_x: 1,
num_cbs_y: 1,
};
let resolution = J2kPacketizationResolution {
subbands: vec![subband],
};
let resolutions = [resolution];
let job = J2kPacketizationEncodeJob {
resolution_count: 1,
num_layers: 1,
num_components: 1,
code_block_count: 1,
progression_order: J2kPacketizationProgressionOrder::Lrcp,
packet_descriptors: &[],
resolutions: &resolutions,
};
let err = encode_j2k_packetization_scalar(job)
.expect_err("overflowing HT packetization segment lengths rejected");
assert_eq!(
err,
EncodeError::ArithmeticOverflow {
what: "multi-pass HTJ2K packet contribution length overflow",
}
);
}
#[derive(Default)]
struct DecodeWorkCounter {
classic_code_blocks: usize,
ht_code_blocks: usize,
idwt_output_samples: usize,
stored_coefficients: usize,
}
impl DecodeWorkCounter {
fn code_blocks(&self) -> usize {
self.classic_code_blocks + self.ht_code_blocks
}
}
struct FailingHtDecoder {
called: bool,
}
impl HtCodeBlockDecoder for FailingHtDecoder {
fn decode_code_block(
&mut self,
_job: HtCodeBlockDecodeJob<'_>,
_output: &mut [f32],
) -> Result<()> {
self.called = true;
Err(DecodingError::CodeBlockDecodeFailure.into())
}
}
struct FailingClassicDecoder {
called: bool,
}
impl HtCodeBlockDecoder for FailingClassicDecoder {
fn decode_code_block(
&mut self,
_job: HtCodeBlockDecodeJob<'_>,
_output: &mut [f32],
) -> Result<()> {
panic!("HT hook must not be used for classic J2K test")
}
fn decode_j2k_code_block(
&mut self,
_job: J2kCodeBlockDecodeJob<'_>,
_output: &mut [f32],
) -> Result<bool> {
self.called = true;
Err(DecodingError::CodeBlockDecodeFailure.into())
}
}
struct FailingClassicBatchDecoder {
called: bool,
}
impl HtCodeBlockDecoder for FailingClassicBatchDecoder {
fn decode_code_block(
&mut self,
_job: HtCodeBlockDecodeJob<'_>,
_output: &mut [f32],
) -> Result<()> {
panic!("HT hook must not be used for classic J2K batch test")
}
fn decode_j2k_code_block(
&mut self,
_job: J2kCodeBlockDecodeJob<'_>,
_output: &mut [f32],
) -> Result<bool> {
panic!("per-block classic hook must not be used when the batch hook handles the sub-band")
}
fn decode_j2k_sub_band(
&mut self,
_job: J2kSubBandDecodeJob<'_>,
_output: &mut [f32],
) -> Result<bool> {
self.called = true;
Err(DecodingError::CodeBlockDecodeFailure.into())
}
}
fn fixture() -> Vec<u8> {
let pixels = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
..EncodeOptions::default()
};
encode(&pixels, 2, 2, 3, 8, false, &options).expect("encode")
}
#[test]
fn decode_into_rejects_short_output_buffer() {
let bytes = fixture();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let mut output = vec![0; 11];
let err = image
.decode_into(&mut output, &mut context)
.expect_err("short output buffer must be rejected");
assert_eq!(
err,
DecodeError::Decoding(DecodingError::OutputBufferTooSmall)
);
}
fn fixture_multi_block() -> Vec<u8> {
let pixels: Vec<u8> = (0..64).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
..EncodeOptions::default()
};
encode(&pixels, 8, 8, 1, 8, false, &options).expect("encode multi-block classic")
}
fn fixture_gray() -> Vec<u8> {
let pixels: Vec<u8> = (0..16).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
..EncodeOptions::default()
};
encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode classic gray8")
}
#[test]
fn reversible_coefficient_handoff_is_complete_and_releases_decode_storage() {
let bytes = fixture_gray();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let coefficients = image
.decode_reversible_53_coefficients_with_context(&mut context)
.expect("extract reversible coefficients");
assert_eq!(coefficients.image.components.len(), 1);
let component = &coefficients.image.components[0].dwt;
let coefficient_count = component.ll.len()
+ component
.levels
.iter()
.map(|level| level.hl.len() + level.lh.len() + level.hh.len())
.sum::<usize>();
assert_eq!(coefficient_count, 16);
assert_eq!(context.storage.retained_capacity_bytes().unwrap(), 0);
assert_eq!(
context.tile_decode_context.tier1_capacity_bytes().unwrap(),
0
);
}
#[test]
fn repeated_decode_and_recode_reuse_immutable_tile_part_metadata() {
let bytes = fixture_gray();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let ht_options = EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
use_ht_block_coding: true,
..EncodeOptions::default()
};
let initial_pixels = image
.decode_native_with_context(&mut context)
.expect("first decode");
let initial_coefficients = image
.decode_reversible_53_coefficients_with_context(&mut context)
.expect("first coefficient extraction");
let initial_codestream = initial_coefficients
.encode_htj2k(&ht_options)
.expect("first coefficient recode");
let repeated_pixels = image
.decode_native_with_context(&mut context)
.expect("second decode");
let repeated_coefficients = image
.decode_reversible_53_coefficients_with_context(&mut context)
.expect("second coefficient extraction");
let repeated_codestream = repeated_coefficients
.encode_htj2k(&ht_options)
.expect("second coefficient recode");
assert_eq!(repeated_pixels.data, initial_pixels.data);
assert_eq!(repeated_codestream, initial_codestream);
}
#[test]
fn repeated_direct_plan_build_reuses_immutable_tile_part_metadata() {
let bytes = fixture_gray();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let first = image
.build_direct_grayscale_plan_with_context(&mut context)
.expect("first direct plan");
let second = image
.build_direct_grayscale_plan_with_context(&mut context)
.expect("second direct plan");
assert_eq!(second.dimensions, first.dimensions);
assert_eq!(second.bit_depth, first.bit_depth);
assert_eq!(second.steps.len(), first.steps.len());
for (second_step, first_step) in second.steps.iter().zip(&first.steps) {
assert_eq!(
core::mem::discriminant(second_step),
core::mem::discriminant(first_step)
);
}
}
#[test]
fn native_bytes_per_sample_tracks_high_bit_depths() {
for (bit_depth, expected) in [
(1_u8, 1_usize),
(8, 1),
(9, 2),
(16, 2),
(17, 3),
(24, 3),
(32, 4),
(38, 5),
] {
assert_eq!(native_bytes_per_sample(bit_depth).unwrap(), expected);
}
}
#[test]
fn native_sample_packing_writes_high_bit_unsigned_little_endian_bytes() {
let mut out = Vec::new();
Image::push_native_sample_bytes(&mut out, 1_193_046.0, 24, false);
assert_eq!(out, [0x56, 0x34, 0x12]);
out.clear();
Image::push_native_sample_bytes(&mut out, f32::MAX, 24, false);
assert_eq!(out, [0xff, 0xff, 0xff]);
}
#[test]
fn native_sample_packing_writes_high_bit_signed_little_endian_bytes() {
let mut out = Vec::new();
Image::push_native_sample_bytes(&mut out, -1.0, 38, true);
assert_eq!(out, [0xff, 0xff, 0xff, 0xff, 0xff]);
out.clear();
Image::push_native_sample_bytes(&mut out, -137_438_953_472.0, 38, true);
assert_eq!(out, [0x00, 0x00, 0x00, 0x00, 0xe0]);
}
#[test]
#[expect(
clippy::cast_precision_loss,
reason = "sample magnitudes stay below 2^17 and are exact in f32"
)]
fn chunked_native_sample_packing_matches_per_sample_packing() {
let mut samples = vec![
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
0.0,
-0.0,
f32::MIN_POSITIVE,
-f32::MIN_POSITIVE,
0.499_999_97,
-0.499_999_97,
f32::MAX,
f32::MIN,
];
for whole in -70_000_i32..=70_000 {
if whole.unsigned_abs() > 300 && whole % 97 != 0 && whole.unsigned_abs() < 32_000 {
continue;
}
let whole = whole as f32;
samples.extend([whole, whole + 0.5, whole - 0.5, whole + 0.25, whole - 0.75]);
samples.extend([whole.next_up(), whole.next_down()]);
samples.extend([(whole + 0.5).next_down(), (whole - 0.5).next_up()]);
}
for bit_depth in 1_u8..=20 {
for signed in [false, true] {
let component = crate::j2c::ComponentData {
container: crate::math::SimdBuffer::new(samples.clone()),
integer_container: None,
bit_depth,
signed,
};
let mut expected = Vec::new();
for index in 0..samples.len() {
Image::push_component_native_sample_bytes(
&mut expected,
&component,
index,
bit_depth,
);
}
for parallel in [false, true] {
let mut actual = Vec::with_capacity(expected.len());
Image::extend_component_native_sample_bytes(
&mut actual,
&component,
0..samples.len(),
bit_depth,
parallel,
);
assert_eq!(
actual, expected,
"bit_depth={bit_depth} signed={signed} parallel={parallel}"
);
}
}
}
}
fn rewrite_siz_to_single_large_tile(codestream: &mut [u8], dimensions: u32) {
let siz = codestream
.windows(2)
.position(|w| w == [0xFF, 0x51])
.expect("SIZ marker");
codestream[siz + 6..siz + 10].copy_from_slice(&dimensions.to_be_bytes());
codestream[siz + 10..siz + 14].copy_from_slice(&dimensions.to_be_bytes());
codestream[siz + 22..siz + 26].copy_from_slice(&dimensions.to_be_bytes());
codestream[siz + 26..siz + 30].copy_from_slice(&dimensions.to_be_bytes());
}
fn rewrite_siz_tile_grid(codestream: &mut [u8], dimensions: (u32, u32), tile_size: (u32, u32)) {
let siz = codestream
.windows(2)
.position(|w| w == [0xFF, 0x51])
.expect("SIZ marker");
codestream[siz + 6..siz + 10].copy_from_slice(&dimensions.0.to_be_bytes());
codestream[siz + 10..siz + 14].copy_from_slice(&dimensions.1.to_be_bytes());
codestream[siz + 22..siz + 26].copy_from_slice(&tile_size.0.to_be_bytes());
codestream[siz + 26..siz + 30].copy_from_slice(&tile_size.1.to_be_bytes());
}
fn rewrite_siz_component_count(codestream: &mut Vec<u8>, component_count: u16) {
let siz = codestream
.windows(2)
.position(|w| w == [0xFF, 0x51])
.expect("SIZ marker");
let old_component_count =
u16::from_be_bytes([codestream[siz + 38], codestream[siz + 39]]) as usize;
let component_start = siz + 40;
let component_end = component_start + old_component_count * 3;
let descriptor = codestream[component_start..component_start + 3].to_vec();
let mut descriptors = Vec::with_capacity(usize::from(component_count) * 3);
for _ in 0..component_count {
descriptors.extend_from_slice(&descriptor);
}
let siz_len = 38_u16
.checked_add(
component_count
.checked_mul(3)
.expect("SIZ component bytes fit"),
)
.expect("SIZ length fits");
codestream[siz + 2..siz + 4].copy_from_slice(&siz_len.to_be_bytes());
codestream[siz + 38..siz + 40].copy_from_slice(&component_count.to_be_bytes());
codestream.splice(component_start..component_end, descriptors);
}
#[test]
fn inspect_rejects_component_count_above_j2k_spec_cap() {
let mut bytes = fixture_gray();
rewrite_siz_component_count(&mut bytes, MAX_J2K_SPEC_COMPONENTS + 1);
let err = inspect_j2k_codestream_header(&bytes)
.expect_err("SIZ component count above spec cap must be rejected");
assert_eq!(
err,
J2kCodestreamHeaderError::InvalidSiz {
what: "component count exceeds JPEG 2000 limit"
}
);
}
#[test]
fn inspect_fallibly_materializes_the_maximum_component_metadata() {
let mut bytes = fixture_gray();
rewrite_siz_component_count(&mut bytes, MAX_J2K_SPEC_COMPONENTS);
let metadata = inspect_j2k_codestream_header(&bytes)
.expect("maximum JPEG 2000 component metadata remains inspectable");
assert_eq!(metadata.components, MAX_J2K_SPEC_COMPONENTS);
assert_eq!(
metadata.component_info.len(),
usize::from(MAX_J2K_SPEC_COMPONENTS)
);
}
#[test]
fn native_parse_accepts_spec_component_count_above_u8() {
let mut bytes = fixture_gray();
rewrite_siz_component_count(&mut bytes, MAX_J2K_SPEC_COMPONENTS + 1);
let Err(err) = Image::new(&bytes, &DecodeSettings::default()) else {
panic!("component count above the JPEG 2000 spec cap must still reject");
};
assert_eq!(
err,
DecodeError::Validation(ValidationError::TooManyChannels)
);
let mut bytes = fixture_gray();
rewrite_siz_component_count(&mut bytes, 256);
Image::new(&bytes, &DecodeSettings::default())
.expect("component count above u8 should parse within the JPEG 2000 spec cap");
}
#[test]
fn tile_parse_rejects_component_tile_structural_bomb_before_allocation() {
let mut bytes = fixture_gray();
rewrite_siz_component_count(&mut bytes, MAX_J2K_SPEC_COMPONENTS);
rewrite_siz_tile_grid(&mut bytes, (256, 256), (1, 1));
let parsed = j2c::parse_raw(&bytes, &DecodeSettings::default()).expect("raw header parses");
let mut context = j2c::DecoderContext::default();
let mut ht_decoder: Option<&mut dyn HtCodeBlockDecoder> = None;
let retained_header_bytes = j2c::codestream::allocation::retained_header_bytes(&parsed.header)
.expect("parsed header capacity");
let err = j2c::decode(
parsed.data,
&parsed.header,
retained_header_bytes,
&mut context,
&mut ht_decoder,
#[cfg(feature = "std")]
None,
)
.expect_err("tile structural budget must reject before tile allocation");
assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
}
#[test]
fn retained_container_metadata_rejects_header_parse_before_decode_growth() {
let bytes = fixture_gray();
let Err(error) = j2c::parse_raw_with_retained_baseline(
&bytes,
&DecodeSettings::default(),
DEFAULT_MAX_DECODE_BYTES,
None,
) else {
panic!("a full retained-container baseline must leave no room for the header");
};
assert!(matches!(
error,
DecodeError::AllocationTooLarge {
what: "native codestream header metadata",
requested,
cap: DEFAULT_MAX_DECODE_BYTES,
} if requested > DEFAULT_MAX_DECODE_BYTES
));
}
#[test]
fn owned_decode_rejects_large_siz_before_allocating_output() {
let mut bytes = fixture_gray();
rewrite_siz_to_single_large_tile(&mut bytes, 65_537);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
let Err(err) = image.decode() else {
panic!("large owned decode must be capped");
};
assert_large_siz_component_budget(&err);
}
#[test]
fn decode_into_rejects_large_siz_before_allocating_component_storage() {
let mut bytes = fixture_gray();
rewrite_siz_to_single_large_tile(&mut bytes, 65_537);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
let mut context = DecoderContext::default();
let mut out = [];
let Err(err) = image.decode_into(&mut out, &mut context) else {
panic!("component storage must be capped before allocation");
};
assert_large_siz_component_budget(&err);
assert!(
context
.tile_decode_context
.channel_data
.iter()
.all(|component| component.container.capacity() == 0
&& component.integer_container.is_none()),
"the cap must reject before component sample owners are allocated"
);
}
fn assert_large_siz_component_budget(error: &DecodeError) {
match error {
DecodeError::AllocationTooLarge {
what,
requested,
cap,
} => {
assert_eq!(*what, "native decoder context retained components");
assert!(*requested > *cap);
assert_eq!(*cap, DEFAULT_MAX_DECODE_BYTES);
}
other => panic!("unexpected large-SIZ rejection category: {other:?}"),
}
}
#[test]
fn decode_region_budgets_coefficients_for_the_requested_region() {
let mut codestream = fixture_gray();
rewrite_siz_to_single_large_tile(&mut codestream, 65_537);
let image = Image::new(&codestream, &DecodeSettings::default()).expect("large SIZ parses");
let Err(tiny) = image.decode_region((0, 0, 1, 1)) else {
panic!("rewritten fixture has no packet data for a 65537x65537 tile");
};
assert!(
matches!(tiny, DecodeError::Decoding(_)),
"tiny ROI must get past the coefficient budget: {tiny:?}"
);
let Err(large) = image.decode_region((0, 0, 9_000, 9_000)) else {
panic!("a 9000x9000 region must exceed the coefficient budget");
};
assert_eq!(
large,
DecodeError::Validation(ValidationError::ImageTooLarge)
);
}
struct DecliningHtDecoder;
impl HtCodeBlockDecoder for DecliningHtDecoder {}
#[test]
fn decode_region_budgets_the_windowed_idwt_peak_before_packet_parsing() {
let mut codestream = fixture_gray();
rewrite_siz_to_single_large_tile(&mut codestream, 7_168);
let image = Image::new(&codestream, &DecodeSettings::default()).expect("large SIZ parses");
let mut context = DecoderContext::default();
let Err(hooked) = image.decode_region_components_with_ht_decoder(
&mut context,
(0, 0, 7_168, 7_168),
&mut DecliningHtDecoder,
) else {
panic!("rewritten fixture has no packet data for a 7168x7168 tile");
};
assert_eq!(
hooked,
DecodeError::Validation(ValidationError::ImageTooLarge),
"the windowed IDWT peak must be charged before packet parsing"
);
let Err(full) = image.decode_region((0, 0, 7_168, 7_168)) else {
panic!("rewritten fixture has no packet data for a 7168x7168 tile");
};
assert!(
matches!(full, DecodeError::Decoding(_)),
"a last level written into the output plane must get past the budget: {full:?}"
);
let mut context = DecoderContext::default();
let Err(smaller) = image.decode_region_components_with_ht_decoder(
&mut context,
(0, 0, 6_000, 6_000),
&mut DecliningHtDecoder,
) else {
panic!("rewritten fixture has no packet data for a 6000x6000 region");
};
assert!(
matches!(smaller, DecodeError::Decoding(_)),
"a region whose IDWT peak fits must get past the budget: {smaller:?}"
);
}
#[test]
fn decode_region_without_levels_budgets_the_ll_window_only_where_it_is_copied() {
let mut codestream = fixture_gray();
rewrite_siz_to_single_large_tile(&mut codestream, 14_336);
let image = Image::new_with_reduction(&codestream, &DecodeSettings::default(), 1)
.expect("large SIZ parses");
let region = (0, 0, 7_168, 7_168);
let Err(streamed) = image.decode_region(region) else {
panic!("rewritten fixture has no packet data for a 14336x14336 tile");
};
assert!(
matches!(streamed, DecodeError::Decoding(_)),
"a band written into the output plane must get past the budget: {streamed:?}"
);
let mut context = DecoderContext::default();
let Err(hooked) = image.decode_region_components_with_ht_decoder(
&mut context,
region,
&mut DecliningHtDecoder,
) else {
panic!("rewritten fixture has no packet data for a 14336x14336 tile");
};
assert_eq!(
hooked,
DecodeError::Validation(ValidationError::ImageTooLarge),
"a copied LL window must be charged before packet parsing"
);
}
#[test]
fn rgb_decode_budgets_one_component_of_coefficients_at_a_time() {
let mut codestream = fixture();
rewrite_siz_to_single_large_tile(&mut codestream, 4_800);
let image = Image::new(&codestream, &DecodeSettings::default()).expect("large SIZ parses");
for (label, result) in [
("whole", image.decode().err()),
("region", image.decode_region((0, 0, 4_800, 4_800)).err()),
] {
assert!(
matches!(result, Some(DecodeError::Decoding(_))),
"{label} decode must get past the budget: {result:?}"
);
}
let mut codestream = fixture();
rewrite_siz_to_single_large_tile(&mut codestream, 5_500);
let image = Image::new(&codestream, &DecodeSettings::default()).expect("large SIZ parses");
assert_eq!(
image.decode().err(),
Some(DecodeError::Validation(ValidationError::ImageTooLarge))
);
}
fn many_precinct_layer_codestream(dimensions: u32) -> Vec<u8> {
let pixels: Vec<u8> = (0..16).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
precinct_exponents: vec![(15, 15), (15, 15)],
..EncodeOptions::default()
};
let mut codestream =
encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode precinct fixture");
rewrite_siz_to_single_large_tile(&mut codestream, dimensions);
let cod = codestream
.windows(2)
.position(|w| w == [0xFF, 0x52])
.expect("COD marker");
assert_eq!(codestream[cod + 4] & 1, 1, "COD signals precinct sizes");
assert_eq!(codestream[cod + 9], 1, "one decomposition level");
codestream[cod + 6..cod + 8].copy_from_slice(&32_u16.to_be_bytes());
codestream[cod + 14] = 0x00;
codestream[cod + 15] = 0x11;
codestream
}
#[test]
fn decode_region_rejects_an_oversized_precinct_layout_before_walking_the_progression() {
let codestream = many_precinct_layer_codestream(4_096);
let image = Image::new(&codestream, &DecodeSettings::default()).expect("large SIZ parses");
let mut context = DecoderContext::default();
let Err(error) = image.decode_region_with_context((0, 0, 1, 1), &mut context) else {
panic!("the precinct layout must exceed the structural budget");
};
assert_eq!(
error,
DecodeError::Validation(ValidationError::ImageTooLarge)
);
assert_eq!(
context
.tile_decode_context
.debug_counters
.selection_scanned_packets,
0
);
}
#[test]
fn region_precinct_selection_walks_no_more_packets_than_the_tile_has_header_bytes() {
let codestream = many_precinct_layer_codestream(1_024);
let image = Image::new(&codestream, &DecodeSettings::default()).expect("large SIZ parses");
let mut context = DecoderContext::default();
let _ = image.decode_region_with_context((512, 512, 1, 1), &mut context);
let scanned = context
.tile_decode_context
.debug_counters
.selection_scanned_packets;
assert!(
scanned > 0 && scanned <= codestream.len(),
"walked {scanned} progression positions for a {}-byte codestream",
codestream.len()
);
}
#[test]
fn region_selection_releases_the_previous_tiles_retained_decomposition_capacity() {
let pixels = (0..128_u8).collect::<Vec<_>>();
let bytes = encode(
&pixels,
16,
8,
1,
8,
false,
&EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(2, 2)],
tile_size: Some((8, 8)),
..EncodeOptions::default()
},
)
.expect("encode two-tile region fixture");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("two-tile image");
let mut context = DecoderContext::default();
image
.decode_region_with_context((0, 0, 16, 8), &mut context)
.expect("decode both selected tiles");
assert!(
context
.tile_decode_context
.debug_counters
.selection_released_storage_bytes
> 0,
"the second selection must release the first tile's retained decomposition owner"
);
}
#[test]
fn decode_region_rejects_code_block_layer_metadata_amplification() {
let pixels: Vec<u8> = (0..64).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
num_layers: 32,
..EncodeOptions::default()
};
let mut bytes =
encode(&pixels, 8, 8, 1, 8, false, &options).expect("encode many-layer classic fixture");
rewrite_siz_to_single_large_tile(&mut bytes, 4_096);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
for roi in [(0, 0, 4_096, 4_096), (0, 0, 1, 1)] {
let Err(err) = image.decode_region(roi) else {
panic!("ROI {roi:?} must not bypass the structural metadata budget");
};
assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
}
}
#[test]
fn region_decode_without_plt_keeps_no_layer_records_for_code_blocks_it_skips() {
const SIZE: u32 = 256;
const LAYERS: u8 = 4;
let mut state = 0x9e37_79b9_u32;
let pixels: Vec<u8> = (0..SIZE * SIZE)
.map(|_| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
(state >> 24) as u8
})
.collect();
for use_ht_block_coding in [false, true] {
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(4, 4); 3],
progression_order: EncodeProgressionOrder::Lrcp,
num_layers: LAYERS,
use_ht_block_coding,
..EncodeOptions::default()
};
let bytes =
encode(&pixels, SIZE, SIZE, 1, 8, false, &options).expect("encode LRCP fixture");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("parse LRCP fixture");
let whole = image.decode_native().expect("decode whole image").data;
let roi = (SIZE - 24, SIZE - 24, 24, 24);
let mut context = DecoderContext::default();
let region = image
.decode_native_region_with_context(roi, &mut context)
.expect("decode corner region");
let expected: Vec<u8> = whole
.chunks_exact(image.width() as usize)
.skip(roi.1 as usize)
.take(roi.3 as usize)
.flat_map(|row| &row[roi.0 as usize..(roi.0 + roi.2) as usize])
.copied()
.collect();
assert_eq!(region.data, expected, "ht={use_ht_block_coding}");
let counters = context.tile_decode_context.debug_counters;
let layers = usize::from(LAYERS);
assert!(counters.built_layer_records > 0, "ht={use_ht_block_coding}");
assert_eq!(
counters.built_layer_records % layers,
0,
"ht={use_ht_block_coding}"
);
assert!(
counters.built_layer_records * 4 < counters.built_code_blocks * layers,
"ht={use_ht_block_coding}: {} layer records for {} code-blocks",
counters.built_layer_records,
counters.built_code_blocks
);
let mut context = DecoderContext::default();
image
.decode_native_with_context(&mut context)
.expect("decode whole image");
let counters = context.tile_decode_context.debug_counters;
assert_eq!(
counters.built_layer_records,
counters.built_code_blocks * layers,
"ht={use_ht_block_coding}"
);
}
}
#[test]
fn region_decode_parses_skipped_precincts_in_shared_scratch() {
const SIZE: u32 = 96;
const COMPONENTS: u16 = 3;
const LAYERS: u8 = 4;
const SCRATCH_CODE_BLOCKS: usize = 4 + 1 + 1;
let components = u32::from(COMPONENTS);
let mut state = 0x9e37_79b9_u32;
let pixels: Vec<u8> = (0..SIZE * SIZE * components)
.map(|_| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
(state >> 24) as u8
})
.collect();
for (progression_order, in_scratch) in [
(EncodeProgressionOrder::Lrcp, false),
(EncodeProgressionOrder::Rpcl, true),
(EncodeProgressionOrder::Pcrl, true),
(EncodeProgressionOrder::Cprl, true),
] {
for use_ht_block_coding in [false, true] {
let case = format!("{progression_order:?}, ht={use_ht_block_coding}");
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(3, 3); 3],
progression_order,
num_layers: LAYERS,
use_ht_block_coding,
..EncodeOptions::default()
};
let bytes = encode(&pixels, SIZE, SIZE, COMPONENTS, 8, false, &options)
.expect("encode fixture");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("parse fixture");
let whole = image.decode_native().expect("decode whole image").data;
for roi in [(SIZE - 12, SIZE - 12, 12, 12), (40, 52, 14, 10)] {
let mut context = DecoderContext::default();
let region = image
.decode_native_region_with_context(roi, &mut context)
.expect("decode region");
let row_bytes = (image.width() * components) as usize;
let expected: Vec<u8> = whole
.chunks_exact(row_bytes)
.skip(roi.1 as usize)
.take(roi.3 as usize)
.flat_map(|row| {
&row[(roi.0 * components) as usize..((roi.0 + roi.2) * components) as usize]
})
.copied()
.collect();
assert_eq!(region.data, expected, "{case}, {roi:?}");
let counters = context.tile_decode_context.debug_counters;
let unlayered =
counters.built_code_blocks - counters.built_layer_records / usize::from(LAYERS);
assert_eq!(
unlayered <= SCRATCH_CODE_BLOCKS,
in_scratch,
"{case}, {roi:?}: {unlayered} built code-blocks without layer records"
);
}
}
}
}
#[test]
fn region_decode_builds_no_records_for_precincts_it_never_parses() {
const SIZE: u32 = 96;
let mut state = 0x9e37_79b9_u32;
let pixels: Vec<u8> = (0..SIZE * SIZE)
.map(|_| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
(state >> 24) as u8
})
.collect();
for (progression_order, write_plt) in [
(EncodeProgressionOrder::Lrcp, true),
(EncodeProgressionOrder::Rpcl, true),
(EncodeProgressionOrder::Rpcl, false),
] {
for use_ht_block_coding in [false, true] {
let case = format!("{progression_order:?}, plt={write_plt}, ht={use_ht_block_coding}");
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(3, 3); 3],
progression_order,
num_layers: 3,
use_ht_block_coding,
write_plt,
..EncodeOptions::default()
};
let bytes = encode(&pixels, SIZE, SIZE, 1, 8, false, &options).expect("encode fixture");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("parse fixture");
let mut context = DecoderContext::default();
let whole = image
.decode_native_with_context(&mut context)
.expect("decode whole image")
.data;
let whole_precincts = context.tile_decode_context.debug_counters.built_precincts;
let roi = (4, 4, 12, 12);
let mut context = DecoderContext::default();
let region = image
.decode_native_region_with_context(roi, &mut context)
.expect("decode region");
let expected: Vec<u8> = whole
.chunks_exact(image.width() as usize)
.skip(roi.1 as usize)
.take(roi.3 as usize)
.flat_map(|row| &row[roi.0 as usize..(roi.0 + roi.2) as usize])
.copied()
.collect();
assert_eq!(region.data, expected, "{case}");
let built = context.tile_decode_context.debug_counters.built_precincts;
assert!(
built * 2 < whole_precincts,
"{case}: {built} of {whole_precincts} precinct records built"
);
}
}
}
#[test]
fn region_decode_scratch_keeps_state_across_tile_parts() {
const WIDTH: u32 = 64;
const HEIGHT: u32 = 32;
let mut state = 0x9e37_79b9_u32;
let pixels: Vec<u8> = (0..WIDTH * HEIGHT)
.map(|i| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
if i % WIDTH < 8 && i / WIDTH < 8 {
128 + (i % 2) as u8
} else {
(state >> 24) as u8
}
})
.collect();
for progression_order in [
EncodeProgressionOrder::Rpcl,
EncodeProgressionOrder::Pcrl,
EncodeProgressionOrder::Cprl,
] {
for (use_ht_block_coding, empty_first) in [(false, false), (false, true), (true, false)] {
for late_poc in [false, true] {
let case = format!("{progression_order:?}, ht={use_ht_block_coding}, empty={empty_first}, poc={late_poc}");
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(3, 3)],
progression_order,
num_layers: 2,
quality_layer_byte_targets: if empty_first {
vec![0, 8192]
} else {
Vec::new()
},
tile_part_packet_limit: Some(1),
use_ht_block_coding,
..EncodeOptions::default()
};
let mut bytes = encode(&pixels, WIDTH, HEIGHT, 1, 8, false, &options)
.unwrap_or_else(|error| panic!("{case}: encode fixture: {error:?}"));
let first = bytes
.windows(2)
.position(|w| w == [0xff, 0x90])
.expect("first SOT");
let length =
u32::from_be_bytes(bytes[first + 6..first + 10].try_into().unwrap()) as usize;
let second = first + length;
assert_eq!(&bytes[second..second + 2], &[0xff, 0x90]);
if empty_first {
assert_eq!(bytes[first + 14], 0, "{case}: empty layer 0");
assert_ne!(bytes[second + 14], 0, "{case}: nonempty layer 1");
}
if late_poc {
let order = match progression_order {
EncodeProgressionOrder::Rpcl => 2,
EncodeProgressionOrder::Pcrl => 3,
EncodeProgressionOrder::Cprl => 4,
_ => unreachable!(),
};
let poc = [0xff, 0x5f, 0, 9, 0, 0, 0, 2, 1, 1, order];
let length =
u32::from_be_bytes(bytes[second + 6..second + 10].try_into().unwrap());
let poc_len = u32::try_from(poc.len()).expect("POC marker length");
bytes[second + 6..second + 10]
.copy_from_slice(&(length + poc_len).to_be_bytes());
bytes.splice(second + 12..second + 12, poc);
}
let image = Image::new(&bytes, &DecodeSettings::default()).expect("parse fixture");
let whole = image.decode_native().expect("decode whole image").data;
assert_eq!(whole, pixels, "{case}");
let mut context = DecoderContext::default();
let region = image
.decode_native_region_with_context((WIDTH - 8, HEIGHT - 8, 8, 8), &mut context)
.expect("decode region");
let expected: Vec<u8> = whole
.as_chunks::<{ WIDTH as usize }>()
.0
.iter()
.skip((HEIGHT - 8) as usize)
.flat_map(|row| &row[(WIDTH - 8) as usize..])
.copied()
.collect();
assert_eq!(region.data, expected, "{case}");
let counters = context.tile_decode_context.debug_counters;
let unlayered = counters.built_code_blocks - counters.built_layer_records / 2;
assert_eq!(
unlayered <= 4,
!late_poc,
"{case}: {unlayered} unlayered blocks"
);
}
}
}
}
fn fixture_ht_gray() -> Vec<u8> {
let pixels: Vec<u8> = (0..16).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
..EncodeOptions::default()
};
encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode ht gray8")
}
fn fixture_ht_multi_block() -> Vec<u8> {
let pixels: Vec<u8> = (0..64).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(2, 2)],
write_plt: true,
..EncodeOptions::default()
};
encode_htj2k(&pixels, 8, 8, 1, 8, false, &options).expect("encode multi-block HT gray8")
}
fn fixture_ht_rgb_multi_block() -> Vec<u8> {
let pixels = gradient_pixels(16, 16, 3);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(2, 2)],
write_plt: true,
..EncodeOptions::default()
};
encode_htj2k(&pixels, 16, 16, 3, 8, false, &options).expect("encode multi-block HT RGB8")
}
fn direct_ht_job_count(plan: &J2kDirectGrayscalePlan) -> usize {
plan.steps
.iter()
.map(|step| match step {
J2kDirectGrayscaleStep::HtSubBand(sub_band) => sub_band.jobs.len(),
_ => 0,
})
.sum()
}
fn direct_color_ht_job_count(plan: &J2kDirectColorPlan) -> usize {
plan.component_plans.iter().map(direct_ht_job_count).sum()
}
fn corrupt_last_plt_packet(bytes: &mut [u8]) {
let plt = bytes
.windows(2)
.position(|window| window == [0xFF, 0x58])
.expect("PLT marker");
let marker_len = usize::from(u16::from_be_bytes([bytes[plt + 2], bytes[plt + 3]]));
let mut packet_lengths = Vec::new();
let mut length = 0usize;
for &byte in &bytes[plt + 5..plt + 2 + marker_len] {
length = length
.checked_mul(128)
.and_then(|value| value.checked_add(usize::from(byte & 0x7f)))
.expect("packet length fits");
if byte & 0x80 == 0 {
packet_lengths.push(length);
length = 0;
}
}
assert_eq!(length, 0, "PLT packet length terminates");
assert!(packet_lengths.len() > 1, "fixture has trailing packets");
let sod = bytes
.windows(2)
.position(|window| window == [0xFF, 0x93])
.expect("SOD marker");
let last_start = sod
+ 2
+ packet_lengths[..packet_lengths.len() - 1]
.iter()
.sum::<usize>();
let last_end = last_start + packet_lengths[packet_lengths.len() - 1];
assert!(
last_end <= bytes.len(),
"last PLT packet fits the tile-part"
);
bytes[last_start..last_end].fill(0xff);
}
fn gradient_pixels(width: u32, height: u32, components: u8) -> Vec<u8> {
let mut pixels = Vec::with_capacity(width as usize * height as usize * components as usize);
for y in 0..height {
for x in 0..width {
for component in 0..components {
pixels.push(((x * 3 + y * 5 + u32::from(component) * 41) & 0xff) as u8);
}
}
}
pixels
}
fn roi_fixture(classic: bool, components: u8) -> Vec<u8> {
let width = 64;
let height = 64;
let pixels = gradient_pixels(width, height, components);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
code_block_width_exp: 0,
code_block_height_exp: 0,
..EncodeOptions::default()
};
if classic {
encode(
&pixels,
width,
height,
components.into(),
8,
false,
&options,
)
.expect("encode ROI classic fixture")
} else {
encode_htj2k(
&pixels,
width,
height,
components.into(),
8,
false,
&options,
)
.expect("encode ROI HT fixture")
}
}
fn crop_interleaved(
full: &[u8],
full_width: u32,
channels: usize,
roi: (u32, u32, u32, u32),
) -> Vec<u8> {
let (x, y, width, height) = roi;
let mut out = Vec::with_capacity(width as usize * height as usize * channels);
let row_bytes = full_width as usize * channels;
let roi_row_bytes = width as usize * channels;
for row in y as usize..(y + height) as usize {
let start = row * row_bytes + x as usize * channels;
out.extend_from_slice(&full[start..start + roi_row_bytes]);
}
out
}
fn count_decode_work(bytes: &[u8], roi: Option<(u32, u32, u32, u32)>) -> DecodeWorkCounter {
let image = Image::new(bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
match roi {
Some(roi) => {
image
.decode_region_with_context(roi, &mut context)
.expect("region decode with counter");
}
None => {
image
.decode_with_context(&mut context)
.expect("full decode with counter");
}
}
let counters = context.tile_decode_context.debug_counters;
DecodeWorkCounter {
classic_code_blocks: counters.decoded_code_blocks,
ht_code_blocks: 0,
idwt_output_samples: counters.idwt_output_samples,
stored_coefficients: counters.stored_coefficients,
}
}
#[test]
fn roi_decode_matches_full_crop_for_classic_and_htj2k_gray_and_rgb() {
let cases = [
(true, 1_u8, true, false),
(true, 3_u8, false, false),
(false, 1_u8, true, false),
(false, 3_u8, false, false),
];
let rois = [
(20, 18, 17, 19),
(0, 0, 9, 11),
(63, 63, 1, 1),
(7, 5, 13, 9),
(0, 0, 64, 64),
];
for (classic, components, expect_gray, has_alpha) in cases {
let bytes = roi_fixture(classic, components);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let full = image.decode().expect("full decode");
let channels = components as usize;
for roi in rois {
let region = image.decode_region(roi).expect("region decode");
assert_eq!(matches!(region.color_space, ColorSpace::Gray), expect_gray);
assert_eq!(region.has_alpha, has_alpha);
assert_eq!(
region.data,
crop_interleaved(&full, 64, channels, roi),
"classic={classic} components={components} roi={roi:?}"
);
}
}
}
#[test]
fn roi_decode_prunes_code_blocks_and_idwt_work_for_classic_and_htj2k() {
let roi = (48, 48, 16, 16);
for classic in [true, false] {
let bytes = {
let pixels = gradient_pixels(128, 128, 1);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 3,
code_block_width_exp: 0,
code_block_height_exp: 0,
..EncodeOptions::default()
};
if classic {
encode(&pixels, 128, 128, 1, 8, false, &options)
.expect("encode classic work fixture")
} else {
encode_htj2k(&pixels, 128, 128, 1, 8, false, &options)
.expect("encode ht work fixture")
}
};
let full = count_decode_work(&bytes, None);
let region = count_decode_work(&bytes, Some(roi));
assert!(
region.code_blocks() > 0 && region.code_blocks() < full.code_blocks(),
"ROI should decode fewer code-blocks for classic={classic}; full={}, region={}",
full.code_blocks(),
region.code_blocks()
);
assert!(
region.idwt_output_samples > 0
&& region.idwt_output_samples < full.idwt_output_samples,
"ROI should produce fewer IDWT output samples for classic={classic}; full={}, region={}",
full.idwt_output_samples,
region.idwt_output_samples
);
}
}
#[test]
fn roi_decode_prunes_work_for_regions_touching_right_and_bottom_tile_edges() {
for reversible in [true, false] {
let pixels = gradient_pixels(128, 128, 1);
let options = EncodeOptions {
reversible,
num_decomposition_levels: 3,
code_block_width_exp: 0,
code_block_height_exp: 0,
..EncodeOptions::default()
};
let bytes = encode(&pixels, 128, 128, 1, 8, false, &options).expect("encode work fixture");
let full = count_decode_work(&bytes, None);
for roi in [
(112, 48, 16, 16),
(48, 112, 16, 16),
(112, 112, 16, 16),
(127, 0, 1, 128),
(0, 127, 128, 1),
] {
let region = count_decode_work(&bytes, Some(roi));
assert!(
region.code_blocks() > 0 && region.code_blocks() < full.code_blocks(),
"edge ROI {roi:?} should decode fewer code-blocks (reversible={reversible}); full={}, region={}",
full.code_blocks(),
region.code_blocks()
);
assert!(
region.idwt_output_samples > 0
&& region.idwt_output_samples < full.idwt_output_samples,
"edge ROI {roi:?} should produce fewer IDWT output samples (reversible={reversible}); full={}, region={}",
full.idwt_output_samples,
region.idwt_output_samples
);
}
}
}
#[test]
fn region_decode_stores_only_the_code_blocks_it_needs() {
let (width, height) = (1024_u32, 768_u32);
let pixels = gradient_pixels(width, height, 1);
for reversible in [true, false] {
let options = EncodeOptions {
reversible,
num_decomposition_levels: 3,
..EncodeOptions::default()
};
let bytes = encode(&pixels, width, height, 1, 8, false, &options).expect("encode");
let full = count_decode_work(&bytes, None);
assert_eq!(full.stored_coefficients, (width * height) as usize);
for roi in [(500, 380, 32, 32), (992, 736, 32, 32), (0, 0, 16, 16)] {
let region = count_decode_work(&bytes, Some(roi));
assert!(
region.stored_coefficients > 0
&& region.stored_coefficients * 4 <= full.stored_coefficients,
"ROI {roi:?} stored {} of {} coefficients (reversible={reversible})",
region.stored_coefficients,
full.stored_coefficients
);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
assert_eq!(
image.decode_region(roi).expect("region decode").data,
crop_interleaved(&image.decode().expect("full decode"), width, 1, roi),
"ROI {roi:?} (reversible={reversible})"
);
}
}
}
#[test]
fn region_decode_reuses_region_sized_component_storage() {
let bytes = fixture();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let bitmap = image
.decode_region_with_context((1, 0, 1, 2), &mut context)
.expect("region decode");
assert_eq!((bitmap.width, bitmap.height), (1, 2));
assert_eq!(context.tile_decode_context.channel_data.len(), 3);
assert!(context
.tile_decode_context
.channel_data
.iter()
.all(|component| component.container.truncated().len() == 2));
}
#[test]
fn native_region_decode_reuses_region_sized_component_storage() {
let bytes = fixture();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let bitmap = image
.decode_native_region_with_context((1, 0, 1, 2), &mut context)
.expect("native region decode");
assert_eq!((bitmap.width, bitmap.height), (1, 2));
assert_eq!(context.tile_decode_context.channel_data.len(), 3);
assert!(context
.tile_decode_context
.channel_data
.iter()
.all(|component| component.container.truncated().len() == 2));
}
#[derive(Debug, PartialEq, Eq)]
struct ChannelCapacitySnapshot {
outer_ptr: *const ComponentData,
outer_capacity: usize,
sample_owners: Vec<(*const f32, usize)>,
integer_owners: Vec<Option<(*const i64, usize)>>,
}
fn channel_capacity_snapshot(context: &DecoderContext<'_>) -> ChannelCapacitySnapshot {
let components = &context.tile_decode_context.channel_data;
let idwt_output = &context.tile_decode_context.idwt_output.coefficients;
let mut sample_owners = components
.iter()
.map(|component| (component.container.as_ptr(), component.container.capacity()))
.chain([(idwt_output.as_ptr(), idwt_output.capacity())])
.collect::<Vec<_>>();
sample_owners.sort_unstable();
ChannelCapacitySnapshot {
outer_ptr: components.as_ptr(),
outer_capacity: components.capacity(),
sample_owners,
integer_owners: components
.iter()
.map(|component| {
component
.integer_container
.as_ref()
.map(|samples| (samples.as_ptr(), samples.capacity()))
})
.collect(),
}
}
#[test]
fn decoder_context_reuses_component_owners_across_packed_and_component_outputs() {
let bytes = fixture();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let first = image
.decode_with_context(&mut context)
.expect("first packed decode");
assert_eq!(context.tile_decode_context.channel_data.len(), 3);
let expected = first.data.clone();
let owners = channel_capacity_snapshot(&context);
context.tile_decode_context.channel_data[0].container[0] = f32::INFINITY;
drop(first);
let second = image
.decode_with_context(&mut context)
.expect("second packed decode");
assert_eq!(second.data, expected);
assert_eq!(channel_capacity_snapshot(&context), owners);
drop(second);
let component_output = image
.decode_native_components_with_context(&mut context)
.expect("owned component decode");
assert_eq!(component_output.planes().len(), 3);
assert_eq!(channel_capacity_snapshot(&context), owners);
drop(component_output);
let native = image
.decode_native_with_context(&mut context)
.expect("native packed decode");
assert_eq!(native.data, expected);
assert_eq!(channel_capacity_snapshot(&context), owners);
}
#[test]
fn exact_integer_decoder_context_reuses_and_resets_i64_samples() {
let samples = [0_u32, 1, (1_u32 << 28) + 7, (1_u32 << 29) - 1];
let pixels = samples
.iter()
.flat_map(|sample| sample.to_le_bytes())
.collect::<Vec<_>>();
let planes = [EncodeTypedComponentPlane {
data: &pixels,
x_rsiz: 1,
y_rsiz: 1,
bit_depth: 29,
signed: false,
}];
let bytes = encode_typed_component_planes_53(
&planes,
2,
2,
&EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
use_mct: false,
..EncodeOptions::default()
},
)
.expect("encode exact integer fixture");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let first = image
.decode_native_with_context(&mut context)
.expect("first exact decode");
assert_eq!(first.data, pixels);
let owners = channel_capacity_snapshot(&context);
let integer_samples = context.tile_decode_context.channel_data[0]
.integer_container
.as_mut()
.expect("29-bit decode uses exact integer samples");
assert_eq!(integer_samples.len(), 4);
integer_samples.fill(i64::MIN);
drop(first);
let second = image
.decode_native_with_context(&mut context)
.expect("second exact decode");
assert_eq!(second.data, pixels);
assert_eq!(channel_capacity_snapshot(&context), owners);
}
#[test]
fn decoder_context_defaults_to_auto_cpu_parallelism() {
let context = DecoderContext::default();
assert_eq!(context.cpu_decode_parallelism(), CpuDecodeParallelism::Auto);
}
#[test]
fn classic_j2k_auto_and_serial_cpu_parallelism_match_pixels() {
let bytes = fixture_multi_block();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut auto_context = DecoderContext::default();
let mut serial_context = DecoderContext::default();
serial_context.set_cpu_decode_parallelism(CpuDecodeParallelism::Serial);
let auto = image
.decode_with_context(&mut auto_context)
.expect("auto decode");
let serial = image
.decode_with_context(&mut serial_context)
.expect("serial decode");
assert_eq!(auto.data, serial.data);
}
#[test]
fn htj2k_97_auto_and_serial_cpu_parallelism_match_pixels() {
let width = 128_u32;
let height = 128_u32;
let pixels = (0..width * height)
.map(|idx| ((idx * 17 + idx / width * 31) & 0xff) as u8)
.collect::<Vec<_>>();
let bytes = encode_htj2k(
&pixels,
width,
height,
1,
8,
false,
&EncodeOptions {
reversible: false,
guard_bits: 2,
num_decomposition_levels: 5,
..EncodeOptions::default()
},
)
.expect("encode HTJ2K 9/7");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut auto_context = DecoderContext::default();
let mut serial_context = DecoderContext::default();
serial_context.set_cpu_decode_parallelism(CpuDecodeParallelism::Serial);
let auto = image
.decode_with_context(&mut auto_context)
.expect("auto decode");
let serial = image
.decode_with_context(&mut serial_context)
.expect("serial decode");
assert_eq!(auto.data, serial.data);
}
#[test]
fn serial_cpu_parallelism_disables_classic_sub_band_parallel_branch() {
assert!(!j2c::should_decode_classic_sub_band_in_parallel(
CpuDecodeParallelism::Serial,
16
));
}
#[test]
fn grayscale_direct_plan_is_built_without_materializing_channel_data() {
let bytes = fixture_gray();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let plan = image
.build_direct_grayscale_plan_with_context(&mut context)
.expect("build direct plan");
assert_eq!(plan.dimensions, (4, 4));
assert_eq!(plan.bit_depth, 8);
assert!(
!plan.steps.is_empty(),
"direct plan must contain executable steps"
);
assert!(
plan.steps.iter().any(|step| matches!(
step,
J2kDirectGrayscaleStep::ClassicSubBand(plan) if !plan.jobs.is_empty()
)),
"classic J2K direct plan must contain at least one non-empty classic sub-band job"
);
assert!(
context.tile_decode_context.channel_data.is_empty(),
"building a direct plan must not materialize host component planes"
);
assert_eq!(
context.storage.retained_capacity_bytes().unwrap(),
0,
"all source graph owners must be released after the owned plan handoff"
);
}
#[test]
fn grayscale_direct_plan_honors_target_resolution() {
let bytes = fixture_ht_gray();
let image = Image::new(
&bytes,
&DecodeSettings {
target_resolution: Some((2, 2)),
..DecodeSettings::default()
},
)
.expect("scaled image");
let mut context = DecoderContext::default();
let plan = image
.build_direct_grayscale_plan_with_context(&mut context)
.expect("build scaled direct plan");
assert_eq!(plan.dimensions, (2, 2));
assert!(plan.steps.iter().any(|step| matches!(
step,
J2kDirectGrayscaleStep::HtSubBand(plan) if !plan.jobs.is_empty()
)));
assert!(plan.steps.iter().any(|step| matches!(
step,
J2kDirectGrayscaleStep::Store(store)
if store.output_width == 2 && store.output_height == 2
)));
assert!(
context.tile_decode_context.channel_data.is_empty(),
"building a scaled direct plan must not materialize host component planes"
);
}
#[test]
fn odd_dimensions_honor_covering_target_resolution() {
let pixels = gradient_pixels(9, 7, 1);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
..EncodeOptions::default()
};
let bytes = encode_htj2k(&pixels, 9, 7, 1, 8, false, &options).expect("encode odd HT gray8");
for (target, expected) in [((5, 4), (5, 4)), ((3, 2), (3, 2))] {
let image = Image::new(
&bytes,
&DecodeSettings {
target_resolution: Some(target),
..DecodeSettings::default()
},
)
.expect("scaled odd image");
assert_eq!(
(image.width(), image.height()),
expected,
"target {target:?}"
);
}
}
#[test]
fn grayscale_direct_plan_region_prunes_unneeded_ht_code_blocks() {
let bytes = fixture_ht_multi_block();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut full_context = DecoderContext::default();
let mut roi_context = DecoderContext::default();
let full = image
.build_direct_grayscale_plan_with_context(&mut full_context)
.expect("build full direct plan");
let roi = image
.build_direct_grayscale_plan_region_with_context(&mut roi_context, (0, 0, 2, 2))
.expect("build ROI direct plan");
let full_jobs = direct_ht_job_count(&full);
let roi_jobs = direct_ht_job_count(&roi);
assert!(full_jobs > 1, "fixture must expose multiple HT jobs");
assert!(
roi_jobs < full_jobs,
"ROI direct plan must prune HT jobs before device preparation"
);
assert!(
roi_context.direct_plan_code_blocks < full_context.direct_plan_code_blocks,
"ROI direct plan must not build unneeded grayscale code-block metadata"
);
}
#[test]
fn grayscale_direct_region_does_not_parse_an_unneeded_trailing_packet() {
let pixels: Vec<u8> = (0..64).collect();
let mut bytes = encode_htj2k(
&pixels,
8,
8,
1,
8,
false,
&EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(2, 2)],
write_plt: true,
..EncodeOptions::default()
},
)
.expect("encode grayscale PLT fixture");
corrupt_last_plt_packet(&mut bytes);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("corrupted packet image");
image
.build_direct_grayscale_plan_with_context(&mut DecoderContext::default())
.expect_err("the full direct plan parses the corrupted trailing packet");
image
.build_direct_grayscale_plan_region_with_context(
&mut DecoderContext::default(),
(0, 0, 1, 1),
)
.expect("the region plan stops before the unneeded trailing packet");
}
#[test]
fn color_direct_plan_region_prunes_unneeded_ht_code_blocks() {
let bytes = fixture_ht_rgb_multi_block();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut full_context = DecoderContext::default();
let mut roi_context = DecoderContext::default();
let full = image
.build_direct_color_plan_with_context(&mut full_context)
.expect("build full RGB direct plan");
let roi = image
.build_direct_color_plan_region_with_context(&mut roi_context, (0, 0, 2, 2))
.expect("build ROI RGB direct plan");
let full_jobs = direct_color_ht_job_count(&full);
let roi_jobs = direct_color_ht_job_count(&roi);
assert!(full_jobs > 3, "fixture must expose multiple RGB HT jobs");
assert!(
roi_jobs < full_jobs,
"RGB ROI direct plan must prune HT jobs before device preparation"
);
assert!(
roi_context.direct_plan_code_blocks < full_context.direct_plan_code_blocks,
"ROI direct plan must not build unneeded color code-block metadata: full={}, roi={}",
full_context.direct_plan_code_blocks,
roi_context.direct_plan_code_blocks,
);
}
#[test]
fn color_direct_region_does_not_parse_an_unneeded_trailing_packet() {
let pixels = gradient_pixels(8, 8, 3);
let mut bytes = encode_htj2k(
&pixels,
8,
8,
3,
8,
false,
&EncodeOptions {
reversible: true,
num_decomposition_levels: 0,
code_block_width_exp: 0,
code_block_height_exp: 0,
precinct_exponents: vec![(2, 2)],
write_plt: true,
..EncodeOptions::default()
},
)
.expect("encode color PLT fixture");
corrupt_last_plt_packet(&mut bytes);
let image = Image::new(&bytes, &DecodeSettings::default()).expect("corrupted packet image");
image
.build_direct_color_plan_with_context(&mut DecoderContext::default())
.expect_err("the full direct plan parses the corrupted trailing packet");
image
.build_direct_color_plan_region_with_context(&mut DecoderContext::default(), (0, 0, 1, 1))
.expect("the region plan stops before the unneeded trailing packet");
}
#[test]
fn color_direct_plan_honors_target_resolution() {
for (name, bytes) in [
("classic", {
let pixels = gradient_pixels(8, 8, 3);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
..EncodeOptions::default()
};
encode(&pixels, 8, 8, 3, 8, false, &options).expect("encode classic rgb8")
}),
("htj2k", {
let pixels = gradient_pixels(8, 8, 3);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
..EncodeOptions::default()
};
encode_htj2k(&pixels, 8, 8, 3, 8, false, &options).expect("encode ht rgb8")
}),
] {
let image = Image::new(
&bytes,
&DecodeSettings {
target_resolution: Some((4, 4)),
..DecodeSettings::default()
},
)
.expect("scaled RGB image");
let mut context = DecoderContext::default();
let plan = image
.build_direct_color_plan_with_context(&mut context)
.expect("build scaled direct color plan");
assert_eq!(plan.dimensions, (4, 4), "{name}: output dimensions");
assert_eq!(plan.component_plans.len(), 3, "{name}: component count");
for component_plan in &plan.component_plans {
assert_eq!(component_plan.dimensions, (4, 4), "{name}: component dims");
assert!(component_plan.steps.iter().any(|step| matches!(
step,
J2kDirectGrayscaleStep::Store(store)
if store.output_width == 4 && store.output_height == 4
)));
}
assert!(
context.tile_decode_context.channel_data.is_empty(),
"{name}: building a scaled color direct plan must not materialize host component planes"
);
}
}
#[test]
fn direct_color_cpu_rgb8_executor_matches_scaled_region_decode() {
for (name, bytes) in [
("classic", {
let pixels = gradient_pixels(16, 16, 3);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
..EncodeOptions::default()
};
encode(&pixels, 16, 16, 3, 8, false, &options).expect("encode classic rgb8")
}),
("classic-9/7", {
let pixels = gradient_pixels(16, 16, 3);
let options = EncodeOptions {
reversible: false,
num_decomposition_levels: 2,
..EncodeOptions::default()
};
encode(&pixels, 16, 16, 3, 8, false, &options)
.expect("encode irreversible classic rgb8")
}),
("htj2k", {
let pixels = gradient_pixels(16, 16, 3);
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 2,
..EncodeOptions::default()
};
encode_htj2k(&pixels, 16, 16, 3, 8, false, &options).expect("encode ht rgb8")
}),
] {
let image = Image::new(
&bytes,
&DecodeSettings {
target_resolution: Some((4, 4)),
..DecodeSettings::default()
},
)
.expect("scaled RGB image");
let mut expected_context = DecoderContext::default();
let expected_full = image
.decode_with_context(&mut expected_context)
.expect("decode scaled reference");
let output_region = J2kRect {
x0: 1,
y0: 1,
x1: 3,
y1: 3,
};
let mut direct_context = DecoderContext::default();
let plan = image
.build_direct_color_plan_region_with_context(
&mut direct_context,
(
output_region.x0,
output_region.y0,
output_region.width(),
output_region.height(),
),
)
.expect("build direct RGB region plan");
let stride = output_region.width() as usize * 3;
let mut direct = vec![0_u8; stride * output_region.height() as usize];
let mut scratch = J2kDirectCpuScratch::new();
execute_direct_color_plan_rgb8_into(
&plan,
output_region,
&mut scratch,
&mut direct,
stride,
)
.expect("execute direct RGB plan");
let mut expected = Vec::with_capacity(direct.len());
let full_stride = image.width() as usize * 3;
for y in output_region.y0..output_region.y1 {
let start = y as usize * full_stride + output_region.x0 as usize * 3;
expected.extend_from_slice(&expected_full.data[start..start + stride]);
}
assert_eq!(direct, expected, "{name}: direct RGB output");
let rgba_stride = output_region.width() as usize * 4;
let mut direct_rgba = vec![0_u8; rgba_stride * output_region.height() as usize];
execute_direct_color_plan_rgba8_into(
&plan,
output_region,
&mut scratch,
&mut direct_rgba,
rgba_stride,
)
.expect("execute direct RGBA plan");
let mut expected_rgba = Vec::with_capacity(direct_rgba.len());
for rgb in expected.as_chunks::<3>().0 {
expected_rgba.extend_from_slice(rgb);
expected_rgba.push(255);
}
assert_eq!(direct_rgba, expected_rgba, "{name}: direct RGBA output");
}
}
#[test]
fn htj2k_grayscale_direct_plan_contains_ht_sub_band_steps() {
let bytes = fixture_ht_gray();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut context = DecoderContext::default();
let plan = image
.build_direct_grayscale_plan_with_context(&mut context)
.expect("build direct plan");
assert!(
plan.steps.iter().any(|step| matches!(
step,
J2kDirectGrayscaleStep::HtSubBand(plan) if !plan.jobs.is_empty()
)),
"HTJ2K direct plan must contain at least one non-empty HT sub-band decode step"
);
}
#[test]
fn ht_decoder_hook_is_used_for_htj2k_codeblocks() {
let pixels: Vec<u8> = (0..16).collect();
let options = EncodeOptions {
reversible: true,
num_decomposition_levels: 1,
..EncodeOptions::default()
};
let bytes = encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode ht");
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut hooked_context = DecoderContext::default();
let mut hook = FailingHtDecoder { called: false };
let Err(error) = image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) else {
panic!("hooked decode must use external HT decoder");
};
assert!(hook.called, "HT decoder hook must be invoked");
assert_eq!(
error,
DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
);
}
#[test]
fn scalar_htj2k_encoder_contract_is_cleanup_only() {
let coefficients = (0..64)
.map(|index| {
let magnitude = (index % 7) + 1;
if index % 2 == 0 {
magnitude
} else {
-magnitude
}
})
.collect::<Vec<_>>();
let encoded =
encode_ht_code_block_scalar(&coefficients, 8, 8, 8).expect("encode HT code block");
assert_eq!(
encoded.num_coding_passes, 1,
"current scalar HTJ2K encoder emits only the cleanup pass"
);
assert_eq!(
encoded.num_zero_bitplanes, 7,
"current cleanup-only HTJ2K encoder includes one bitplane"
);
assert!(
!encoded.data.is_empty(),
"non-zero cleanup-only block must still produce payload bytes"
);
}
#[test]
fn classic_decoder_hook_is_used_for_j2k_codeblocks() {
let bytes = fixture();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut hooked_context = DecoderContext::default();
let mut hook = FailingClassicDecoder { called: false };
let Err(error) = image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) else {
panic!("hooked decode must use external classic decoder");
};
assert!(hook.called, "classic decoder hook must be invoked");
assert_eq!(
error,
DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
);
}
#[test]
fn classic_sub_band_decoder_hook_is_used_for_j2k_codeblocks() {
let bytes = fixture_multi_block();
let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
let mut hooked_context = DecoderContext::default();
let mut hook = FailingClassicBatchDecoder { called: false };
let Err(error) = image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) else {
panic!("hooked decode must use external classic batch decoder");
};
assert!(hook.called, "classic sub-band decoder hook must be invoked");
assert_eq!(
error,
DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
);
}
#[test]
fn forward_dwt53_reference_matches_internal_path() {
let samples: Vec<f32> = (0_u8..16).map(f32::from).collect();
let out = forward_dwt53_reference(&samples, 4, 4, 1).expect("fallible 5/3 reference DWT");
let internal = j2c::fdwt::forward_dwt(&samples, 4, 4, 1, true);
assert_eq!(out.ll, internal.ll, "LL subband mismatch");
assert_eq!(out.ll_width, internal.ll_width, "LL width mismatch");
assert_eq!(out.ll_height, internal.ll_height, "LL height mismatch");
assert_eq!(out.levels.len(), internal.levels.len(), "level count");
for (pub_lvl, int_lvl) in out.levels.iter().zip(internal.levels.iter()) {
assert_eq!(pub_lvl.hl, int_lvl.hl, "HL mismatch");
assert_eq!(pub_lvl.lh, int_lvl.lh, "LH mismatch");
assert_eq!(pub_lvl.hh, int_lvl.hh, "HH mismatch");
}
}
#[test]
fn scalar_forward_dwt_rejects_caller_geometry_with_typed_errors() {
let short = forward_dwt53_reference(&[0.0; 3], 2, 2, 1)
.expect_err("short 5/3 reference plane must fail");
assert_eq!(
short,
EncodeError::InvalidInput {
what: "packed forward DWT coefficient length mismatch",
}
);
let zero = forward_dwt97_reference(&[], 0, 1, 1)
.expect_err("zero-width 9/7 reference plane must fail");
assert_eq!(
zero,
EncodeError::InvalidInput {
what: "packed forward DWT dimensions must be non-zero",
}
);
}
#[test]
fn forward_rct_reference_matches_internal_path() {
let planes = vec![vec![100.0f32], vec![150.0f32], vec![200.0f32]];
let result = forward_rct_reference(planes.clone());
let mut internal = planes;
j2c::forward_mct::forward_rct(&mut internal);
assert_eq!(result, internal, "RCT output mismatch");
assert_eq!(result[0][0], 150.0, "Y component");
assert_eq!(result[1][0], 50.0, "Cb component");
assert_eq!(result[2][0], -50.0, "Cr component");
}
#[test]
fn forward_ict_reference_matches_internal_path() {
let planes = vec![vec![100.0f32], vec![150.0f32], vec![200.0f32]];
let result = forward_ict_reference(planes.clone());
let mut internal = planes;
j2c::forward_mct::forward_ict(&mut internal);
assert_eq!(result, internal, "ICT output mismatch");
}
#[test]
fn forward_dwt97_reference_matches_internal_path() {
let samples = (0..64)
.map(|idx| {
f32::from(u8::try_from((idx * 19 + idx / 3) & 0xff).expect("masked sample fits u8"))
- 128.0
})
.collect::<Vec<_>>();
let result = forward_dwt97_reference(&samples, 8, 8, 2).expect("fallible 9/7 reference DWT");
let internal = j2c::fdwt::forward_dwt(&samples, 8, 8, 2, false);
assert_eq!(result.ll, internal.ll, "DWT 9/7 LL mismatch");
assert_eq!(result.ll_width, internal.ll_width);
assert_eq!(result.ll_height, internal.ll_height);
assert_eq!(result.levels.len(), internal.levels.len());
for (actual, expected) in result.levels.iter().zip(internal.levels.iter()) {
assert_eq!(actual.hl, expected.hl, "DWT 9/7 HL mismatch");
assert_eq!(actual.lh, expected.lh, "DWT 9/7 LH mismatch");
assert_eq!(actual.hh, expected.hh, "DWT 9/7 HH mismatch");
}
}
#[test]
fn quantize_reversible_reference_matches_internal_path() {
let coefficients = vec![3.7f32, -8.2, 0.5, -0.5, 10.0];
let exponent = 8u16;
let mantissa = 0u16;
let range_bits = 8u8;
let result = quantize_reversible_reference(&coefficients, exponent, mantissa, range_bits, true);
let step = j2c::quantize::QuantStepSize { exponent, mantissa };
let internal = j2c::quantize::quantize_subband(&coefficients, &step, range_bits, true);
assert_eq!(result, internal, "quantize output mismatch");
assert_eq!(result[0], 4, "3.7 rounds to 4");
assert_eq!(result[1], -8, "-8.2 rounds to -8");
}
#[test]
fn quantize_subband_reference_matches_irreversible_internal_path() {
let coefficients = vec![3.7f32, -8.2, 0.5, -0.5, 10.0];
let exponent = 8u16;
let mantissa = 256u16;
let range_bits = 8u8;
let result = quantize_subband_reference(&coefficients, exponent, mantissa, range_bits, false);
let step = j2c::quantize::QuantStepSize { exponent, mantissa };
let internal = j2c::quantize::quantize_subband(&coefficients, &step, range_bits, false);
assert_eq!(result, internal, "irreversible quantize output mismatch");
}
#[test]
fn deinterleave_reference_matches_internal_path() {
let pixels: Vec<u8> = vec![128, 64, 200, 10, 20, 30];
let result = try_deinterleave_reference(&pixels, 2, 3, 8, false)
.expect("valid deinterleave reference input");
let internal = j2c::encode::deinterleave_to_f32(&pixels, 2, 3, 8, false);
assert_eq!(result, internal, "deinterleave output mismatch");
assert_eq!(result.len(), 3, "three component planes");
assert_eq!(result[0].len(), 2, "two pixels per plane");
assert!((result[0][0] - 0.0f32).abs() < 1e-6, "R0 level-shifted");
assert!((result[1][0] - (-64.0f32)).abs() < 1e-6, "G0 level-shifted");
assert!((result[2][0] - 72.0f32).abs() < 1e-6, "B0 level-shifted");
}
#[test]
fn try_deinterleave_reference_rejects_invalid_geometry() {
let valid_pixels: Vec<u8> = vec![128, 64, 200, 10, 20, 30];
for (label, result) in [
(
"zero components",
try_deinterleave_reference(&valid_pixels, 2, 0, 8, false),
),
(
"zero bit depth",
try_deinterleave_reference(&valid_pixels, 2, 3, 0, false),
),
(
"unsupported bit depth",
try_deinterleave_reference(&valid_pixels, 2, 3, 39, false),
),
(
"short input",
try_deinterleave_reference(&valid_pixels[..5], 2, 3, 8, false),
),
(
"trailing bytes",
try_deinterleave_reference(&[valid_pixels.as_slice(), &[0]].concat(), 2, 3, 8, false),
),
] {
assert!(
matches!(
result,
Err(DecodeError::Validation(
ValidationError::InvalidComponentMetadata
))
),
"{label} should be rejected, got {result:?}"
);
}
}
#[test]
fn decode_settings_constructors_make_strictness_explicit() {
assert!(DecodeSettings::default().strict);
assert!(!DecodeSettings::default().lenient_tolerance_enabled());
assert!(DecodeSettings::lenient().lenient_tolerance_enabled());
assert!(!DecodeSettings::strict().lenient_tolerance_enabled());
assert!(DecodeSettings::strict().strict);
}