use j2k::{
encode_j2k_lossless, J2kBlockCodingMode, J2kEncodeValidation, J2kError,
J2kLosslessEncodeOptions, J2kLosslessSamples, J2kToHtj2kMode, J2kToHtj2kOptions,
ReversibleTransform,
};
use j2k_core::{CodecError, CompressedPayloadKind, CompressedTransferSyntax};
use j2k_native::{DecodeSettings, EncodeOptions, Image};
use j2k_test_support::{patterned_gray8, patterned_rgb8, wrap_jp2_codestream};
fn decode_native(codestream: &[u8]) -> j2k_native::RawBitmap {
Image::new(codestream, &DecodeSettings::default())
.expect("codestream should parse")
.decode_native()
.expect("codestream should decode")
}
fn lossless_options(block_coding_mode: J2kBlockCodingMode) -> J2kLosslessEncodeOptions {
J2kLosslessEncodeOptions::default()
.with_block_coding_mode(block_coding_mode)
.with_validation(J2kEncodeValidation::External)
}
fn native_encode_options(reversible: bool, use_mct: bool) -> EncodeOptions {
EncodeOptions {
reversible,
use_mct,
use_ht_block_coding: false,
num_decomposition_levels: 1,
validate_high_throughput_codestream: false,
..EncodeOptions::default()
}
}
#[test]
fn classic_lossless_53_rgb_recode_to_htj2k_decodes_pixel_exact() {
let width = 64;
let height = 64;
let pixels = patterned_rgb8(width, height);
let samples =
J2kLosslessSamples::new(&pixels, width, height, 3, 8, false).expect("valid RGB samples");
let classic = encode_j2k_lossless(
samples,
&lossless_options(J2kBlockCodingMode::Classic)
.with_reversible_transform(ReversibleTransform::Rct53),
)
.expect("classic lossless encode")
.codestream;
let recoded = j2k::recode_j2k_to_htj2k_lossless(&classic, J2kToHtj2kOptions::default())
.expect("coefficient-domain recode");
assert_eq!(recoded.report.mode, J2kToHtj2kMode::CoefficientPreserving);
assert_eq!(
recoded.report.output_transfer_syntax,
CompressedTransferSyntax::HtJpeg2000Lossless
);
assert!(recoded.bytes.starts_with(&[0xff, 0x4f]));
let decoded = decode_native(&recoded.bytes);
assert_eq!((decoded.width, decoded.height), (width, height));
assert_eq!(decoded.num_components, 3);
assert_eq!(decoded.bit_depth, 8);
assert_eq!(decoded.data, pixels);
}
#[test]
fn classic_lossless_53_gray16_recode_to_htj2k_decodes_pixel_exact() {
let width = 64;
let height = 64;
let mut pixels = Vec::new();
for sample in patterned_gray8(width, height) {
let value = u16::from(sample) * 257;
pixels.extend_from_slice(&value.to_le_bytes());
}
let samples = J2kLosslessSamples::new(&pixels, width, height, 1, 16, false)
.expect("valid gray16 samples");
let classic = encode_j2k_lossless(samples, &lossless_options(J2kBlockCodingMode::Classic))
.expect("classic lossless encode")
.codestream;
let recoded = j2k::recode_j2k_to_htj2k_lossless(&classic, J2kToHtj2kOptions::default())
.expect("coefficient-domain recode");
assert_eq!(recoded.report.mode, J2kToHtj2kMode::CoefficientPreserving);
let decoded = decode_native(&recoded.bytes);
assert_eq!(decoded.data, pixels);
}
#[test]
fn jp2_wrapped_classic_lossless_53_recode_emits_raw_htj2k_codestream() {
let width = 64;
let height = 64;
let pixels = patterned_rgb8(width, height);
let samples =
J2kLosslessSamples::new(&pixels, width, height, 3, 8, false).expect("valid RGB samples");
let classic = encode_j2k_lossless(
samples,
&lossless_options(J2kBlockCodingMode::Classic)
.with_reversible_transform(ReversibleTransform::Rct53),
)
.expect("classic lossless encode")
.codestream;
let jp2 = wrap_jp2_codestream(&classic, width, height, 3, 8, 16);
let recoded = j2k::recode_j2k_to_htj2k_lossless(&jp2, J2kToHtj2kOptions::default())
.expect("JP2 coefficient-domain recode");
assert_eq!(
recoded.report.input_payload_kind,
CompressedPayloadKind::Jp2File
);
assert_eq!(
recoded.report.output_payload_kind,
CompressedPayloadKind::Jpeg2000Codestream
);
assert!(recoded.bytes.starts_with(&[0xff, 0x4f]));
assert_eq!(decode_native(&recoded.bytes).data, pixels);
}
#[test]
fn already_raw_htj2k_lossless_returns_passthrough() {
let width = 32;
let height = 32;
let pixels = patterned_gray8(width, height);
let samples =
J2kLosslessSamples::new(&pixels, width, height, 1, 8, false).expect("valid gray samples");
let htj2k = encode_j2k_lossless(
samples,
&lossless_options(J2kBlockCodingMode::HighThroughput),
)
.expect("HTJ2K encode")
.codestream;
let recoded = j2k::recode_j2k_to_htj2k_lossless(&htj2k, J2kToHtj2kOptions::default())
.expect("passthrough recode");
assert_eq!(recoded.report.mode, J2kToHtj2kMode::Passthrough);
assert_eq!(recoded.bytes, htj2k);
}
#[test]
fn malformed_input_returns_explicit_error() {
let err = j2k::recode_j2k_to_htj2k_lossless(b"not jpeg 2000", J2kToHtj2kOptions::default())
.expect_err("malformed input should fail");
assert!(matches!(err, J2kError::Unsupported(_)) || err.is_truncated());
}
#[test]
fn lossy_97_source_is_rejected_for_lossless_53_coefficient_recode() {
let width = 32;
let height = 32;
let pixels = patterned_gray8(width, height);
let lossy = j2k_native::encode(
&pixels,
width,
height,
1,
8,
false,
&native_encode_options(false, false),
)
.expect("lossy 9/7 encode");
let err = j2k::recode_j2k_to_htj2k_lossless(&lossy, J2kToHtj2kOptions::default())
.expect_err("lossy source should fail");
assert!(matches!(err, J2kError::Unsupported(_)));
}
#[test]
fn signed_source_is_rejected_before_recode() {
let pixels = [0_u8, 1, 255, 127];
let signed = j2k_native::encode(
&pixels,
2,
2,
1,
8,
true,
&native_encode_options(true, false),
)
.expect("signed classic encode");
let err = j2k::recode_j2k_to_htj2k_lossless(&signed, J2kToHtj2kOptions::default())
.expect_err("signed source should fail");
assert!(matches!(err, J2kError::Unsupported(_)));
}
#[test]
fn unsupported_component_count_is_rejected() {
let pixels = vec![127_u8; 16 * 16 * 4];
let four_component = j2k_native::encode(
&pixels,
16,
16,
4,
8,
false,
&native_encode_options(true, false),
)
.expect("four-component classic encode");
let err = j2k::recode_j2k_to_htj2k_lossless(&four_component, J2kToHtj2kOptions::default())
.expect_err("four-component source should fail");
assert!(matches!(err, J2kError::Unsupported(_)));
}