Skip to main content

j2k_native/
lib.rs

1/*!
2Internal pure-Rust JPEG 2000 codec engine for `j2k`.
3
4This module tree was imported from the `dicom-toolkit-jpeg2000` 0.5.0 crate
5and adapted in-repo so `j2k` no longer depends on an external
6production decoder crate.
7
8`dicom-toolkit-jpeg2000` is the JPEG 2000 engine used by `dicom-toolkit-rs`.
9It is a maintained fork of the original `hayro-jpeg2000` project with
10DICOM-focused extensions, including native-bit-depth decode for 8/12/16-bit
11images and pure-Rust JPEG 2000 encoding.
12
13The crate can decode both raw JPEG 2000 codestreams (`.j2c`) and images wrapped
14inside the JP2 container format. The decoder supports the vast majority of features
15defined in the JPEG 2000 core coding system (ISO/IEC 15444-1) as well as some color
16spaces from the extensions (ISO/IEC 15444-2). There are still some missing pieces
17for some "obscure" features (for example support for progression order
18changes in tile-parts), but the features that commonly appear in real-world
19images are supported.
20
21The crate offers both a high-level 8-bit decode path for general image use and
22a native-bit-depth decode path for integrations such as DICOM, plus encoder APIs
23for emitting raw JPEG 2000 and HTJ2K codestreams.
24
25# Example
26```rust,no_run
27use j2k_native::{DecodeSettings, Image};
28
29let data = std::fs::read("image.jp2").unwrap();
30let image = Image::new(&data, &DecodeSettings::default()).unwrap();
31
32println!(
33    "{}x{} image in {:?} with alpha={}",
34    image.width(),
35    image.height(),
36    image.color_space(),
37    image.has_alpha(),
38);
39
40let bitmap = image.decode().unwrap();
41```
42
43If you want to see a more comprehensive example, please take a look
44at the example in [GitHub](https://github.com/knopkem/dicom-toolkit-rs/blob/main/crates/dicom-toolkit-jpeg2000/examples/png.rs),
45which shows the main steps needed to convert a JPEG 2000 image into PNG.
46
47# Testing
48The decoder has been tested against 20.000+ images scraped from random PDFs
49on the internet and also passes a large part of the `OpenJPEG` test suite. So you
50can expect the crate to perform decently in terms of decoding correctness.
51
52# Performance
53A decent amount of effort has already been put into optimizing this crate
54(both in terms of raw performance but also memory allocations). However, there
55are some more important optimizations that have not been implemented yet, so
56there is definitely still room for improvement (and I am planning on implementing
57them eventually).
58
59Overall, you should expect this crate to have worse performance than `OpenJPEG`,
60but the difference gap should not be too large.
61
62# Safety
63By default, the crate has the `simd` feature enabled, which uses the
64[`fearless_simd`](https://github.com/linebender/fearless_simd) crate to accelerate
65important parts of the pipeline. If you want to eliminate any usage of unsafe
66in this crate as well as its dependencies, you can simply disable this
67feature, at the cost of worse decoding performance. Unsafe code is forbidden
68via a crate-level attribute.
69
70The crate is `no_std` compatible but requires an allocator to be available.
71*/
72
73#![cfg_attr(not(feature = "std"), no_std)]
74#![forbid(unsafe_code)]
75#![forbid(missing_docs)]
76#![allow(clippy::too_many_arguments)]
77
78extern crate alloc;
79
80use alloc::vec;
81use alloc::vec::Vec;
82
83use crate::error::{bail, err};
84use crate::j2c::{ComponentData, Header};
85use crate::jp2::cdef::{ChannelAssociation, ChannelType};
86use crate::jp2::cmap::ComponentMappingType;
87use crate::jp2::colr::{CieLab, EnumeratedColorspace};
88use crate::jp2::icc::ICCMetadata;
89use crate::jp2::{DecodedImage, ImageBoxes};
90
91pub mod error;
92mod inspect;
93#[macro_use]
94pub(crate) mod log;
95mod direct_cpu;
96mod direct_plan;
97pub(crate) mod math;
98#[doc(hidden)]
99pub mod packet_math;
100pub(crate) mod profile;
101pub(crate) mod writer;
102
103use crate::math::{dispatch, f32x8, Level, Simd, SIMD_WIDTH};
104pub use direct_cpu::{
105    execute_direct_color_plan_rgb8_into, execute_direct_color_plan_rgba8_into, J2kDirectCpuScratch,
106};
107pub use direct_plan::{
108    HtOwnedCodeBlockBatchJob, HtOwnedSubBandPlan, J2kDirectBandId, J2kDirectColorPlan,
109    J2kDirectGrayscalePlan, J2kDirectGrayscaleStep, J2kDirectIdwtStep, J2kDirectStoreStep,
110    J2kOwnedCodeBlockBatchJob, J2kOwnedSubBandPlan,
111};
112pub use inspect::{
113    inspect_j2k_codestream_header, looks_like_j2k_codestream, J2kCodestreamComponentHeader,
114    J2kCodestreamHeaderError, J2kCodestreamHeaderMetadata,
115};
116
117/// Maps an output coordinate within an IDWT step to the source sub-band index.
118///
119/// `origin` is the global coordinate of the IDWT output rectangle,
120/// `local_coord` is the coordinate within that output rectangle, and
121/// `low_pass` selects the low-pass (`LL`/`LH`) or high-pass (`HL`/`HH`) band
122/// along one axis. This helper is exposed so backend adapters can compute
123/// required input windows with the same odd-origin rounding as the native IDWT.
124#[must_use]
125pub fn idwt_band_index(origin: u32, local_coord: u32, low_pass: bool) -> u32 {
126    let global = u64::from(origin) + u64::from(local_coord);
127    let origin = u64::from(origin);
128    let index = if low_pass {
129        global.div_ceil(2).saturating_sub(origin.div_ceil(2))
130    } else {
131        (global / 2).saturating_sub(origin / 2)
132    };
133    u32::try_from(index).unwrap_or(u32::MAX)
134}
135
136pub use error::{
137    ColorError, DecodeError, DecodingError, FormatError, MarkerError, Result, TileError,
138    ValidationError,
139};
140pub use j2c::encode::{
141    encode, encode_htj2k, encode_precomputed_htj2k_53,
142    encode_precomputed_htj2k_53_with_accelerator, encode_precomputed_htj2k_53_with_mct,
143    encode_precomputed_htj2k_53_with_mct_and_accelerator, encode_precomputed_htj2k_97,
144    encode_precomputed_htj2k_97_batch_with_accelerator,
145    encode_precomputed_htj2k_97_with_accelerator, encode_preencoded_htj2k_97,
146    encode_preencoded_htj2k_97_compact_owned_with_accelerator,
147    encode_preencoded_htj2k_97_owned_with_accelerator, encode_preencoded_htj2k_97_with_accelerator,
148    encode_prequantized_htj2k_97, encode_prequantized_htj2k_97_with_accelerator,
149    encode_with_accelerator, irreversible_quantization_step_for_subband, EncodeOptions,
150    EncodeProgressionOrder,
151};
152pub use j2c::{CpuDecodeParallelism, DecoderContext, Reversible53CoefficientImage};
153pub use j2k_types::{
154    CpuOnlyJ2kEncodeStageAccelerator, EncodedHtJ2kCodeBlock, EncodedJ2kCodeBlock,
155    IrreversibleQuantizationStep, IrreversibleQuantizationSubbandScales, J2kCodeBlockSegment,
156    J2kCodeBlockStyle, J2kDeinterleaveToF32Job, J2kEncodeDispatchReport, J2kForwardDwt53Job,
157    J2kForwardDwt53Level, J2kForwardDwt53Output, J2kForwardDwt97Job, J2kForwardDwt97Level,
158    J2kForwardDwt97Output, J2kForwardIctJob, J2kForwardRctJob, J2kHtCodeBlockEncodeJob,
159    J2kHtSubbandEncodeJob, J2kHtj2kTileEncodeJob, J2kPacketizationBlockCodingMode,
160    J2kPacketizationCodeBlock, J2kPacketizationEncodeJob, J2kPacketizationPacketDescriptor,
161    J2kPacketizationProgressionOrder, J2kPacketizationResolution, J2kPacketizationSubband,
162    J2kQuantizeSubbandJob, J2kSubBandType, J2kTier1CodeBlockEncodeJob, PrecomputedHtj2k53Component,
163    PrecomputedHtj2k53Image, PrecomputedHtj2k97Component, PrecomputedHtj2k97Image,
164    PreencodedHtj2k97CodeBlock, PreencodedHtj2k97CompactCodeBlock,
165    PreencodedHtj2k97CompactComponent, PreencodedHtj2k97CompactImage,
166    PreencodedHtj2k97CompactResolution, PreencodedHtj2k97CompactSubband,
167    PreencodedHtj2k97Component, PreencodedHtj2k97Image, PreencodedHtj2k97Resolution,
168    PreencodedHtj2k97Subband, PrequantizedHtj2k97CodeBlock, PrequantizedHtj2k97Component,
169    PrequantizedHtj2k97Image, PrequantizedHtj2k97Resolution, PrequantizedHtj2k97Subband,
170};
171
172mod j2c;
173mod jp2;
174pub(crate) mod reader;
175pub use j2c::ht_encode_tables::HtUvlcTableEntry;
176
177const MAX_CLASSIC_DECODE_BITPLANES: u8 = 32;
178pub(crate) const MAX_J2K_SPEC_COMPONENTS: u16 = 16_384;
179pub(crate) const MAX_NATIVE_DECODE_COMPONENTS: u16 = u8::MAX as u16;
180pub(crate) const MAX_J2K_IMAGE_DIMENSION: u32 = 60_000;
181pub(crate) const MAX_J2K_TILE_COUNT: u64 = u16::MAX as u64 + 1;
182pub(crate) const DEFAULT_MAX_DECODE_BYTES: usize = 512 * 1024 * 1024;
183
184#[inline]
185pub(crate) fn checked_decode_usize_product2(left: usize, right: usize) -> Result<usize> {
186    left.checked_mul(right)
187        .ok_or(ValidationError::ImageTooLarge.into())
188}
189
190#[inline]
191fn checked_decode_byte_cap(len: usize) -> Result<usize> {
192    if len > DEFAULT_MAX_DECODE_BYTES {
193        bail!(ValidationError::ImageTooLarge);
194    }
195    Ok(len)
196}
197
198#[inline]
199pub(crate) fn checked_decode_byte_len2(left: usize, right: usize) -> Result<usize> {
200    checked_decode_byte_cap(checked_decode_usize_product2(left, right)?)
201}
202
203#[inline]
204pub(crate) fn checked_decode_byte_len3(first: usize, second: usize, third: usize) -> Result<usize> {
205    let partial = checked_decode_usize_product2(first, second)?;
206    checked_decode_byte_cap(checked_decode_usize_product2(partial, third)?)
207}
208
209#[inline]
210pub(crate) fn checked_decode_byte_len4(
211    first: usize,
212    second: usize,
213    third: usize,
214    fourth: usize,
215) -> Result<usize> {
216    let partial = checked_decode_usize_product2(first, second)?;
217    let partial = checked_decode_usize_product2(partial, third)?;
218    checked_decode_byte_cap(checked_decode_usize_product2(partial, fourth)?)
219}
220
221#[inline]
222pub(crate) fn checked_decode_sample_count(width: u32, height: u32) -> Result<usize> {
223    #[cfg(target_pointer_width = "64")]
224    {
225        Ok((u64::from(width) * u64::from(height)) as usize)
226    }
227
228    #[cfg(not(target_pointer_width = "64"))]
229    {
230        checked_decode_usize_product2(width as usize, height as usize)
231    }
232}
233
234/// Adapter HTJ2K code-block job description for backend experimentation.
235#[derive(Debug, Clone, Copy)]
236pub struct HtCodeBlockDecodeJob<'a> {
237    /// Combined cleanup/refinement bytes for the code block.
238    pub data: &'a [u8],
239    /// Cleanup segment length in bytes.
240    pub cleanup_length: u32,
241    /// Refinement segment length in bytes.
242    pub refinement_length: u32,
243    /// Code-block width in samples.
244    pub width: u32,
245    /// Code-block height in samples.
246    pub height: u32,
247    /// Output row stride, in samples, for the target sub-band storage.
248    pub output_stride: usize,
249    /// Missing most-significant bit planes for this code block.
250    pub missing_bit_planes: u8,
251    /// Number of coding passes present for this code block.
252    pub number_of_coding_passes: u8,
253    /// Total coded bitplanes for the parent sub-band.
254    pub num_bitplanes: u8,
255    /// Region-of-interest maxshift value from RGN marker metadata.
256    pub roi_shift: u8,
257    /// Whether vertically causal context was enabled.
258    pub stripe_causal: bool,
259    /// Whether strict decode validation is enabled for the parent image.
260    pub strict: bool,
261    /// Dequantization step to apply to decoded coefficients.
262    pub dequantization_step: f32,
263}
264
265/// Adapter HTJ2K scalar decode phase limit for backend experimentation.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum HtCodeBlockDecodePhaseLimit {
268    /// Stop after the cleanup pass has produced coefficient magnitudes/signs.
269    Cleanup,
270    /// Stop after the significance propagation refinement pass.
271    SignificancePropagation,
272    /// Decode through the magnitude refinement pass when present.
273    MagnitudeRefinement,
274}
275
276/// Adapter HTJ2K batched code-block decode job for one sub-band.
277#[derive(Debug, Clone, Copy)]
278pub struct HtCodeBlockBatchJob<'a> {
279    /// X offset within the target sub-band coefficient buffer.
280    pub output_x: u32,
281    /// Y offset within the target sub-band coefficient buffer.
282    pub output_y: u32,
283    /// The actual code-block decode parameters.
284    pub code_block: HtCodeBlockDecodeJob<'a>,
285}
286
287/// Adapter HTJ2K batched sub-band decode request for backend experimentation.
288#[derive(Debug, Clone, Copy)]
289pub struct HtSubBandDecodeJob<'a> {
290    /// Sub-band width in samples.
291    pub width: u32,
292    /// Sub-band height in samples.
293    pub height: u32,
294    /// Code blocks to decode into this sub-band.
295    pub jobs: &'a [HtCodeBlockBatchJob<'a>],
296}
297
298/// Adapter Classic Tier-1 compact token segment for backend experimentation.
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub struct J2kTier1TokenSegment {
301    /// Bit offset of this segment within the compact token buffer.
302    pub token_bit_offset: u32,
303    /// Number of token bits in this segment.
304    ///
305    /// Arithmetic segments contain 6-bit MQ tokens. Raw bypass segments contain
306    /// one bit per raw bypass event.
307    pub token_bit_count: u32,
308    /// First coding pass covered by this segment.
309    pub start_coding_pass: u8,
310    /// One-past-last coding pass covered by this segment.
311    pub end_coding_pass: u8,
312    /// Whether this segment should be packed through the MQ arithmetic path.
313    pub use_arithmetic: bool,
314}
315
316/// Adapter classic J2K code-block job description for backend experimentation.
317#[derive(Debug, Clone, Copy)]
318pub struct J2kCodeBlockDecodeJob<'a> {
319    /// Combined payload bytes for all coded segments in this code block.
320    pub data: &'a [u8],
321    /// Coded segments for the code block.
322    pub segments: &'a [J2kCodeBlockSegment],
323    /// Code-block width in samples.
324    pub width: u32,
325    /// Code-block height in samples.
326    pub height: u32,
327    /// Output row stride, in samples, for the target sub-band storage.
328    pub output_stride: usize,
329    /// Missing most-significant bit planes for this code block.
330    pub missing_bit_planes: u8,
331    /// Number of coding passes present for this code block.
332    pub number_of_coding_passes: u8,
333    /// Total coded bitplanes for the parent sub-band.
334    pub total_bitplanes: u8,
335    /// Region-of-interest maxshift value from RGN marker metadata.
336    pub roi_shift: u8,
337    /// The sub-band type containing this code block.
338    pub sub_band_type: J2kSubBandType,
339    /// The code-block style flags.
340    pub style: J2kCodeBlockStyle,
341    /// Whether strict decode validation is enabled for the parent image.
342    pub strict: bool,
343    /// Dequantization step to apply to decoded coefficients.
344    pub dequantization_step: f32,
345}
346
347/// Adapter HTJ2K cleanup-encode shape counters for backend benchmarking.
348#[derive(Debug, Clone, Default, PartialEq, Eq)]
349pub struct HtCleanupEncodeDistribution {
350    /// Total 2x2 cleanup quads visited.
351    pub total_quads: u64,
352    /// Quads encoded in the first cleanup row pair.
353    pub initial_quads: u64,
354    /// Quads encoded after the first cleanup row pair.
355    pub non_initial_quads: u64,
356    /// All-quad `rho` histogram, indexed by the low four `rho` bits.
357    pub rho_counts: [u64; 16],
358    /// First-row-pair `rho` histogram, indexed by the low four `rho` bits.
359    pub initial_rho_counts: [u64; 16],
360    /// Non-initial-row `rho` histogram, indexed by the low four `rho` bits.
361    pub non_initial_rho_counts: [u64; 16],
362    /// Non-initial-row `u_q` histogram.
363    pub non_initial_u_q_counts: [u64; 32],
364    /// Non-initial-row `e_qmax` histogram.
365    pub non_initial_e_qmax_counts: [u64; 32],
366    /// Non-initial-row `kappa` histogram.
367    pub non_initial_kappa_counts: [u64; 32],
368    /// Non-initial-row joint `rho`/`u_q` histogram.
369    pub non_initial_rho_u_q_counts: [[u64; 32]; 16],
370    /// Calls that emitted at least one magnitude/sign sample.
371    pub mag_sign_calls: u64,
372    /// Magnitude/sign call histogram, indexed by the low four `rho` bits.
373    pub mag_sign_rho_counts: [u64; 16],
374    /// Encoded magnitude/sign sample payload bit-count histogram.
375    pub mag_sign_sample_bit_counts: [u64; 32],
376    /// Number of individual magnitude/sign samples emitted.
377    pub mag_sign_encoded_samples: u64,
378}
379
380/// Adapter JPEG 2000 encode-stage accelerator for backend experimentation.
381pub trait J2kEncodeStageAccelerator {
382    /// Report cumulative backend dispatches completed by this accelerator.
383    fn dispatch_report(&self) -> J2kEncodeDispatchReport {
384        J2kEncodeDispatchReport::default()
385    }
386
387    /// Optionally deinterleave interleaved pixel bytes into f32 component planes.
388    ///
389    /// Return `Ok(Some(components))` with one plane per component. Return
390    /// `Ok(None)` to use the CPU fallback.
391    fn encode_deinterleave(
392        &mut self,
393        _job: J2kDeinterleaveToF32Job<'_>,
394    ) -> core::result::Result<Option<Vec<Vec<f32>>>, &'static str> {
395        Ok(None)
396    }
397
398    /// Optionally apply forward RCT in place.
399    ///
400    /// Return `Ok(true)` after writing transformed planes. Return `Ok(false)`
401    /// to use the CPU fallback.
402    fn encode_forward_rct(
403        &mut self,
404        _job: J2kForwardRctJob<'_>,
405    ) -> core::result::Result<bool, &'static str> {
406        Ok(false)
407    }
408
409    /// Optionally apply forward ICT in place.
410    ///
411    /// Return `Ok(true)` after writing transformed planes. Return `Ok(false)`
412    /// to use the CPU fallback.
413    fn encode_forward_ict(
414        &mut self,
415        _job: J2kForwardIctJob<'_>,
416    ) -> core::result::Result<bool, &'static str> {
417        Ok(false)
418    }
419
420    /// Optionally run a forward reversible 5/3 DWT.
421    ///
422    /// Return `Ok(Some(output))` with all subbands populated. Return
423    /// `Ok(None)` to use the CPU fallback.
424    fn encode_forward_dwt53(
425        &mut self,
426        _job: J2kForwardDwt53Job<'_>,
427    ) -> core::result::Result<Option<J2kForwardDwt53Output>, &'static str> {
428        Ok(None)
429    }
430
431    /// Optionally run a forward irreversible 9/7 DWT.
432    ///
433    /// Return `Ok(Some(output))` with all subbands populated. Return
434    /// `Ok(None)` to use the CPU fallback.
435    fn encode_forward_dwt97(
436        &mut self,
437        _job: J2kForwardDwt97Job<'_>,
438    ) -> core::result::Result<Option<J2kForwardDwt97Output>, &'static str> {
439        Ok(None)
440    }
441
442    /// Optionally quantize one sub-band.
443    ///
444    /// Return `Ok(Some(coefficients))` with one quantized coefficient for each
445    /// input coefficient. Return `Ok(None)` to use the CPU fallback.
446    fn encode_quantize_subband(
447        &mut self,
448        _job: J2kQuantizeSubbandJob<'_>,
449    ) -> core::result::Result<Option<Vec<i32>>, &'static str> {
450        Ok(None)
451    }
452
453    /// Optionally encode one classic Tier-1 code-block.
454    ///
455    /// Return `Ok(Some(output))` with encoded bytes and pass metadata. Return
456    /// `Ok(None)` to use the CPU fallback.
457    fn encode_tier1_code_block(
458        &mut self,
459        _job: J2kTier1CodeBlockEncodeJob<'_>,
460    ) -> core::result::Result<Option<EncodedJ2kCodeBlock>, &'static str> {
461        Ok(None)
462    }
463
464    /// Optionally encode multiple classic Tier-1 code-blocks in one backend dispatch.
465    ///
466    /// Return `Ok(Some(outputs))` with one encoded output per input job. Return
467    /// `Ok(None)` to use the per-block hook or CPU fallback.
468    fn encode_tier1_code_blocks(
469        &mut self,
470        _jobs: &[J2kTier1CodeBlockEncodeJob<'_>],
471    ) -> core::result::Result<Option<Vec<EncodedJ2kCodeBlock>>, &'static str> {
472        Ok(None)
473    }
474
475    /// Optionally encode one HTJ2K code-block.
476    ///
477    /// Return `Ok(Some(output))` with encoded bytes and pass metadata. Return
478    /// `Ok(None)` to use the CPU fallback.
479    fn encode_ht_code_block(
480        &mut self,
481        _job: J2kHtCodeBlockEncodeJob<'_>,
482    ) -> core::result::Result<Option<EncodedHtJ2kCodeBlock>, &'static str> {
483        Ok(None)
484    }
485
486    /// Optionally encode multiple HTJ2K code-blocks in one backend dispatch.
487    ///
488    /// Return `Ok(Some(outputs))` with one encoded output per input job. Return
489    /// `Ok(None)` to use the per-block hook or CPU fallback.
490    fn encode_ht_code_blocks(
491        &mut self,
492        _jobs: &[J2kHtCodeBlockEncodeJob<'_>],
493    ) -> core::result::Result<Option<Vec<EncodedHtJ2kCodeBlock>>, &'static str> {
494        Ok(None)
495    }
496
497    /// Optionally quantize and encode one HTJ2K cleanup-only sub-band.
498    ///
499    /// Return `Ok(Some(outputs))` with one encoded output per code block in
500    /// raster code-block order. Return `Ok(None)` to use the separate
501    /// quantization and code-block hooks or CPU fallback.
502    fn encode_ht_subband(
503        &mut self,
504        _job: J2kHtSubbandEncodeJob<'_>,
505    ) -> core::result::Result<Option<Vec<EncodedHtJ2kCodeBlock>>, &'static str> {
506        Ok(None)
507    }
508
509    /// Optionally encode the complete HTJ2K tile packet body.
510    ///
511    /// Return `Ok(Some(bytes))` with the complete tile bitstream body. CPU
512    /// marker/header writing remains outside this hook. Return `Ok(None)` to
513    /// use the normal staged encode pipeline.
514    fn encode_htj2k_tile(
515        &mut self,
516        _job: J2kHtj2kTileEncodeJob<'_>,
517    ) -> core::result::Result<Option<Vec<u8>>, &'static str> {
518        Ok(None)
519    }
520
521    /// Return whether native CPU code-block fallback should use internal rayon parallelism.
522    ///
523    /// External accelerators keep serial per-block fallback so their hooks still
524    /// observe every fallback block after a declined batch hook.
525    fn prefer_parallel_cpu_code_block_fallback(&self) -> bool {
526        false
527    }
528
529    /// Return whether whole-tile CPU-only batch encode may be parallelized by callers.
530    ///
531    /// This is narrower than [`Self::prefer_parallel_cpu_code_block_fallback`]:
532    /// callers must only bypass the supplied accelerator when it is known to
533    /// have no observable hooks.
534    fn prefer_parallel_cpu_tile_encode(&self) -> bool {
535        false
536    }
537
538    /// Optionally packetize prepared packet contributions.
539    ///
540    /// Return `Ok(Some(bytes))` with the complete tile bitstream. Return
541    /// `Ok(None)` to use the CPU fallback.
542    fn encode_packetization(
543        &mut self,
544        _job: J2kPacketizationEncodeJob<'_>,
545    ) -> core::result::Result<Option<Vec<u8>>, &'static str> {
546        Ok(None)
547    }
548}
549
550impl J2kEncodeStageAccelerator for CpuOnlyJ2kEncodeStageAccelerator {
551    fn prefer_parallel_cpu_code_block_fallback(&self) -> bool {
552        true
553    }
554
555    fn prefer_parallel_cpu_tile_encode(&self) -> bool {
556        true
557    }
558}
559
560/// Adapter classic J2K batched code-block decode job for one sub-band.
561#[derive(Debug, Clone, Copy)]
562pub struct J2kCodeBlockBatchJob<'a> {
563    /// X offset within the target sub-band coefficient buffer.
564    pub output_x: u32,
565    /// Y offset within the target sub-band coefficient buffer.
566    pub output_y: u32,
567    /// The actual code-block decode parameters.
568    pub code_block: J2kCodeBlockDecodeJob<'a>,
569}
570
571/// Adapter classic J2K batched sub-band decode request for backend experimentation.
572#[derive(Debug, Clone, Copy)]
573pub struct J2kSubBandDecodeJob<'a> {
574    /// Sub-band width in samples.
575    pub width: u32,
576    /// Sub-band height in samples.
577    pub height: u32,
578    /// Code blocks to decode into this sub-band.
579    pub jobs: &'a [J2kCodeBlockBatchJob<'a>],
580}
581
582/// Adapter integer rectangle for backend experimentation.
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584pub struct J2kRect {
585    /// Inclusive minimum x coordinate.
586    pub x0: u32,
587    /// Inclusive minimum y coordinate.
588    pub y0: u32,
589    /// Exclusive maximum x coordinate.
590    pub x1: u32,
591    /// Exclusive maximum y coordinate.
592    pub y1: u32,
593}
594
595impl J2kRect {
596    /// Rectangle width in samples.
597    pub fn width(self) -> u32 {
598        self.x1.saturating_sub(self.x0)
599    }
600
601    /// Rectangle height in samples.
602    pub fn height(self) -> u32 {
603        self.y1.saturating_sub(self.y0)
604    }
605}
606
607/// Adapter wavelet transform selector for backend experimentation.
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum J2kWaveletTransform {
610    /// Reversible 5/3 transform.
611    Reversible53,
612    /// Irreversible 9/7 transform.
613    Irreversible97,
614}
615
616/// Adapter single sub-band payload for backend experimentation.
617#[derive(Debug, Clone, Copy)]
618pub struct J2kIdwtBand<'a> {
619    /// Rect covered by this band.
620    pub rect: J2kRect,
621    /// Band coefficients in row-major order.
622    pub coefficients: &'a [f32],
623}
624
625/// Adapter single-decomposition IDWT job for backend experimentation.
626#[derive(Debug, Clone, Copy)]
627pub struct J2kSingleDecompositionIdwtJob<'a> {
628    /// Output rect of the decomposition level.
629    pub rect: J2kRect,
630    /// Transform to apply.
631    pub transform: J2kWaveletTransform,
632    /// LL band input.
633    pub ll: J2kIdwtBand<'a>,
634    /// HL band input.
635    pub hl: J2kIdwtBand<'a>,
636    /// LH band input.
637    pub lh: J2kIdwtBand<'a>,
638    /// HH band input.
639    pub hh: J2kIdwtBand<'a>,
640}
641
642/// Adapter inverse MCT job for backend experimentation.
643#[derive(Debug)]
644pub struct J2kInverseMctJob<'a> {
645    /// Transform to apply.
646    pub transform: J2kWaveletTransform,
647    /// First component plane, updated in place.
648    pub plane0: &'a mut [f32],
649    /// Second component plane, updated in place.
650    pub plane1: &'a mut [f32],
651    /// Third component plane, updated in place.
652    pub plane2: &'a mut [f32],
653    /// Constant sign-shift addend applied to the first plane after inverse MCT.
654    pub addend0: f32,
655    /// Constant sign-shift addend applied to the second plane after inverse MCT.
656    pub addend1: f32,
657    /// Constant sign-shift addend applied to the third plane after inverse MCT.
658    pub addend2: f32,
659}
660
661/// Adapter component-store job for backend experimentation.
662#[derive(Debug)]
663pub struct J2kStoreComponentJob<'a> {
664    /// Source IDWT coefficients in row-major order.
665    pub input: &'a [f32],
666    /// Source row width.
667    pub input_width: u32,
668    /// Source x offset to begin copying from.
669    pub source_x: u32,
670    /// Source y offset to begin copying from.
671    pub source_y: u32,
672    /// Number of samples to copy per row.
673    pub copy_width: u32,
674    /// Number of rows to copy.
675    pub copy_height: u32,
676    /// Destination component plane in row-major order.
677    pub output: &'a mut [f32],
678    /// Destination row width.
679    pub output_width: u32,
680    /// Destination x offset to begin writing at.
681    pub output_x: u32,
682    /// Destination y offset to begin writing at.
683    pub output_y: u32,
684    /// Constant value added to every copied sample.
685    pub addend: f32,
686}
687
688/// Adapter HTJ2K code-block decode hook for backend experimentation.
689pub trait HtCodeBlockDecoder {
690    /// Optionally decode a full classic J2K sub-band in one batch.
691    ///
692    /// Implementations should return `Ok(true)` if they handled the request and
693    /// wrote the decoded coefficients into `output`. Returning `Ok(false)`
694    /// falls back to per-code-block decode via `decode_j2k_code_block`.
695    fn decode_j2k_sub_band(
696        &mut self,
697        _job: J2kSubBandDecodeJob<'_>,
698        _output: &mut [f32],
699    ) -> Result<bool> {
700        Ok(false)
701    }
702
703    /// Optionally decode one classic J2K code block.
704    ///
705    /// Implementations should return `Ok(true)` if they handled the request
706    /// and wrote the decoded coefficients into `output`. Returning `Ok(false)`
707    /// falls back to the scalar bitplane decoder.
708    fn decode_j2k_code_block(
709        &mut self,
710        _job: J2kCodeBlockDecodeJob<'_>,
711        _output: &mut [f32],
712    ) -> Result<bool> {
713        Ok(false)
714    }
715
716    /// Optionally decode a full HTJ2K sub-band in one batch.
717    ///
718    /// Implementations should return `Ok(true)` if they handled the request and
719    /// wrote the decoded coefficients into `output`. Returning `Ok(false)`
720    /// falls back to per-code-block decode via `decode_code_block`.
721    fn decode_sub_band(
722        &mut self,
723        _job: HtSubBandDecodeJob<'_>,
724        _output: &mut [f32],
725    ) -> Result<bool> {
726        Ok(false)
727    }
728
729    /// Optionally decode one single-decomposition IDWT level on a backend.
730    ///
731    /// Implementations should return `Ok(true)` if they handled the request
732    /// and wrote the transformed coefficients into `output`. Returning
733    /// `Ok(false)` falls back to the scalar/SIMD CPU IDWT path.
734    fn decode_single_decomposition_idwt(
735        &mut self,
736        _job: J2kSingleDecompositionIdwtJob<'_>,
737        _output: &mut [f32],
738    ) -> Result<bool> {
739        Ok(false)
740    }
741
742    /// Optionally apply inverse MCT on a backend.
743    ///
744    /// Implementations should return `Ok(true)` if they handled the request
745    /// and updated the component planes in place. Returning `Ok(false)` falls
746    /// back to the scalar/SIMD CPU MCT path.
747    fn decode_inverse_mct(&mut self, _job: J2kInverseMctJob<'_>) -> Result<bool> {
748        Ok(false)
749    }
750
751    /// Optionally store one component plane on a backend.
752    ///
753    /// Implementations should return `Ok(true)` if they handled the request
754    /// and updated the destination plane in place. Returning `Ok(false)` falls
755    /// back to the CPU store path.
756    fn decode_store_component(&mut self, _job: J2kStoreComponentJob<'_>) -> Result<bool> {
757        Ok(false)
758    }
759
760    /// Decode one HTJ2K code block into `output`, writing `job.width` samples per row.
761    fn decode_code_block(
762        &mut self,
763        job: HtCodeBlockDecodeJob<'_>,
764        output: &mut [f32],
765    ) -> Result<()>;
766}
767
768fn internal_j2k_sub_band_type(sub_band_type: J2kSubBandType) -> j2c::build::SubBandType {
769    match sub_band_type {
770        J2kSubBandType::LowLow => j2c::build::SubBandType::LowLow,
771        J2kSubBandType::HighLow => j2c::build::SubBandType::HighLow,
772        J2kSubBandType::LowHigh => j2c::build::SubBandType::LowHigh,
773        J2kSubBandType::HighHigh => j2c::build::SubBandType::HighHigh,
774    }
775}
776
777fn internal_j2k_code_block_style(style: J2kCodeBlockStyle) -> j2c::codestream::CodeBlockStyle {
778    j2c::codestream::CodeBlockStyle {
779        selective_arithmetic_coding_bypass: style.selective_arithmetic_coding_bypass,
780        reset_context_probabilities: style.reset_context_probabilities,
781        termination_on_each_pass: style.termination_on_each_pass,
782        vertically_causal_context: style.vertically_causal_context,
783        segmentation_symbols: style.segmentation_symbols,
784        high_throughput_block_coding: false,
785    }
786}
787
788pub(crate) fn add_roi_shift_to_bitplanes(
789    bitplanes: u8,
790    roi_shift: u8,
791    max_bitplanes: u8,
792) -> Result<u8> {
793    let Some(coded_bitplanes) = bitplanes.checked_add(roi_shift) else {
794        bail!(DecodingError::TooManyBitplanes);
795    };
796    if coded_bitplanes > max_bitplanes {
797        bail!(DecodingError::TooManyBitplanes);
798    }
799    Ok(coded_bitplanes)
800}
801
802pub(crate) fn apply_roi_maxshift_inverse_i32(coefficient: i32, roi_shift: u8) -> i32 {
803    if roi_shift == 0 || coefficient == 0 {
804        return coefficient;
805    }
806
807    let magnitude = i64::from(coefficient).abs();
808    let threshold = 1_i64.checked_shl(roi_shift as u32).unwrap_or(i64::MAX);
809    if magnitude < threshold {
810        return coefficient;
811    }
812
813    let shifted = magnitude >> roi_shift;
814    let shifted = shifted.min(i64::from(i32::MAX)) as i32;
815    if coefficient < 0 {
816        -shifted
817    } else {
818        shifted
819    }
820}
821
822/// Adapter scalar classic J2K encoder helper for backend experimentation.
823pub fn encode_j2k_code_block_scalar_with_style(
824    coefficients: &[i32],
825    width: u32,
826    height: u32,
827    sub_band_type: J2kSubBandType,
828    total_bitplanes: u8,
829    style: J2kCodeBlockStyle,
830) -> core::result::Result<EncodedJ2kCodeBlock, &'static str> {
831    let encoded = j2c::bitplane_encode::encode_code_block_segments_with_style(
832        coefficients,
833        width,
834        height,
835        internal_j2k_sub_band_type(sub_band_type),
836        total_bitplanes,
837        &internal_j2k_code_block_style(style),
838    );
839    let segments = encoded
840        .segments
841        .into_iter()
842        .map(|segment| J2kCodeBlockSegment {
843            data_offset: segment.data_offset,
844            data_length: segment.data_length,
845            start_coding_pass: segment.start_coding_pass,
846            end_coding_pass: segment.end_coding_pass,
847            use_arithmetic: segment.use_arithmetic,
848        })
849        .collect();
850
851    Ok(EncodedJ2kCodeBlock {
852        data: encoded.data,
853        segments,
854        number_of_coding_passes: encoded.num_coding_passes,
855        missing_bit_planes: encoded.num_zero_bitplanes,
856    })
857}
858
859/// Adapter scalar Classic Tier-1 compact token packer for backend experimentation.
860///
861/// The token format matches the Metal Classic Tier-1 token-emitter contract:
862/// arithmetic segments are 6-bit `(context_label, bit)` MQ tokens, while raw
863/// bypass segments are one bit per raw bypass event.
864pub fn pack_j2k_code_block_scalar_from_tier1_tokens(
865    token_bytes: &[u8],
866    token_segments: &[J2kTier1TokenSegment],
867    number_of_coding_passes: u8,
868    missing_bit_planes: u8,
869) -> core::result::Result<EncodedJ2kCodeBlock, &'static str> {
870    let internal_segments = token_segments
871        .iter()
872        .map(|segment| j2c::bitplane_encode::ClassicTier1TokenSegment {
873            token_bit_offset: segment.token_bit_offset,
874            token_bit_count: segment.token_bit_count,
875            start_coding_pass: segment.start_coding_pass,
876            end_coding_pass: segment.end_coding_pass,
877            use_arithmetic: segment.use_arithmetic,
878        })
879        .collect::<Vec<_>>();
880    let encoded = j2c::bitplane_encode::pack_classic_selective_bypass_tier1_tokens(
881        token_bytes,
882        &internal_segments,
883        number_of_coding_passes,
884        missing_bit_planes,
885    )?;
886    let segments = encoded
887        .segments
888        .into_iter()
889        .map(|segment| J2kCodeBlockSegment {
890            data_offset: segment.data_offset,
891            data_length: segment.data_length,
892            start_coding_pass: segment.start_coding_pass,
893            end_coding_pass: segment.end_coding_pass,
894            use_arithmetic: segment.use_arithmetic,
895        })
896        .collect();
897
898    Ok(EncodedJ2kCodeBlock {
899        data: encoded.data,
900        segments,
901        number_of_coding_passes: encoded.num_coding_passes,
902        missing_bit_planes: encoded.num_zero_bitplanes,
903    })
904}
905
906/// Adapter scalar HTJ2K cleanup-only encoder helper for backend experimentation.
907pub fn encode_ht_code_block_scalar(
908    coefficients: &[i32],
909    width: u32,
910    height: u32,
911    total_bitplanes: u8,
912) -> core::result::Result<EncodedHtJ2kCodeBlock, &'static str> {
913    let encoded =
914        j2c::ht_block_encode::encode_code_block(coefficients, width, height, total_bitplanes)?;
915    Ok(EncodedHtJ2kCodeBlock {
916        data: encoded.data,
917        cleanup_length: encoded.ht_cleanup_length,
918        refinement_length: encoded.ht_refinement_length,
919        num_coding_passes: encoded.num_coding_passes,
920        num_zero_bitplanes: encoded.num_zero_bitplanes,
921    })
922}
923
924/// Adapter HTJ2K cleanup-encode distribution helper for benchmark tuning.
925pub fn collect_ht_cleanup_encode_distribution(
926    coefficients: &[i32],
927    width: u32,
928    height: u32,
929    total_bitplanes: u8,
930) -> core::result::Result<HtCleanupEncodeDistribution, &'static str> {
931    j2c::ht_block_encode::collect_encode_distribution(coefficients, width, height, total_bitplanes)
932}
933
934/// Adapter scalar forward 5/3 DWT reference for CUDA stage parity.
935///
936/// Runs the native CPU reversible 5/3 forward DWT on `samples` and returns
937/// the decomposed subbands packed into the public `J2kForwardDwt53Output`
938/// type.  The returned layout matches what the encoder feeds to Tier-1.
939pub fn forward_dwt53_reference(
940    samples: &[f32],
941    width: u32,
942    height: u32,
943    num_levels: u8,
944) -> J2kForwardDwt53Output {
945    let decomp = j2c::fdwt::forward_dwt(samples, width, height, num_levels, true);
946    let levels = decomp
947        .levels
948        .into_iter()
949        .map(|lvl| J2kForwardDwt53Level {
950            hl: lvl.hl,
951            lh: lvl.lh,
952            hh: lvl.hh,
953            width: lvl.low_width + lvl.high_width,
954            height: lvl.low_height + lvl.high_height,
955            low_width: lvl.low_width,
956            low_height: lvl.low_height,
957            high_width: lvl.high_width,
958            high_height: lvl.high_height,
959        })
960        .collect();
961    J2kForwardDwt53Output {
962        ll: decomp.ll,
963        ll_width: decomp.ll_width,
964        ll_height: decomp.ll_height,
965        levels,
966    }
967}
968
969/// Adapter scalar forward 9/7 DWT reference for Metal/CUDA stage parity.
970///
971/// Runs the native CPU irreversible 9/7 forward DWT on `samples` and returns
972/// the decomposed subbands packed into the public `J2kForwardDwt97Output`
973/// type. The returned layout matches what the encoder feeds to Tier-1.
974pub fn forward_dwt97_reference(
975    samples: &[f32],
976    width: u32,
977    height: u32,
978    num_levels: u8,
979) -> J2kForwardDwt97Output {
980    let decomp = j2c::fdwt::forward_dwt(samples, width, height, num_levels, false);
981    let levels = decomp
982        .levels
983        .into_iter()
984        .map(|lvl| J2kForwardDwt97Level {
985            hl: lvl.hl,
986            lh: lvl.lh,
987            hh: lvl.hh,
988            width: lvl.low_width + lvl.high_width,
989            height: lvl.low_height + lvl.high_height,
990            low_width: lvl.low_width,
991            low_height: lvl.low_height,
992            high_width: lvl.high_width,
993            high_height: lvl.high_height,
994        })
995        .collect();
996    J2kForwardDwt97Output {
997        ll: decomp.ll,
998        ll_width: decomp.ll_width,
999        ll_height: decomp.ll_height,
1000        levels,
1001    }
1002}
1003
1004/// Adapter scalar forward RCT reference for CUDA stage parity.
1005///
1006/// Applies the native CPU forward Reversible Color Transform to three
1007/// component planes supplied as owned `Vec<f32>` arrays.  The transform is
1008/// applied in place and the mutated planes are returned, so callers do not
1009/// need to pass a mutable slice.
1010pub fn forward_rct_reference(mut planes: Vec<Vec<f32>>) -> Vec<Vec<f32>> {
1011    j2c::forward_mct::forward_rct(&mut planes);
1012    planes
1013}
1014
1015/// Adapter scalar forward ICT reference for Metal/CUDA stage parity.
1016///
1017/// Applies the native CPU forward Irreversible Color Transform to three
1018/// component planes supplied as owned `Vec<f32>` arrays. The transform is
1019/// applied in place and the mutated planes are returned.
1020pub fn forward_ict_reference(mut planes: Vec<Vec<f32>>) -> Vec<Vec<f32>> {
1021    j2c::forward_mct::forward_ict(&mut planes);
1022    planes
1023}
1024
1025/// Adapter scalar sub-band quantization reference for backend stage parity.
1026pub fn quantize_subband_reference(
1027    coefficients: &[f32],
1028    step_exponent: u16,
1029    step_mantissa: u16,
1030    range_bits: u8,
1031    reversible: bool,
1032) -> Vec<i32> {
1033    let step = j2c::quantize::QuantStepSize {
1034        exponent: step_exponent,
1035        mantissa: step_mantissa,
1036    };
1037    j2c::quantize::quantize_subband(coefficients, &step, range_bits, reversible)
1038}
1039
1040/// Adapter scalar reversible sub-band quantization reference for CUDA stage parity.
1041///
1042/// Quantizes `coefficients` using the reversible (lossless) integer path of
1043/// the native CPU quantizer.  `step_exponent` and `step_mantissa` encode the
1044/// JPEG 2000 `QuantStepSize` for the sub-band; `range_bits` is the nominal
1045/// bit depth for the sub-band.  When `reversible` is `true` the step-size
1046/// parameters are ignored and each coefficient is rounded to the nearest
1047/// integer.
1048pub fn quantize_reversible_reference(
1049    coefficients: &[f32],
1050    step_exponent: u16,
1051    step_mantissa: u16,
1052    range_bits: u8,
1053    reversible: bool,
1054) -> Vec<i32> {
1055    quantize_subband_reference(
1056        coefficients,
1057        step_exponent,
1058        step_mantissa,
1059        range_bits,
1060        reversible,
1061    )
1062}
1063
1064/// Adapter scalar pixel deinterleave/level-shift reference for CUDA stage parity.
1065///
1066/// Converts interleaved pixel bytes to per-component f32 planes with the
1067/// same level-shift logic as the native CPU encode path.  The result is one
1068/// `Vec<f32>` per component, each of length `num_pixels`.
1069pub fn deinterleave_reference(
1070    pixels: &[u8],
1071    num_pixels: usize,
1072    num_components: u8,
1073    bit_depth: u8,
1074    signed: bool,
1075) -> Vec<Vec<f32>> {
1076    j2c::encode::deinterleave_to_f32(pixels, num_pixels, num_components, bit_depth, signed)
1077}
1078
1079/// Adapter scalar Tier-2 packetization helper for backend experimentation.
1080pub fn encode_j2k_packetization_scalar(
1081    job: J2kPacketizationEncodeJob<'_>,
1082) -> core::result::Result<Vec<u8>, &'static str> {
1083    let mut resolutions = job
1084        .resolutions
1085        .iter()
1086        .map(|resolution| j2c::packet_encode::ResolutionPacket {
1087            subbands: resolution
1088                .subbands
1089                .iter()
1090                .map(|subband| j2c::packet_encode::SubbandPrecinct {
1091                    code_blocks: subband
1092                        .code_blocks
1093                        .iter()
1094                        .map(|code_block| j2c::packet_encode::CodeBlockPacketData {
1095                            data: code_block.data.to_vec(),
1096                            ht_cleanup_length: code_block.ht_cleanup_length,
1097                            ht_refinement_length: code_block.ht_refinement_length,
1098                            num_coding_passes: code_block.num_coding_passes,
1099                            classic_segment_lengths: Vec::new(),
1100                            num_zero_bitplanes: code_block.num_zero_bitplanes,
1101                            previously_included: code_block.previously_included,
1102                            l_block: code_block.l_block,
1103                            block_coding_mode: match code_block.block_coding_mode {
1104                                J2kPacketizationBlockCodingMode::Classic => {
1105                                    j2c::codestream_write::BlockCodingMode::Classic
1106                                }
1107                                J2kPacketizationBlockCodingMode::HighThroughput => {
1108                                    j2c::codestream_write::BlockCodingMode::HighThroughput
1109                                }
1110                            },
1111                        })
1112                        .collect(),
1113                    num_cbs_x: subband.num_cbs_x,
1114                    num_cbs_y: subband.num_cbs_y,
1115                })
1116                .collect(),
1117        })
1118        .collect::<Vec<_>>();
1119
1120    let descriptors = job
1121        .packet_descriptors
1122        .iter()
1123        .map(|descriptor| j2c::packet_encode::PacketDescriptor {
1124            packet_index: descriptor.packet_index,
1125            state_index: descriptor.state_index,
1126            layer: descriptor.layer,
1127            resolution: descriptor.resolution,
1128            component: descriptor.component,
1129            precinct: descriptor.precinct,
1130        })
1131        .collect::<Vec<_>>();
1132
1133    j2c::packet_encode::validate_ht_segment_lengths(&resolutions)?;
1134
1135    if descriptors.is_empty() {
1136        Ok(j2c::packet_encode::form_tile_bitstream_for_progression(
1137            &mut resolutions,
1138            job.num_layers,
1139            job.num_components,
1140            job.progression_order,
1141        ))
1142    } else {
1143        j2c::packet_encode::form_tile_bitstream_with_descriptors(&mut resolutions, &descriptors)
1144    }
1145}
1146
1147/// Adapter scalar classic J2K decoder helper for backend experimentation.
1148pub fn decode_j2k_code_block_scalar(
1149    job: J2kCodeBlockDecodeJob<'_>,
1150    output: &mut [f32],
1151) -> Result<()> {
1152    let mut workspace = J2kCodeBlockDecodeWorkspace::default();
1153    decode_j2k_code_block_scalar_with_workspace(job, output, &mut workspace)
1154}
1155
1156/// Reusable scratch for scalar classic J2K code-block decoding.
1157#[derive(Default)]
1158pub struct J2kCodeBlockDecodeWorkspace {
1159    bit_plane_decode_context: j2c::bitplane::BitPlaneDecodeContext,
1160}
1161
1162/// Adapter scalar classic J2K decoder helper that reuses caller-provided scratch.
1163pub fn decode_j2k_code_block_scalar_with_workspace(
1164    job: J2kCodeBlockDecodeJob<'_>,
1165    output: &mut [f32],
1166    workspace: &mut J2kCodeBlockDecodeWorkspace,
1167) -> Result<()> {
1168    let required_len = if job.height == 0 {
1169        0
1170    } else {
1171        job.output_stride
1172            .checked_mul(job.height as usize - 1)
1173            .and_then(|prefix| prefix.checked_add(job.width as usize))
1174            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1175    };
1176    if output.len() < required_len {
1177        bail!(DecodingError::CodeBlockDecodeFailure);
1178    }
1179
1180    let style = internal_j2k_code_block_style(job.style);
1181    let sub_band_type = internal_j2k_sub_band_type(job.sub_band_type);
1182    let code_block_stride =
1183        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1184    let coded_bitplanes = add_roi_shift_to_bitplanes(
1185        job.total_bitplanes,
1186        job.roi_shift,
1187        MAX_CLASSIC_DECODE_BITPLANES,
1188    )?;
1189
1190    j2c::bitplane::decode_code_block_segments_validated(
1191        job.data,
1192        job.segments,
1193        job.width,
1194        job.height,
1195        job.missing_bit_planes,
1196        job.number_of_coding_passes,
1197        coded_bitplanes,
1198        sub_band_type,
1199        &style,
1200        job.strict,
1201        &mut workspace.bit_plane_decode_context,
1202    )?;
1203
1204    for (row_idx, coeff_row) in workspace
1205        .bit_plane_decode_context
1206        .coefficient_rows()
1207        .enumerate()
1208        .take(job.height as usize)
1209    {
1210        let row_start = row_idx * job.output_stride;
1211        let output_row = &mut output[row_start..row_start + code_block_stride];
1212        for (coefficient, sample) in coeff_row.iter().zip(output_row.iter_mut()) {
1213            let coefficient = apply_roi_maxshift_inverse_i32(coefficient.get(), job.roi_shift);
1214            *sample = coefficient as f32 * job.dequantization_step;
1215        }
1216    }
1217
1218    Ok(())
1219}
1220
1221/// Adapter scalar classic J2K pass timings for backend experimentation.
1222#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
1223pub struct J2kCodeBlockDecodeProfile {
1224    /// Significance propagation pass elapsed time in microseconds.
1225    pub sigprop_us: u128,
1226    /// Magnitude refinement pass elapsed time in microseconds.
1227    pub magref_us: u128,
1228    /// Cleanup pass elapsed time in microseconds.
1229    pub cleanup_us: u128,
1230    /// Raw bypass pass elapsed time in microseconds.
1231    pub bypass_us: u128,
1232    /// Coefficient output conversion elapsed time in microseconds.
1233    pub output_convert_us: u128,
1234}
1235
1236impl J2kCodeBlockDecodeProfile {
1237    fn add_native_stats(&mut self, stats: j2c::bitplane::J2kBlockDecodeStats) {
1238        self.sigprop_us += stats.sigprop_us;
1239        self.magref_us += stats.magref_us;
1240        self.cleanup_us += stats.cleanup_us;
1241        self.bypass_us += stats.bypass_us;
1242    }
1243}
1244
1245/// Adapter scalar classic J2K decoder helper that records pass timings.
1246pub fn decode_j2k_code_block_scalar_profiled(
1247    job: J2kCodeBlockDecodeJob<'_>,
1248    output: &mut [f32],
1249    profile: &mut J2kCodeBlockDecodeProfile,
1250) -> Result<()> {
1251    let mut workspace = J2kCodeBlockDecodeWorkspace::default();
1252    decode_j2k_code_block_scalar_with_workspace_profiled(job, output, &mut workspace, profile)
1253}
1254
1255/// Adapter scalar classic J2K decoder helper that records pass timings and reuses scratch.
1256pub fn decode_j2k_code_block_scalar_with_workspace_profiled(
1257    job: J2kCodeBlockDecodeJob<'_>,
1258    output: &mut [f32],
1259    workspace: &mut J2kCodeBlockDecodeWorkspace,
1260    profile: &mut J2kCodeBlockDecodeProfile,
1261) -> Result<()> {
1262    let required_len = if job.height == 0 {
1263        0
1264    } else {
1265        job.output_stride
1266            .checked_mul(job.height as usize - 1)
1267            .and_then(|prefix| prefix.checked_add(job.width as usize))
1268            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1269    };
1270    if output.len() < required_len {
1271        bail!(DecodingError::CodeBlockDecodeFailure);
1272    }
1273
1274    let style = internal_j2k_code_block_style(job.style);
1275    let sub_band_type = internal_j2k_sub_band_type(job.sub_band_type);
1276    let code_block_stride =
1277        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1278    let coded_bitplanes = add_roi_shift_to_bitplanes(
1279        job.total_bitplanes,
1280        job.roi_shift,
1281        MAX_CLASSIC_DECODE_BITPLANES,
1282    )?;
1283    let mut stats = j2c::bitplane::J2kBlockDecodeStats::default();
1284
1285    j2c::bitplane::decode_code_block_segments_validated_profiled(
1286        job.data,
1287        job.segments,
1288        job.width,
1289        job.height,
1290        job.missing_bit_planes,
1291        job.number_of_coding_passes,
1292        coded_bitplanes,
1293        sub_band_type,
1294        &style,
1295        job.strict,
1296        &mut workspace.bit_plane_decode_context,
1297        &mut stats,
1298        true,
1299    )?;
1300    profile.add_native_stats(stats);
1301
1302    let output_convert_started = profile::profile_now(true);
1303    for (row_idx, coeff_row) in workspace
1304        .bit_plane_decode_context
1305        .coefficient_rows()
1306        .enumerate()
1307        .take(job.height as usize)
1308    {
1309        let row_start = row_idx * job.output_stride;
1310        let output_row = &mut output[row_start..row_start + code_block_stride];
1311        for (coefficient, sample) in coeff_row.iter().zip(output_row.iter_mut()) {
1312            let coefficient = apply_roi_maxshift_inverse_i32(coefficient.get(), job.roi_shift);
1313            *sample = coefficient as f32 * job.dequantization_step;
1314        }
1315    }
1316    profile.output_convert_us += profile::elapsed_us(output_convert_started);
1317
1318    Ok(())
1319}
1320
1321/// Adapter scalar classic J2K batched decoder helper for backend experimentation.
1322pub fn decode_j2k_sub_band_scalar(job: J2kSubBandDecodeJob<'_>, output: &mut [f32]) -> Result<()> {
1323    let required_len = if job.height == 0 {
1324        0
1325    } else {
1326        usize::try_from(job.width)
1327            .ok()
1328            .and_then(|width| width.checked_mul(job.height as usize))
1329            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1330    };
1331    if output.len() < required_len {
1332        bail!(DecodingError::CodeBlockDecodeFailure);
1333    }
1334
1335    let sub_band_width =
1336        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1337
1338    for batch_job in job.jobs {
1339        let code_block = batch_job.code_block;
1340        if code_block.output_stride != sub_band_width {
1341            bail!(DecodingError::CodeBlockDecodeFailure);
1342        }
1343        if batch_job
1344            .output_x
1345            .checked_add(code_block.width)
1346            .is_none_or(|x1| x1 > job.width)
1347            || batch_job
1348                .output_y
1349                .checked_add(code_block.height)
1350                .is_none_or(|y1| y1 > job.height)
1351        {
1352            bail!(DecodingError::CodeBlockDecodeFailure);
1353        }
1354
1355        let base_idx = usize::try_from(batch_job.output_y)
1356            .ok()
1357            .and_then(|y| y.checked_mul(sub_band_width))
1358            .and_then(|row| row.checked_add(batch_job.output_x as usize))
1359            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1360        let block_output_len = if code_block.height == 0 {
1361            0
1362        } else {
1363            code_block
1364                .output_stride
1365                .checked_mul(code_block.height as usize - 1)
1366                .and_then(|prefix| prefix.checked_add(code_block.width as usize))
1367                .ok_or(DecodingError::CodeBlockDecodeFailure)?
1368        };
1369        let end_idx = base_idx
1370            .checked_add(block_output_len)
1371            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1372        if end_idx > output.len() {
1373            bail!(DecodingError::CodeBlockDecodeFailure);
1374        }
1375
1376        decode_j2k_code_block_scalar(code_block, &mut output[base_idx..end_idx])?;
1377    }
1378
1379    Ok(())
1380}
1381
1382/// Adapter scalar HTJ2K decoder helper for backend experimentation.
1383pub fn decode_ht_code_block_scalar(
1384    job: HtCodeBlockDecodeJob<'_>,
1385    output: &mut [f32],
1386) -> Result<()> {
1387    decode_ht_code_block_scalar_for_phase::<{ j2c::ht_block_decode::PHASE_LIMIT_MAGREF }>(
1388        job, output,
1389    )
1390}
1391
1392/// Adapter scalar HTJ2K decoder helper that stops after the selected phase.
1393pub fn decode_ht_code_block_scalar_until_phase(
1394    job: HtCodeBlockDecodeJob<'_>,
1395    output: &mut [f32],
1396    phase_limit: HtCodeBlockDecodePhaseLimit,
1397) -> Result<()> {
1398    match phase_limit {
1399        HtCodeBlockDecodePhaseLimit::Cleanup => decode_ht_code_block_scalar_for_phase::<
1400            { j2c::ht_block_decode::PHASE_LIMIT_CLEANUP },
1401        >(job, output),
1402        HtCodeBlockDecodePhaseLimit::SignificancePropagation => {
1403            decode_ht_code_block_scalar_for_phase::<{ j2c::ht_block_decode::PHASE_LIMIT_SIGPROP }>(
1404                job, output,
1405            )
1406        }
1407        HtCodeBlockDecodePhaseLimit::MagnitudeRefinement => {
1408            decode_ht_code_block_scalar_for_phase::<{ j2c::ht_block_decode::PHASE_LIMIT_MAGREF }>(
1409                job, output,
1410            )
1411        }
1412    }
1413}
1414
1415/// Adapter reusable scalar HTJ2K decode workspace for backend experimentation.
1416#[derive(Default)]
1417pub struct HtCodeBlockDecodeWorkspace {
1418    coefficients: Vec<u32>,
1419    scratch: j2c::ht_block_decode::HtBlockDecodeScratch,
1420}
1421
1422impl HtCodeBlockDecodeWorkspace {
1423    /// Current coefficient buffer capacity retained by this workspace.
1424    pub fn coefficient_capacity(&self) -> usize {
1425        self.coefficients.capacity()
1426    }
1427}
1428
1429/// Adapter scalar HTJ2K phase timings for backend experimentation.
1430#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
1431pub struct HtCodeBlockDecodeProfile {
1432    /// Number of decoded HT code blocks.
1433    pub blocks: u128,
1434    /// Number of decoded HT code blocks with refinement data.
1435    pub refinement_blocks: u128,
1436    /// Total cleanup segment bytes consumed by decoded HT code blocks.
1437    pub cleanup_bytes: u128,
1438    /// Total refinement segment bytes consumed by decoded HT code blocks.
1439    pub refinement_bytes: u128,
1440    /// Cleanup phase elapsed time in microseconds.
1441    pub cleanup_us: u128,
1442    /// Magnitude/sign phase elapsed time in microseconds.
1443    pub mag_sgn_us: u128,
1444    /// Sigma build phase elapsed time in microseconds.
1445    pub sigma_us: u128,
1446    /// Significance propagation phase elapsed time in microseconds.
1447    pub sigprop_us: u128,
1448    /// Magnitude refinement phase elapsed time in microseconds.
1449    pub magref_us: u128,
1450}
1451
1452impl HtCodeBlockDecodeProfile {
1453    fn add_native_stats(&mut self, stats: j2c::ht_block_decode::HtBlockDecodeStats) {
1454        self.blocks += stats.blocks;
1455        self.refinement_blocks += stats.refinement_blocks;
1456        self.cleanup_bytes += stats.cleanup_bytes;
1457        self.refinement_bytes += stats.refinement_bytes;
1458        self.cleanup_us += stats.ht_cleanup_us;
1459        self.mag_sgn_us += stats.ht_mag_sgn_us;
1460        self.sigma_us += stats.ht_sigma_us;
1461        self.sigprop_us += stats.ht_sigprop_us;
1462        self.magref_us += stats.ht_magref_us;
1463    }
1464}
1465
1466/// Adapter scalar HTJ2K decoder helper that reuses caller-owned scratch buffers.
1467pub fn decode_ht_code_block_scalar_with_workspace(
1468    job: HtCodeBlockDecodeJob<'_>,
1469    output: &mut [f32],
1470    workspace: &mut HtCodeBlockDecodeWorkspace,
1471) -> Result<()> {
1472    decode_ht_code_block_scalar_for_phase_with_workspace::<
1473        { j2c::ht_block_decode::PHASE_LIMIT_MAGREF },
1474    >(job, output, workspace)
1475}
1476
1477/// Adapter scalar HTJ2K decoder helper that reuses scratch and records phase timings.
1478pub fn decode_ht_code_block_scalar_with_workspace_profiled(
1479    job: HtCodeBlockDecodeJob<'_>,
1480    output: &mut [f32],
1481    workspace: &mut HtCodeBlockDecodeWorkspace,
1482    profile: &mut HtCodeBlockDecodeProfile,
1483) -> Result<()> {
1484    decode_ht_code_block_scalar_for_phase_with_workspace_profiled::<
1485        { j2c::ht_block_decode::PHASE_LIMIT_MAGREF },
1486    >(job, output, workspace, profile)
1487}
1488
1489fn decode_ht_code_block_scalar_for_phase<const PHASE_LIMIT: u8>(
1490    job: HtCodeBlockDecodeJob<'_>,
1491    output: &mut [f32],
1492) -> Result<()> {
1493    let mut workspace = HtCodeBlockDecodeWorkspace::default();
1494    decode_ht_code_block_scalar_for_phase_with_workspace::<PHASE_LIMIT>(job, output, &mut workspace)
1495}
1496
1497fn decode_ht_code_block_scalar_for_phase_with_workspace<const PHASE_LIMIT: u8>(
1498    job: HtCodeBlockDecodeJob<'_>,
1499    output: &mut [f32],
1500    workspace: &mut HtCodeBlockDecodeWorkspace,
1501) -> Result<()> {
1502    let required_len = if job.height == 0 {
1503        0
1504    } else {
1505        job.output_stride
1506            .checked_mul(job.height as usize - 1)
1507            .and_then(|prefix| prefix.checked_add(job.width as usize))
1508            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1509    };
1510    if output.len() < required_len {
1511        bail!(DecodingError::CodeBlockDecodeFailure);
1512    }
1513    let code_block_stride =
1514        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1515    let code_block_len = code_block_stride
1516        .checked_mul(job.height as usize)
1517        .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1518
1519    let segments = j2c::ht_block_decode::HtCodeBlockSegments::from_combined_payload(
1520        job.data,
1521        job.cleanup_length,
1522        job.refinement_length,
1523    )?;
1524    let coded_bitplanes = add_roi_shift_to_bitplanes(job.num_bitplanes, job.roi_shift, 31)?;
1525    workspace.coefficients.clear();
1526    workspace.coefficients.resize(code_block_len, 0);
1527    j2c::ht_block_decode::decode_segments_validated_with_scratch_for_phase::<PHASE_LIMIT>(
1528        &segments,
1529        job.missing_bit_planes,
1530        coded_bitplanes,
1531        job.number_of_coding_passes,
1532        job.stripe_causal,
1533        job.strict,
1534        &mut workspace.coefficients,
1535        job.width,
1536        job.height,
1537        job.width,
1538        &mut workspace.scratch,
1539        None,
1540        false,
1541    )?;
1542
1543    for (row_idx, coeff_row) in workspace
1544        .coefficients
1545        .chunks_exact(code_block_stride)
1546        .enumerate()
1547        .take(job.height as usize)
1548    {
1549        let row_start = row_idx * job.output_stride;
1550        let output_row = &mut output[row_start..row_start + code_block_stride];
1551        for (coefficient, sample) in coeff_row.iter().copied().zip(output_row.iter_mut()) {
1552            let coefficient =
1553                j2c::ht_block_decode::coefficient_to_i32(coefficient, coded_bitplanes);
1554            let coefficient = apply_roi_maxshift_inverse_i32(coefficient, job.roi_shift);
1555            *sample = coefficient as f32 * job.dequantization_step;
1556        }
1557    }
1558
1559    Ok(())
1560}
1561
1562fn decode_ht_code_block_scalar_for_phase_with_workspace_profiled<const PHASE_LIMIT: u8>(
1563    job: HtCodeBlockDecodeJob<'_>,
1564    output: &mut [f32],
1565    workspace: &mut HtCodeBlockDecodeWorkspace,
1566    profile: &mut HtCodeBlockDecodeProfile,
1567) -> Result<()> {
1568    let required_len = if job.height == 0 {
1569        0
1570    } else {
1571        job.output_stride
1572            .checked_mul(job.height as usize - 1)
1573            .and_then(|prefix| prefix.checked_add(job.width as usize))
1574            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1575    };
1576    if output.len() < required_len {
1577        bail!(DecodingError::CodeBlockDecodeFailure);
1578    }
1579    let code_block_stride =
1580        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1581    let code_block_len = code_block_stride
1582        .checked_mul(job.height as usize)
1583        .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1584
1585    let segments = j2c::ht_block_decode::HtCodeBlockSegments::from_combined_payload(
1586        job.data,
1587        job.cleanup_length,
1588        job.refinement_length,
1589    )?;
1590    let coded_bitplanes = add_roi_shift_to_bitplanes(job.num_bitplanes, job.roi_shift, 31)?;
1591    workspace.coefficients.clear();
1592    workspace.coefficients.resize(code_block_len, 0);
1593    let mut stats = j2c::ht_block_decode::HtBlockDecodeStats::default();
1594    j2c::ht_block_decode::decode_segments_validated_with_scratch_for_phase::<PHASE_LIMIT>(
1595        &segments,
1596        job.missing_bit_planes,
1597        coded_bitplanes,
1598        job.number_of_coding_passes,
1599        job.stripe_causal,
1600        job.strict,
1601        &mut workspace.coefficients,
1602        job.width,
1603        job.height,
1604        job.width,
1605        &mut workspace.scratch,
1606        Some(&mut stats),
1607        true,
1608    )?;
1609    profile.add_native_stats(stats);
1610
1611    for (row_idx, coeff_row) in workspace
1612        .coefficients
1613        .chunks_exact(code_block_stride)
1614        .enumerate()
1615        .take(job.height as usize)
1616    {
1617        let row_start = row_idx * job.output_stride;
1618        let output_row = &mut output[row_start..row_start + code_block_stride];
1619        for (coefficient, sample) in coeff_row.iter().copied().zip(output_row.iter_mut()) {
1620            let coefficient =
1621                j2c::ht_block_decode::coefficient_to_i32(coefficient, coded_bitplanes);
1622            let coefficient = apply_roi_maxshift_inverse_i32(coefficient, job.roi_shift);
1623            *sample = coefficient as f32 * job.dequantization_step;
1624        }
1625    }
1626
1627    Ok(())
1628}
1629
1630/// Adapter HTJ2K SigProp benchmark state for backend experimentation.
1631pub struct HtSigPropBenchmarkState(j2c::ht_block_decode::HtSigPropBenchmarkState);
1632
1633impl HtSigPropBenchmarkState {
1634    /// Coefficient buffer length required by `decode_ht_sigprop_benchmark_state`.
1635    pub fn output_len(&self) -> usize {
1636        self.0.output_len()
1637    }
1638}
1639
1640/// Adapter helper that precomputes cleanup-derived SigProp inputs for benchmarks.
1641pub fn prepare_ht_sigprop_benchmark_state(
1642    job: HtCodeBlockDecodeJob<'_>,
1643) -> Result<HtSigPropBenchmarkState> {
1644    let segments = j2c::ht_block_decode::HtCodeBlockSegments::from_combined_payload(
1645        job.data,
1646        job.cleanup_length,
1647        job.refinement_length,
1648    )?;
1649    let state = j2c::ht_block_decode::prepare_sigprop_benchmark_state(
1650        &segments,
1651        job.missing_bit_planes,
1652        job.num_bitplanes,
1653        job.number_of_coding_passes,
1654        job.stripe_causal,
1655        job.strict,
1656        job.width,
1657        job.height,
1658        job.width,
1659    )?;
1660    Ok(HtSigPropBenchmarkState(state))
1661}
1662
1663/// Adapter helper that runs only the HTJ2K significance-propagation phase.
1664pub fn decode_ht_sigprop_benchmark_state(
1665    state: &mut HtSigPropBenchmarkState,
1666    output: &mut [u32],
1667) -> Result<()> {
1668    j2c::ht_block_decode::decode_sigprop_benchmark_state(&mut state.0, output)
1669}
1670
1671/// Adapter HTJ2K VLC table 0 for backend experimentation.
1672pub fn ht_vlc_table0() -> &'static [u16; 1024] {
1673    &j2c::ht_tables::VLC_TABLE0
1674}
1675
1676/// Adapter HTJ2K VLC table 1 for backend experimentation.
1677pub fn ht_vlc_table1() -> &'static [u16; 1024] {
1678    &j2c::ht_tables::VLC_TABLE1
1679}
1680
1681/// Adapter HTJ2K UVLC table 0 for backend experimentation.
1682pub fn ht_uvlc_table0() -> &'static [u16; 320] {
1683    &j2c::ht_tables::UVLC_TABLE0
1684}
1685
1686/// Adapter HTJ2K UVLC table 1 for backend experimentation.
1687pub fn ht_uvlc_table1() -> &'static [u16; 256] {
1688    &j2c::ht_tables::UVLC_TABLE1
1689}
1690
1691/// Adapter HTJ2K cleanup encoder VLC table 0 for backend experimentation.
1692pub fn ht_vlc_encode_table0() -> &'static [u16; 2048] {
1693    &j2c::ht_encode_tables::HT_VLC_ENCODE_TABLE0
1694}
1695
1696/// Adapter HTJ2K cleanup encoder VLC table 1 for backend experimentation.
1697pub fn ht_vlc_encode_table1() -> &'static [u16; 2048] {
1698    &j2c::ht_encode_tables::HT_VLC_ENCODE_TABLE1
1699}
1700
1701/// Adapter HTJ2K cleanup encoder UVLC table for backend experimentation.
1702pub fn ht_uvlc_encode_table() -> &'static [HtUvlcTableEntry; 75] {
1703    &j2c::ht_encode_tables::HT_UVLC_ENCODE_TABLE
1704}
1705
1706/// Adapter HTJ2K cleanup encoder UVLC table packed for byte-addressed backends.
1707pub fn ht_uvlc_encode_table_bytes() -> &'static [u8] {
1708    &j2c::ht_encode_tables::HT_UVLC_ENCODE_TABLE_BYTES
1709}
1710
1711/// JP2 signature box: 00 00 00 0C 6A 50 20 20
1712pub(crate) const JP2_MAGIC: &[u8] = b"\x00\x00\x00\x0C\x6A\x50\x20\x20";
1713/// Codestream signature: FF 4F FF 51 (SOC + SIZ markers)
1714pub(crate) const CODESTREAM_MAGIC: &[u8] = b"\xFF\x4F\xFF\x51";
1715
1716/// Settings to apply during decoding.
1717#[derive(Debug, Copy, Clone)]
1718pub struct DecodeSettings {
1719    /// Whether palette indices should be resolved.
1720    ///
1721    /// JPEG2000 images can be stored in two different ways. First, by storing
1722    /// RGB values (depending on the color space) for each pixel. Secondly, by
1723    /// only storing a single index for each channel, and then resolving the
1724    /// actual color using the index.
1725    ///
1726    /// If you disable this option, in case you have an image with palette
1727    /// indices, they will not be resolved, but instead a grayscale image
1728    /// will be returned, with each pixel value corresponding to the palette
1729    /// index of the location.
1730    pub resolve_palette_indices: bool,
1731    /// Whether strict mode should be enabled when decoding.
1732    ///
1733    /// It is recommended to leave this flag disabled, unless you have a
1734    /// specific reason not to.
1735    pub strict: bool,
1736    /// A hint for the target resolution that the image should be decoded at.
1737    pub target_resolution: Option<(u32, u32)>,
1738}
1739
1740impl Default for DecodeSettings {
1741    fn default() -> Self {
1742        Self {
1743            resolve_palette_indices: true,
1744            strict: false,
1745            target_resolution: None,
1746        }
1747    }
1748}
1749
1750/// A JPEG2000 image or codestream.
1751pub struct Image<'a> {
1752    /// The tile-part payload used by the legacy JPEG 2000 decoder.
1753    pub(crate) codestream: &'a [u8],
1754    /// The header of the J2C codestream.
1755    pub(crate) header: Header<'a>,
1756    /// The JP2 boxes of the image. In the case of a raw codestream, we
1757    /// will synthesize the necessary boxes.
1758    pub(crate) boxes: ImageBoxes,
1759    /// Settings that should be applied during decoding.
1760    pub(crate) settings: DecodeSettings,
1761    /// Whether the image has an alpha channel.
1762    pub(crate) has_alpha: bool,
1763    /// The color space of the image.
1764    pub(crate) color_space: ColorSpace,
1765}
1766
1767impl<'a> Image<'a> {
1768    /// Try to create a new JPEG2000 image from the given data.
1769    pub fn new(data: &'a [u8], settings: &DecodeSettings) -> Result<Self> {
1770        if data.starts_with(JP2_MAGIC) {
1771            jp2::parse(data, *settings)
1772        } else if data.starts_with(CODESTREAM_MAGIC) {
1773            j2c::parse(data, settings)
1774        } else {
1775            err!(FormatError::InvalidSignature)
1776        }
1777    }
1778
1779    /// Whether the image has an alpha channel.
1780    pub fn has_alpha(&self) -> bool {
1781        self.has_alpha
1782    }
1783
1784    /// The color space of the image.
1785    pub fn color_space(&self) -> &ColorSpace {
1786        &self.color_space
1787    }
1788
1789    /// The width of the image.
1790    pub fn width(&self) -> u32 {
1791        self.header.size_data.image_width()
1792    }
1793
1794    /// The height of the image.
1795    pub fn height(&self) -> u32 {
1796        self.header.size_data.image_height()
1797    }
1798
1799    /// The original bit depth of the image. You usually don't need to do anything
1800    /// with this parameter, it just exists for informational purposes.
1801    pub fn original_bit_depth(&self) -> u8 {
1802        // Note that this only works if all components have the same precision.
1803        self.header.component_infos[0].size_info.precision
1804    }
1805
1806    /// Whether decode finishes with additional host-side component mutation or reordering.
1807    pub fn supports_direct_device_plane_reuse(&self) -> bool {
1808        if self.settings.resolve_palette_indices && self.boxes.palette.is_some() {
1809            return false;
1810        }
1811        if self.boxes.channel_definition.is_some() {
1812            return false;
1813        }
1814        !matches!(
1815            self.boxes
1816                .color_specification
1817                .as_ref()
1818                .map(|spec| &spec.color_space),
1819            Some(jp2::colr::ColorSpace::Enumerated(
1820                EnumeratedColorspace::Sycc | EnumeratedColorspace::CieLab(_)
1821            ))
1822        )
1823    }
1824
1825    /// Decode the image and return its decoded result as a `Vec<u8>`, with each
1826    /// channel interleaved.
1827    pub fn decode(&self) -> Result<Vec<u8>> {
1828        let bitmap = self.decode_with_context(&mut DecoderContext::default())?;
1829        Ok(bitmap.data)
1830    }
1831
1832    /// Decode the image and return its decoded result using a caller-provided
1833    /// decoder context so allocations can be reused across repeated decodes.
1834    pub fn decode_with_context(&self, decoder_context: &mut DecoderContext<'a>) -> Result<Bitmap> {
1835        let buffer_size = checked_decode_byte_len3(
1836            self.width() as usize,
1837            self.height() as usize,
1838            self.color_space.num_channels() as usize + if self.has_alpha { 1 } else { 0 },
1839        )?;
1840        let mut buf = vec![0; buffer_size];
1841        self.decode_into(&mut buf, decoder_context)?;
1842
1843        Ok(Bitmap {
1844            color_space: self.color_space.clone(),
1845            data: buf,
1846            has_alpha: self.has_alpha,
1847            width: self.width(),
1848            height: self.height(),
1849            original_bit_depth: self.original_bit_depth(),
1850        })
1851    }
1852
1853    /// Decode the image into borrowed component planes using a caller-provided
1854    /// decoder context so allocations can be reused across repeated decodes.
1855    pub fn decode_components_with_context<'ctx>(
1856        &self,
1857        decoder_context: &'ctx mut DecoderContext<'a>,
1858    ) -> Result<DecodedComponents<'ctx>> {
1859        let decoded_image = self.prepare_decoded_image(decoder_context)?;
1860        let planes = decoded_image
1861            .decoded_components
1862            .iter()
1863            .map(|component| ComponentPlane {
1864                samples: component.container.truncated(),
1865                bit_depth: component.bit_depth,
1866            })
1867            .collect();
1868
1869        Ok(DecodedComponents {
1870            dimensions: (self.width(), self.height()),
1871            color_space: self.color_space.clone(),
1872            has_alpha: self.has_alpha,
1873            planes,
1874        })
1875    }
1876
1877    /// Build a adapter grayscale direct device plan without materializing host component planes.
1878    pub fn build_direct_grayscale_plan_with_context(
1879        &self,
1880        decoder_context: &mut DecoderContext<'a>,
1881    ) -> Result<J2kDirectGrayscalePlan> {
1882        if !matches!(self.color_space, ColorSpace::Gray) || self.has_alpha {
1883            bail!(DecodingError::UnsupportedFeature(
1884                "direct grayscale plan only supports grayscale images without alpha"
1885            ));
1886        }
1887
1888        j2c::build_direct_grayscale_plan(self.codestream, &self.header, decoder_context)
1889    }
1890
1891    /// Build a adapter grayscale direct device plan for an output-space region.
1892    pub fn build_direct_grayscale_plan_region_with_context(
1893        &self,
1894        decoder_context: &mut DecoderContext<'a>,
1895        output_region: (u32, u32, u32, u32),
1896    ) -> Result<J2kDirectGrayscalePlan> {
1897        if !matches!(self.color_space, ColorSpace::Gray) || self.has_alpha {
1898            bail!(DecodingError::UnsupportedFeature(
1899                "direct grayscale plan only supports grayscale images without alpha"
1900            ));
1901        }
1902
1903        decoder_context.set_output_region(Some(output_region));
1904        let result =
1905            j2c::build_direct_grayscale_plan(self.codestream, &self.header, decoder_context);
1906        decoder_context.set_output_region(None);
1907        result
1908    }
1909
1910    /// Build a adapter RGB direct device plan without materializing host component planes.
1911    pub fn build_direct_color_plan_with_context(
1912        &self,
1913        decoder_context: &mut DecoderContext<'a>,
1914    ) -> Result<J2kDirectColorPlan> {
1915        if !matches!(self.color_space, ColorSpace::RGB) || self.has_alpha {
1916            bail!(DecodingError::UnsupportedFeature(
1917                "direct color plan only supports RGB images without alpha"
1918            ));
1919        }
1920
1921        j2c::build_direct_color_plan(self.codestream, &self.header, decoder_context)
1922    }
1923
1924    /// Build a adapter RGB direct device plan for an output-space region.
1925    pub fn build_direct_color_plan_region_with_context(
1926        &self,
1927        decoder_context: &mut DecoderContext<'a>,
1928        output_region: (u32, u32, u32, u32),
1929    ) -> Result<J2kDirectColorPlan> {
1930        if !matches!(self.color_space, ColorSpace::RGB) || self.has_alpha {
1931            bail!(DecodingError::UnsupportedFeature(
1932                "direct color plan only supports RGB images without alpha"
1933            ));
1934        }
1935
1936        decoder_context.set_output_region(Some(output_region));
1937        let result = j2c::build_direct_color_plan(self.codestream, &self.header, decoder_context);
1938        decoder_context.set_output_region(None);
1939        result
1940    }
1941
1942    /// Decode borrowed component planes while delegating HTJ2K code-block decode.
1943    pub fn decode_components_with_ht_decoder<'ctx>(
1944        &self,
1945        decoder_context: &'ctx mut DecoderContext<'a>,
1946        ht_decoder: &mut dyn HtCodeBlockDecoder,
1947    ) -> Result<DecodedComponents<'ctx>> {
1948        let decoded_image =
1949            self.prepare_decoded_image_with_ht_decoder(decoder_context, ht_decoder)?;
1950        let planes = decoded_image
1951            .decoded_components
1952            .iter()
1953            .map(|component| ComponentPlane {
1954                samples: component.container.truncated(),
1955                bit_depth: component.bit_depth,
1956            })
1957            .collect();
1958
1959        Ok(DecodedComponents {
1960            dimensions: (self.width(), self.height()),
1961            color_space: self.color_space.clone(),
1962            has_alpha: self.has_alpha,
1963            planes,
1964        })
1965    }
1966
1967    /// Decode borrowed component planes for a requested region using a
1968    /// caller-provided decoder context.
1969    pub fn decode_region_components_with_context<'ctx>(
1970        &self,
1971        roi: (u32, u32, u32, u32),
1972        decoder_context: &'ctx mut DecoderContext<'a>,
1973    ) -> Result<DecodedComponents<'ctx>> {
1974        validate_roi((self.width(), self.height()), roi)?;
1975        let (_x, _y, width, height) = roi;
1976        let decoded_image = self.prepare_decoded_image_with_region(decoder_context, Some(roi))?;
1977        let planes = decoded_image
1978            .decoded_components
1979            .iter()
1980            .map(|component| ComponentPlane {
1981                samples: component.container.truncated(),
1982                bit_depth: component.bit_depth,
1983            })
1984            .collect();
1985
1986        Ok(DecodedComponents {
1987            dimensions: (width, height),
1988            color_space: self.color_space.clone(),
1989            has_alpha: self.has_alpha,
1990            planes,
1991        })
1992    }
1993
1994    /// Decode borrowed component planes for a requested region while
1995    /// delegating code-block/transform stages through the adapter backend hook.
1996    pub fn decode_region_components_with_ht_decoder<'ctx>(
1997        &self,
1998        decoder_context: &'ctx mut DecoderContext<'a>,
1999        roi: (u32, u32, u32, u32),
2000        ht_decoder: &mut dyn HtCodeBlockDecoder,
2001    ) -> Result<DecodedComponents<'ctx>> {
2002        validate_roi((self.width(), self.height()), roi)?;
2003        let (_x, _y, width, height) = roi;
2004        let decoded_image = self.prepare_decoded_image_with_region_and_ht_decoder(
2005            decoder_context,
2006            Some(roi),
2007            Some(ht_decoder),
2008        )?;
2009        let planes = decoded_image
2010            .decoded_components
2011            .iter()
2012            .map(|component| ComponentPlane {
2013                samples: component.container.truncated(),
2014                bit_depth: component.bit_depth,
2015            })
2016            .collect();
2017
2018        Ok(DecodedComponents {
2019            dimensions: (width, height),
2020            color_space: self.color_space.clone(),
2021            has_alpha: self.has_alpha,
2022            planes,
2023        })
2024    }
2025
2026    /// Decode a region of the image and return it as an 8-bit interleaved bitmap.
2027    pub fn decode_region(&self, roi: (u32, u32, u32, u32)) -> Result<Bitmap> {
2028        self.decode_region_with_context(roi, &mut DecoderContext::default())
2029    }
2030
2031    /// Decode a region of the image and return it as an 8-bit interleaved bitmap
2032    /// using a caller-provided decoder context.
2033    pub fn decode_region_with_context(
2034        &self,
2035        roi: (u32, u32, u32, u32),
2036        decoder_context: &mut DecoderContext<'a>,
2037    ) -> Result<Bitmap> {
2038        validate_roi((self.width(), self.height()), roi)?;
2039        let mut decoded_image =
2040            self.prepare_decoded_image_with_region(decoder_context, Some(roi))?;
2041        let (_x, _y, width, height) = roi;
2042        let channels =
2043            self.color_space.num_channels() as usize + if self.has_alpha { 1 } else { 0 };
2044        let data_len = checked_decode_byte_len3(width as usize, height as usize, channels)?;
2045        let mut data = vec![0; data_len];
2046        interleave_and_convert_region(
2047            &mut decoded_image,
2048            width as usize,
2049            (0, 0, width, height),
2050            &mut data,
2051        );
2052        Ok(Bitmap {
2053            color_space: self.color_space.clone(),
2054            data,
2055            has_alpha: self.has_alpha,
2056            width,
2057            height,
2058            original_bit_depth: self.original_bit_depth(),
2059        })
2060    }
2061
2062    /// Decode the image at native bit depth without scaling to 8-bit.
2063    ///
2064    /// For images with bit depth ≤ 8, returns pixel data as `Vec<u8>`.
2065    /// For images with bit depth > 8 (e.g., 12-bit or 16-bit), returns
2066    /// pixel data as little-endian `u16` values packed into `Vec<u8>`.
2067    ///
2068    /// This is essential for medical imaging (DICOM) where 12-bit and 16-bit
2069    /// images must preserve their full dynamic range.
2070    pub fn decode_native(&self) -> Result<RawBitmap> {
2071        let mut decoder_context = DecoderContext::default();
2072        self.decode_native_with_context(&mut decoder_context)
2073    }
2074
2075    /// Extract reversible 5/3 wavelet coefficients for coefficient-domain
2076    /// classic JPEG 2000 to HTJ2K recoding.
2077    ///
2078    /// This decodes classic Tier-1 code-blocks into dequantized reversible
2079    /// wavelet coefficients, but does not run inverse DWT or color conversion.
2080    pub fn decode_reversible_53_coefficients(&self) -> Result<Reversible53CoefficientImage> {
2081        let mut decoder_context = DecoderContext::default();
2082        self.decode_reversible_53_coefficients_with_context(&mut decoder_context)
2083    }
2084
2085    /// Extract reversible 5/3 wavelet coefficients using a caller-provided
2086    /// decoder context.
2087    pub fn decode_reversible_53_coefficients_with_context(
2088        &self,
2089        decoder_context: &mut DecoderContext<'a>,
2090    ) -> Result<Reversible53CoefficientImage> {
2091        j2c::recode::extract_reversible_53_coefficients(
2092            self.codestream,
2093            &self.header,
2094            decoder_context,
2095        )
2096    }
2097
2098    /// Decode a region of the image at native bit depth.
2099    pub fn decode_native_region(&self, roi: (u32, u32, u32, u32)) -> Result<RawBitmap> {
2100        self.decode_native_region_with_context(roi, &mut DecoderContext::default())
2101    }
2102
2103    /// Decode the image at native bit depth using a caller-provided decoder
2104    /// context so allocations can be reused across repeated decodes.
2105    pub fn decode_native_with_context(
2106        &self,
2107        decoder_context: &mut DecoderContext<'a>,
2108    ) -> Result<RawBitmap> {
2109        self.decode_with_output_region(decoder_context, None)?;
2110
2111        let components = &decoder_context.tile_decode_context.channel_data;
2112        let bit_depth = self.original_bit_depth();
2113        let num_components =
2114            u8::try_from(components.len()).map_err(|_| ValidationError::TooManyChannels)?;
2115        let width = self.width();
2116        let height = self.height();
2117        let pixel_count = checked_decode_sample_count(width, height)?;
2118
2119        if bit_depth <= 8 {
2120            let max_val = ((1u32 << bit_depth) - 1) as f32;
2121            let capacity = checked_decode_byte_len2(pixel_count, num_components as usize)?;
2122            let mut data = Vec::with_capacity(capacity);
2123            for i in 0..pixel_count {
2124                for component in components.iter() {
2125                    let v = math::round_f32(component.container.truncated()[i]);
2126                    let clamped = if v < 0.0 {
2127                        0.0
2128                    } else if v > max_val {
2129                        max_val
2130                    } else {
2131                        v
2132                    };
2133                    data.push(clamped as u8);
2134                }
2135            }
2136            Ok(RawBitmap {
2137                data,
2138                width,
2139                height,
2140                bit_depth,
2141                num_components,
2142                bytes_per_sample: 1,
2143            })
2144        } else {
2145            let max_val = ((1u32 << bit_depth) - 1) as f32;
2146            let capacity = checked_decode_byte_len3(pixel_count, num_components as usize, 2)?;
2147            let mut data = Vec::with_capacity(capacity);
2148            for i in 0..pixel_count {
2149                for component in components.iter() {
2150                    let v = math::round_f32(component.container.truncated()[i]);
2151                    let clamped = if v < 0.0 {
2152                        0.0
2153                    } else if v > max_val {
2154                        max_val
2155                    } else {
2156                        v
2157                    };
2158                    let val = clamped as u16;
2159                    data.extend_from_slice(&val.to_le_bytes());
2160                }
2161            }
2162            Ok(RawBitmap {
2163                data,
2164                width,
2165                height,
2166                bit_depth,
2167                num_components,
2168                bytes_per_sample: 2,
2169            })
2170        }
2171    }
2172
2173    /// Decode a region of the image at native bit depth using a caller-provided
2174    /// decoder context.
2175    pub fn decode_native_region_with_context(
2176        &self,
2177        roi: (u32, u32, u32, u32),
2178        decoder_context: &mut DecoderContext<'a>,
2179    ) -> Result<RawBitmap> {
2180        validate_roi((self.width(), self.height()), roi)?;
2181        self.decode_with_output_region(decoder_context, Some(roi))?;
2182
2183        let components = &decoder_context.tile_decode_context.channel_data;
2184        let bit_depth = self.original_bit_depth();
2185        let num_components =
2186            u8::try_from(components.len()).map_err(|_| ValidationError::TooManyChannels)?;
2187        let bytes_per_sample = if bit_depth <= 8 { 1 } else { 2 };
2188        let (_x, _y, width, height) = roi;
2189        let capacity = checked_decode_byte_len4(
2190            width as usize,
2191            height as usize,
2192            num_components as usize,
2193            bytes_per_sample,
2194        )?;
2195        let mut data = Vec::with_capacity(capacity);
2196        let max_val = ((1u32 << bit_depth) - 1) as f32;
2197
2198        for row in 0..height as usize {
2199            for col in 0..width as usize {
2200                let idx = row * width as usize + col;
2201                for component in components {
2202                    let v = math::round_f32(component.container.truncated()[idx]);
2203                    let clamped = if v < 0.0 {
2204                        0.0
2205                    } else if v > max_val {
2206                        max_val
2207                    } else {
2208                        v
2209                    };
2210                    if bit_depth <= 8 {
2211                        data.push(clamped as u8);
2212                    } else {
2213                        data.extend_from_slice(&(clamped as u16).to_le_bytes());
2214                    }
2215                }
2216            }
2217        }
2218
2219        Ok(RawBitmap {
2220            data,
2221            width,
2222            height,
2223            bit_depth,
2224            num_components,
2225            bytes_per_sample: bytes_per_sample as u8,
2226        })
2227    }
2228
2229    /// Decode the image into the given buffer.
2230    ///
2231    /// This method does the same as [`Image::decode`], but you can provide
2232    /// a custom buffer for the output, as well as a decoder context. Doing
2233    /// so allows the internal decode engine to reuse memory allocations, so
2234    /// this is especially recommended if you plan on converting multiple
2235    /// images in the same session.
2236    ///
2237    /// The buffer must have the correct size.
2238    pub fn decode_into(
2239        &self,
2240        buf: &mut [u8],
2241        decoder_context: &mut DecoderContext<'a>,
2242    ) -> Result<()> {
2243        let mut decoded_image = self.prepare_decoded_image(decoder_context)?;
2244        validate_interleaved_output_buffer(&decoded_image, buf)?;
2245        interleave_and_convert(&mut decoded_image, buf)?;
2246
2247        Ok(())
2248    }
2249
2250    fn prepare_decoded_image<'ctx>(
2251        &self,
2252        decoder_context: &'ctx mut DecoderContext<'a>,
2253    ) -> Result<DecodedImage<'ctx>> {
2254        self.prepare_decoded_image_with_region(decoder_context, None)
2255    }
2256
2257    fn prepare_decoded_image_with_ht_decoder<'ctx>(
2258        &self,
2259        decoder_context: &'ctx mut DecoderContext<'a>,
2260        ht_decoder: &mut dyn HtCodeBlockDecoder,
2261    ) -> Result<DecodedImage<'ctx>> {
2262        self.prepare_decoded_image_with_region_and_ht_decoder(
2263            decoder_context,
2264            None,
2265            Some(ht_decoder),
2266        )
2267    }
2268
2269    fn prepare_decoded_image_with_region<'ctx>(
2270        &self,
2271        decoder_context: &'ctx mut DecoderContext<'a>,
2272        output_region: Option<(u32, u32, u32, u32)>,
2273    ) -> Result<DecodedImage<'ctx>> {
2274        self.prepare_decoded_image_with_region_and_ht_decoder(decoder_context, output_region, None)
2275    }
2276
2277    fn prepare_decoded_image_with_region_and_ht_decoder<'ctx>(
2278        &self,
2279        decoder_context: &'ctx mut DecoderContext<'a>,
2280        output_region: Option<(u32, u32, u32, u32)>,
2281        ht_decoder: Option<&mut dyn HtCodeBlockDecoder>,
2282    ) -> Result<DecodedImage<'ctx>> {
2283        let settings = &self.settings;
2284        self.decode_with_output_region_and_ht_decoder(decoder_context, output_region, ht_decoder)?;
2285        let mut decoded_image = DecodedImage {
2286            decoded_components: &mut decoder_context.tile_decode_context.channel_data,
2287            boxes: self.boxes.clone(),
2288        };
2289
2290        if settings.resolve_palette_indices {
2291            let components = core::mem::take(decoded_image.decoded_components);
2292            *decoded_image.decoded_components =
2293                resolve_palette_indices(components, &decoded_image.boxes)?;
2294        }
2295
2296        if let Some(cdef) = &decoded_image.boxes.channel_definition {
2297            validate_channel_definition(cdef, decoded_image.decoded_components.len())?;
2298            let mut components = decoded_image
2299                .decoded_components
2300                .iter()
2301                .cloned()
2302                .zip(
2303                    cdef.channel_definitions
2304                        .iter()
2305                        .map(|c| match c._association {
2306                            ChannelAssociation::WholeImage => u16::MAX,
2307                            ChannelAssociation::Colour(c) => c,
2308                        }),
2309                )
2310                .collect::<Vec<_>>();
2311            components.sort_by_key(|component| component.1);
2312            *decoded_image.decoded_components = components.into_iter().map(|c| c.0).collect();
2313        }
2314
2315        let bit_depth = decoded_image.decoded_components[0].bit_depth;
2316        convert_color_space(&mut decoded_image, bit_depth)?;
2317        Ok(decoded_image)
2318    }
2319
2320    fn decode_with_output_region(
2321        &self,
2322        decoder_context: &mut DecoderContext<'a>,
2323        output_region: Option<(u32, u32, u32, u32)>,
2324    ) -> Result<()> {
2325        self.decode_with_output_region_and_ht_decoder(decoder_context, output_region, None)
2326    }
2327
2328    fn decode_with_output_region_and_ht_decoder(
2329        &self,
2330        decoder_context: &mut DecoderContext<'a>,
2331        output_region: Option<(u32, u32, u32, u32)>,
2332        mut ht_decoder: Option<&mut dyn HtCodeBlockDecoder>,
2333    ) -> Result<()> {
2334        decoder_context.set_output_region(output_region);
2335        let decode_result = j2c::decode(
2336            self.codestream,
2337            &self.header,
2338            decoder_context,
2339            &mut ht_decoder,
2340        );
2341        decoder_context.set_output_region(None);
2342        decode_result
2343    }
2344}
2345
2346fn validate_channel_definition(
2347    cdef: &jp2::cdef::ChannelDefinitionBox,
2348    component_count: usize,
2349) -> Result<()> {
2350    if cdef.channel_definitions.len() != component_count {
2351        bail!(ValidationError::InvalidChannelDefinition);
2352    }
2353
2354    let mut seen_color_associations = vec![false; component_count];
2355    for definition in &cdef.channel_definitions {
2356        if let ChannelAssociation::Colour(association) = definition._association {
2357            let Some(index) = association.checked_sub(1).map(usize::from) else {
2358                bail!(ValidationError::InvalidChannelDefinition);
2359            };
2360            if index >= component_count || seen_color_associations[index] {
2361                bail!(ValidationError::InvalidChannelDefinition);
2362            }
2363            seen_color_associations[index] = true;
2364        }
2365    }
2366
2367    Ok(())
2368}
2369
2370pub(crate) fn resolve_alpha_and_color_space(
2371    boxes: &ImageBoxes,
2372    header: &Header<'_>,
2373    settings: &DecodeSettings,
2374) -> Result<(ColorSpace, bool)> {
2375    let mut num_components = header.component_infos.len();
2376
2377    // Override number of components with what is actually in the palette box
2378    // in case we resolve them.
2379    if settings.resolve_palette_indices {
2380        if let Some(palette_box) = &boxes.palette {
2381            num_components = palette_box.columns.len();
2382        }
2383    }
2384
2385    let mut has_alpha = false;
2386
2387    if let Some(cdef) = &boxes.channel_definition {
2388        let last = cdef.channel_definitions.last().unwrap();
2389        has_alpha = last.channel_type == ChannelType::Opacity;
2390    }
2391
2392    let mut color_space = get_color_space(boxes, num_components)?;
2393
2394    // If we didn't resolve palette indices, we need to assume grayscale image.
2395    if !settings.resolve_palette_indices && boxes.palette.is_some() {
2396        has_alpha = false;
2397        color_space = ColorSpace::Gray;
2398    }
2399
2400    let actual_num_components = header.component_infos.len();
2401
2402    // Validate the number of channels.
2403    if boxes.palette.is_none()
2404        && actual_num_components
2405            != (color_space.num_channels() + if has_alpha { 1 } else { 0 }) as usize
2406    {
2407        if !settings.strict
2408            && actual_num_components == color_space.num_channels() as usize + 1
2409            && !has_alpha
2410        {
2411            // See OPENJPEG test case orb-blue10-lin-j2k. Assume that we have an
2412            // alpha channel in this case.
2413            has_alpha = true;
2414        } else {
2415            // Color space is invalid, attempt to repair.
2416            if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
2417                color_space = ColorSpace::Gray;
2418            } else if actual_num_components == 3 {
2419                color_space = ColorSpace::RGB;
2420            } else if actual_num_components == 4 {
2421                if has_alpha {
2422                    color_space = ColorSpace::RGB;
2423                } else {
2424                    color_space = ColorSpace::CMYK;
2425                }
2426            } else {
2427                bail!(ValidationError::TooManyChannels);
2428            }
2429        }
2430    }
2431
2432    Ok((color_space, has_alpha))
2433}
2434
2435/// The color space of the image.
2436#[derive(Debug, Clone)]
2437pub enum ColorSpace {
2438    /// A grayscale image.
2439    Gray,
2440    /// An RGB image.
2441    RGB,
2442    /// A CMYK image.
2443    CMYK,
2444    /// An unknown color space.
2445    Unknown {
2446        /// The number of channels of the color space.
2447        num_channels: u8,
2448    },
2449    /// An image based on an ICC profile.
2450    Icc {
2451        /// The raw data of the ICC profile.
2452        profile: Vec<u8>,
2453        /// The number of channels used by the ICC profile.
2454        num_channels: u8,
2455    },
2456}
2457
2458impl ColorSpace {
2459    /// Return the number of expected channels for the color space.
2460    pub fn num_channels(&self) -> u8 {
2461        match self {
2462            Self::Gray => 1,
2463            Self::RGB => 3,
2464            Self::CMYK => 4,
2465            Self::Unknown { num_channels } => *num_channels,
2466            Self::Icc {
2467                num_channels: num_components,
2468                ..
2469            } => *num_components,
2470        }
2471    }
2472}
2473
2474/// A bitmap storing the decoded result of the image.
2475pub struct Bitmap {
2476    /// The color space of the image.
2477    pub color_space: ColorSpace,
2478    /// The raw pixel data of the image. The result will always be in
2479    /// 8-bit (in case the original image had a different bit-depth, this
2480    /// decode path scales it to 8-bit).
2481    ///
2482    /// The size is guaranteed to equal
2483    /// `width * height * (num_channels + (if has_alpha { 1 } else { 0 })`.
2484    /// Pixels are interleaved on a per-channel basis, the alpha channel always
2485    /// appearing as the last channel, if available.
2486    pub data: Vec<u8>,
2487    /// Whether the image has an alpha channel.
2488    pub has_alpha: bool,
2489    /// The width of the image.
2490    pub width: u32,
2491    /// The height of the image.
2492    pub height: u32,
2493    /// The original bit depth of the image. You usually don't need to do anything
2494    /// with this parameter, it just exists for informational purposes.
2495    pub original_bit_depth: u8,
2496}
2497
2498/// Raw decoded pixel data at native bit depth (no 8-bit scaling).
2499///
2500/// For bit depths ≤ 8, `data` contains one byte per sample.
2501/// For bit depths > 8 (e.g., 12 or 16), `data` contains two bytes per sample
2502/// in little-endian byte order (`u16` LE).
2503///
2504/// Samples are interleaved: for a 3-component image, the layout is
2505/// `[R0, G0, B0, R1, G1, B1, ...]`.
2506pub struct RawBitmap {
2507    /// The raw pixel data at native bit depth.
2508    pub data: Vec<u8>,
2509    /// The width of the image in pixels.
2510    pub width: u32,
2511    /// The height of the image in pixels.
2512    pub height: u32,
2513    /// The original bit depth per sample (e.g., 8, 12, 16).
2514    pub bit_depth: u8,
2515    /// The number of components (e.g., 1 for grayscale, 3 for RGB).
2516    pub num_components: u8,
2517    /// Bytes per sample: 1 for bit_depth ≤ 8, 2 for bit_depth > 8.
2518    pub bytes_per_sample: u8,
2519}
2520
2521/// A borrowed decoded component plane.
2522pub struct ComponentPlane<'a> {
2523    samples: &'a [f32],
2524    bit_depth: u8,
2525}
2526
2527impl<'a> ComponentPlane<'a> {
2528    /// Component samples in row-major order.
2529    pub fn samples(&self) -> &'a [f32] {
2530        self.samples
2531    }
2532
2533    /// Bit depth of this component plane.
2534    pub fn bit_depth(&self) -> u8 {
2535        self.bit_depth
2536    }
2537}
2538
2539/// Borrowed decoded component planes for an image.
2540pub struct DecodedComponents<'a> {
2541    dimensions: (u32, u32),
2542    color_space: ColorSpace,
2543    has_alpha: bool,
2544    planes: Vec<ComponentPlane<'a>>,
2545}
2546
2547impl<'a> DecodedComponents<'a> {
2548    /// Dimensions of the decoded image represented by these planes.
2549    pub fn dimensions(&self) -> (u32, u32) {
2550        self.dimensions
2551    }
2552
2553    /// Color space after JPEG 2000 color conversion has been applied.
2554    pub fn color_space(&self) -> &ColorSpace {
2555        &self.color_space
2556    }
2557
2558    /// Whether the decoded image has an alpha channel.
2559    pub fn has_alpha(&self) -> bool {
2560        self.has_alpha
2561    }
2562
2563    /// Borrowed decoded component planes in display order.
2564    pub fn planes(&self) -> &[ComponentPlane<'a>] {
2565        &self.planes
2566    }
2567}
2568
2569fn validate_interleaved_output_buffer(image: &DecodedImage<'_>, buf: &[u8]) -> Result<()> {
2570    let required_len = interleaved_output_len(image)?;
2571    if buf.len() < required_len {
2572        bail!(DecodingError::OutputBufferTooSmall);
2573    }
2574    Ok(())
2575}
2576
2577fn interleaved_output_len(image: &DecodedImage<'_>) -> Result<usize> {
2578    let Some(first) = image.decoded_components.first() else {
2579        bail!(DecodingError::CodeBlockDecodeFailure);
2580    };
2581    first
2582        .container
2583        .truncated()
2584        .len()
2585        .checked_mul(image.decoded_components.len())
2586        .ok_or(ValidationError::ImageTooLarge.into())
2587}
2588
2589fn interleave_and_convert(image: &mut DecodedImage<'_>, buf: &mut [u8]) -> Result<()> {
2590    let components = &mut *image.decoded_components;
2591    let num_components = components.len();
2592
2593    let mut all_same_bit_depth = Some(components[0].bit_depth);
2594
2595    for component in components.iter().skip(1) {
2596        if Some(component.bit_depth) != all_same_bit_depth {
2597            all_same_bit_depth = None;
2598        }
2599    }
2600
2601    let max_len = components[0].container.truncated().len();
2602
2603    let mut output_iter = buf.iter_mut();
2604
2605    if all_same_bit_depth == Some(8) && num_components <= 4 {
2606        // Fast path for the common case.
2607        match num_components {
2608            // Gray-scale.
2609            1 => {
2610                for (output, input) in output_iter.zip(
2611                    components[0]
2612                        .container
2613                        .iter()
2614                        .map(|v| math::round_f32(*v) as u8),
2615                ) {
2616                    *output = input;
2617                }
2618            }
2619            // Gray-scale with alpha.
2620            2 => {
2621                let c0 = &components[0];
2622                let c1 = &components[1];
2623
2624                let c0 = &c0.container[..max_len];
2625                let c1 = &c1.container[..max_len];
2626
2627                for i in 0..max_len {
2628                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
2629                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
2630                }
2631            }
2632            // RGB
2633            3 => {
2634                let c0 = &components[0];
2635                let c1 = &components[1];
2636                let c2 = &components[2];
2637
2638                let c0 = &c0.container[..max_len];
2639                let c1 = &c1.container[..max_len];
2640                let c2 = &c2.container[..max_len];
2641
2642                for i in 0..max_len {
2643                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
2644                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
2645                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
2646                }
2647            }
2648            // RGBA or CMYK.
2649            4 => {
2650                let c0 = &components[0];
2651                let c1 = &components[1];
2652                let c2 = &components[2];
2653                let c3 = &components[3];
2654
2655                let c0 = &c0.container[..max_len];
2656                let c1 = &c1.container[..max_len];
2657                let c2 = &c2.container[..max_len];
2658                let c3 = &c3.container[..max_len];
2659
2660                for i in 0..max_len {
2661                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
2662                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
2663                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
2664                    *output_iter.next().unwrap() = math::round_f32(c3[i]) as u8;
2665                }
2666            }
2667            _ => bail!(ValidationError::TooManyChannels),
2668        }
2669    } else {
2670        // Slow path that also requires us to scale to 8 bit.
2671        let mul_factor = ((1 << 8) - 1) as f32;
2672
2673        for sample in 0..max_len {
2674            for channel in components.iter() {
2675                *output_iter.next().unwrap() = math::round_f32(
2676                    (channel.container[sample] / ((1_u32 << channel.bit_depth) - 1) as f32)
2677                        * mul_factor,
2678                ) as u8;
2679            }
2680        }
2681    }
2682
2683    Ok(())
2684}
2685
2686fn interleave_and_convert_region(
2687    image: &mut DecodedImage<'_>,
2688    image_width: usize,
2689    roi: (u32, u32, u32, u32),
2690    buf: &mut [u8],
2691) {
2692    let components = &mut *image.decoded_components;
2693    let num_components = components.len();
2694    let (x, y, width, height) = roi;
2695    let mut output_iter = buf.iter_mut();
2696
2697    let mut all_same_bit_depth = Some(components[0].bit_depth);
2698    for component in components.iter().skip(1) {
2699        if Some(component.bit_depth) != all_same_bit_depth {
2700            all_same_bit_depth = None;
2701        }
2702    }
2703
2704    if all_same_bit_depth == Some(8) && num_components <= 4 {
2705        for row in y as usize..(y + height) as usize {
2706            let row_base = row * image_width;
2707            for col in x as usize..(x + width) as usize {
2708                let idx = row_base + col;
2709                for component in components.iter() {
2710                    *output_iter.next().unwrap() = math::round_f32(component.container[idx]) as u8;
2711                }
2712            }
2713        }
2714    } else {
2715        let mul_factor = ((1 << 8) - 1) as f32;
2716        for row in y as usize..(y + height) as usize {
2717            let row_base = row * image_width;
2718            for col in x as usize..(x + width) as usize {
2719                let idx = row_base + col;
2720                for component in components.iter() {
2721                    *output_iter.next().unwrap() = math::round_f32(
2722                        (component.container[idx] / ((1_u32 << component.bit_depth) - 1) as f32)
2723                            * mul_factor,
2724                    ) as u8;
2725                }
2726            }
2727        }
2728    }
2729}
2730
2731fn validate_roi(dims: (u32, u32), roi: (u32, u32, u32, u32)) -> Result<()> {
2732    let (image_width, image_height) = dims;
2733    let (x, y, width, height) = roi;
2734    let x_end = x
2735        .checked_add(width)
2736        .ok_or(ValidationError::InvalidDimensions)?;
2737    let y_end = y
2738        .checked_add(height)
2739        .ok_or(ValidationError::InvalidDimensions)?;
2740    if x_end > image_width || y_end > image_height {
2741        return Err(ValidationError::InvalidDimensions.into());
2742    }
2743    Ok(())
2744}
2745
2746fn convert_color_space(image: &mut DecodedImage<'_>, bit_depth: u8) -> Result<()> {
2747    if let Some(jp2::colr::ColorSpace::Enumerated(e)) = &image
2748        .boxes
2749        .color_specification
2750        .as_ref()
2751        .map(|i| &i.color_space)
2752    {
2753        match e {
2754            EnumeratedColorspace::Sycc => {
2755                dispatch!(Level::new(), simd => {
2756                    sycc_to_rgb(simd, image.decoded_components, bit_depth)
2757                })?;
2758            }
2759            EnumeratedColorspace::CieLab(cielab) => {
2760                dispatch!(Level::new(), simd => {
2761                    cielab_to_rgb(simd, image.decoded_components, bit_depth, cielab)
2762                })?;
2763            }
2764            _ => {}
2765        }
2766    }
2767
2768    Ok(())
2769}
2770
2771fn get_color_space(boxes: &ImageBoxes, num_components: usize) -> Result<ColorSpace> {
2772    let cs = match boxes
2773        .color_specification
2774        .as_ref()
2775        .map(|c| &c.color_space)
2776        .unwrap_or(&jp2::colr::ColorSpace::Unknown)
2777    {
2778        jp2::colr::ColorSpace::Enumerated(e) => {
2779            match e {
2780                EnumeratedColorspace::Cmyk => ColorSpace::CMYK,
2781                EnumeratedColorspace::Srgb => ColorSpace::RGB,
2782                EnumeratedColorspace::RommRgb => {
2783                    // Use an ICC profile to process the RommRGB color space.
2784                    ColorSpace::Icc {
2785                        profile: include_bytes!("../assets/ProPhoto-v2-micro.icc").to_vec(),
2786                        num_channels: 3,
2787                    }
2788                }
2789                EnumeratedColorspace::EsRgb => ColorSpace::RGB,
2790                EnumeratedColorspace::Greyscale => ColorSpace::Gray,
2791                EnumeratedColorspace::Sycc => ColorSpace::RGB,
2792                EnumeratedColorspace::CieLab(_) => ColorSpace::Icc {
2793                    profile: include_bytes!("../assets/LAB.icc").to_vec(),
2794                    num_channels: 3,
2795                },
2796                _ => bail!(FormatError::Unsupported),
2797            }
2798        }
2799        jp2::colr::ColorSpace::Icc(icc) => {
2800            if let Some(metadata) = ICCMetadata::from_data(icc) {
2801                ColorSpace::Icc {
2802                    profile: icc.clone(),
2803                    num_channels: metadata.color_space.num_components(),
2804                }
2805            } else {
2806                // See OPENJPEG test orb-blue10-lin-jp2.jp2. They seem to
2807                // assume RGB in this case (even though the image has 4
2808                // components with no opacity channel, they assume RGBA instead
2809                // of CMYK).
2810                ColorSpace::RGB
2811            }
2812        }
2813        jp2::colr::ColorSpace::Unknown => match num_components {
2814            1 => ColorSpace::Gray,
2815            3 => ColorSpace::RGB,
2816            4 => ColorSpace::CMYK,
2817            _ => ColorSpace::Unknown {
2818                num_channels: num_components as u8,
2819            },
2820        },
2821    };
2822
2823    Ok(cs)
2824}
2825
2826fn resolve_palette_indices(
2827    components: Vec<ComponentData>,
2828    boxes: &ImageBoxes,
2829) -> Result<Vec<ComponentData>> {
2830    let Some(palette) = boxes.palette.as_ref() else {
2831        // Nothing to resolve.
2832        return Ok(components);
2833    };
2834
2835    let Some(mapping) = boxes.component_mapping.as_ref() else {
2836        bail!(ColorError::PaletteResolutionFailed);
2837    };
2838    if mapping.entries.is_empty() {
2839        bail!(ColorError::PaletteResolutionFailed);
2840    }
2841
2842    let mut resolved = Vec::with_capacity(mapping.entries.len());
2843
2844    for entry in &mapping.entries {
2845        let component_idx = entry.component_index as usize;
2846        let component = components
2847            .get(component_idx)
2848            .ok_or(ColorError::PaletteResolutionFailed)?;
2849
2850        match entry.mapping_type {
2851            ComponentMappingType::Direct => resolved.push(component.clone()),
2852            ComponentMappingType::Palette { column } => {
2853                let column_idx = column as usize;
2854                let column_info = palette
2855                    .columns
2856                    .get(column_idx)
2857                    .ok_or(ColorError::PaletteResolutionFailed)?;
2858
2859                let mut mapped =
2860                    Vec::with_capacity(component.container.truncated().len() + SIMD_WIDTH);
2861
2862                for &sample in component.container.truncated() {
2863                    let index = math::round_f32(sample) as i64;
2864                    let value = palette
2865                        .map(index as usize, column_idx)
2866                        .ok_or(ColorError::PaletteResolutionFailed)?;
2867                    mapped.push(value as f32);
2868                }
2869
2870                resolved.push(ComponentData {
2871                    container: math::SimdBuffer::new(mapped),
2872                    bit_depth: column_info.bit_depth,
2873                });
2874            }
2875        }
2876    }
2877
2878    Ok(resolved)
2879}
2880
2881#[inline(always)]
2882fn cielab_to_rgb<S: Simd>(
2883    simd: S,
2884    components: &mut [ComponentData],
2885    bit_depth: u8,
2886    lab: &CieLab,
2887) -> Result<()> {
2888    let (head, _) = components
2889        .split_at_mut_checked(3)
2890        .ok_or(ColorError::LabConversionFailed)?;
2891
2892    let [l, a, b] = head else {
2893        bail!(ColorError::LabConversionFailed);
2894    };
2895
2896    let prec0 = l.bit_depth;
2897    let prec1 = a.bit_depth;
2898    let prec2 = b.bit_depth;
2899
2900    // Prevent underflows/divisions by zero further below.
2901    if prec0 < 4 || prec1 < 4 || prec2 < 4 {
2902        bail!(ColorError::LabConversionFailed);
2903    }
2904
2905    let rl = lab.rl.unwrap_or(100);
2906    let ra = lab.ra.unwrap_or(170);
2907    let rb = lab.ra.unwrap_or(200);
2908    let ol = lab.ol.unwrap_or(0);
2909    let oa = lab.oa.unwrap_or(1 << (bit_depth - 1));
2910    let ob = lab
2911        .ob
2912        .unwrap_or((1 << (bit_depth - 2)) + (1 << (bit_depth - 3)));
2913
2914    // Copied from OpenJPEG.
2915    let min_l = -(rl as f32 * ol as f32) / ((1 << prec0) - 1) as f32;
2916    let max_l = min_l + rl as f32;
2917    let min_a = -(ra as f32 * oa as f32) / ((1 << prec1) - 1) as f32;
2918    let max_a = min_a + ra as f32;
2919    let min_b = -(rb as f32 * ob as f32) / ((1 << prec2) - 1) as f32;
2920    let max_b = min_b + rb as f32;
2921
2922    let bit_max = (1_u32 << bit_depth) - 1;
2923
2924    // Note that we are not doing the actual conversion with the ICC profile yet,
2925    // just decoding the raw LAB values.
2926    // We leave applying the ICC profile to the user.
2927    let divisor_l = ((1 << prec0) - 1) as f32;
2928    let divisor_a = ((1 << prec1) - 1) as f32;
2929    let divisor_b = ((1 << prec2) - 1) as f32;
2930
2931    let scale_l_final = bit_max as f32 / 100.0;
2932    let scale_ab_final = bit_max as f32 / 255.0;
2933
2934    let l_offset = min_l * scale_l_final;
2935    let l_scale = (max_l - min_l) / divisor_l * scale_l_final;
2936    let a_offset = (min_a + 128.0) * scale_ab_final;
2937    let a_scale = (max_a - min_a) / divisor_a * scale_ab_final;
2938    let b_offset = (min_b + 128.0) * scale_ab_final;
2939    let b_scale = (max_b - min_b) / divisor_b * scale_ab_final;
2940
2941    let l_offset_v = f32x8::splat(simd, l_offset);
2942    let l_scale_v = f32x8::splat(simd, l_scale);
2943    let a_offset_v = f32x8::splat(simd, a_offset);
2944    let a_scale_v = f32x8::splat(simd, a_scale);
2945    let b_offset_v = f32x8::splat(simd, b_offset);
2946    let b_scale_v = f32x8::splat(simd, b_scale);
2947
2948    // Note that we are not doing the actual conversion with the ICC profile yet,
2949    // just decoding the raw LAB values.
2950    // We leave applying the ICC profile to the user.
2951    for ((l_chunk, a_chunk), b_chunk) in l
2952        .container
2953        .chunks_exact_mut(SIMD_WIDTH)
2954        .zip(a.container.chunks_exact_mut(SIMD_WIDTH))
2955        .zip(b.container.chunks_exact_mut(SIMD_WIDTH))
2956    {
2957        let l_v = f32x8::from_slice(simd, l_chunk);
2958        let a_v = f32x8::from_slice(simd, a_chunk);
2959        let b_v = f32x8::from_slice(simd, b_chunk);
2960
2961        l_v.mul_add(l_scale_v, l_offset_v).store(l_chunk);
2962        a_v.mul_add(a_scale_v, a_offset_v).store(a_chunk);
2963        b_v.mul_add(b_scale_v, b_offset_v).store(b_chunk);
2964    }
2965
2966    Ok(())
2967}
2968
2969#[inline(always)]
2970fn sycc_to_rgb<S: Simd>(simd: S, components: &mut [ComponentData], bit_depth: u8) -> Result<()> {
2971    let offset = (1_u32 << (bit_depth as u32 - 1)) as f32;
2972    let max_value = ((1_u32 << bit_depth as u32) - 1) as f32;
2973
2974    let (head, _) = components
2975        .split_at_mut_checked(3)
2976        .ok_or(ColorError::SyccConversionFailed)?;
2977
2978    let [y, cb, cr] = head else {
2979        bail!(ColorError::SyccConversionFailed);
2980    };
2981
2982    let offset_v = f32x8::splat(simd, offset);
2983    let max_v = f32x8::splat(simd, max_value);
2984    let zero_v = f32x8::splat(simd, 0.0);
2985    let cr_to_r = f32x8::splat(simd, 1.402);
2986    let cb_to_g = f32x8::splat(simd, -0.344136);
2987    let cr_to_g = f32x8::splat(simd, -0.714136);
2988    let cb_to_b = f32x8::splat(simd, 1.772);
2989
2990    for ((y_chunk, cb_chunk), cr_chunk) in y
2991        .container
2992        .chunks_exact_mut(SIMD_WIDTH)
2993        .zip(cb.container.chunks_exact_mut(SIMD_WIDTH))
2994        .zip(cr.container.chunks_exact_mut(SIMD_WIDTH))
2995    {
2996        let y_v = f32x8::from_slice(simd, y_chunk);
2997        let cb_v = f32x8::from_slice(simd, cb_chunk) - offset_v;
2998        let cr_v = f32x8::from_slice(simd, cr_chunk) - offset_v;
2999
3000        // r = y + 1.402 * cr
3001        let r = cr_v.mul_add(cr_to_r, y_v);
3002        // g = y - 0.344136 * cb - 0.714136 * cr
3003        let g = cr_v.mul_add(cr_to_g, cb_v.mul_add(cb_to_g, y_v));
3004        // b = y + 1.772 * cb
3005        let b = cb_v.mul_add(cb_to_b, y_v);
3006
3007        r.min(max_v).max(zero_v).store(y_chunk);
3008        g.min(max_v).max(zero_v).store(cb_chunk);
3009        b.min(max_v).max(zero_v).store(cr_chunk);
3010    }
3011
3012    Ok(())
3013}
3014
3015#[cfg(test)]
3016mod tests {
3017    use super::*;
3018
3019    #[test]
3020    fn ht_uvlc_encode_table_bytes_match_entry_packing_order() {
3021        let entries = ht_uvlc_encode_table();
3022        let bytes = ht_uvlc_encode_table_bytes();
3023
3024        assert_eq!(bytes.len(), entries.len() * 6);
3025        for (index, entry) in entries.iter().enumerate() {
3026            let offset = index * 6;
3027            assert_eq!(
3028                &bytes[offset..offset + 6],
3029                &[
3030                    entry.pre,
3031                    entry.pre_len,
3032                    entry.suf,
3033                    entry.suf_len,
3034                    entry.ext,
3035                    entry.ext_len
3036                ],
3037            );
3038        }
3039    }
3040
3041    #[test]
3042    fn roi_maxshift_inverse_preserves_background_and_unshifts_roi_coefficients() {
3043        assert_eq!(apply_roi_maxshift_inverse_i32(127, 7), 127);
3044        assert_eq!(apply_roi_maxshift_inverse_i32(-127, 7), -127);
3045        assert_eq!(apply_roi_maxshift_inverse_i32(128, 7), 1);
3046        assert_eq!(apply_roi_maxshift_inverse_i32(-128, 7), -1);
3047        assert_eq!(apply_roi_maxshift_inverse_i32(255, 7), 1);
3048        assert_eq!(apply_roi_maxshift_inverse_i32(-255, 7), -1);
3049        assert_eq!(apply_roi_maxshift_inverse_i32(256, 7), 2);
3050        assert_eq!(apply_roi_maxshift_inverse_i32(-256, 7), -2);
3051        assert_eq!(apply_roi_maxshift_inverse_i32(42, 0), 42);
3052    }
3053
3054    #[test]
3055    fn classic_scalar_decode_applies_nonzero_roi_maxshift() {
3056        let roi_shift = 3;
3057        let total_bitplanes = 3;
3058        let style = J2kCodeBlockStyle {
3059            selective_arithmetic_coding_bypass: false,
3060            reset_context_probabilities: false,
3061            termination_on_each_pass: false,
3062            vertically_causal_context: false,
3063            segmentation_symbols: false,
3064        };
3065        let coded_coefficients = [0, 5, 1 << roi_shift, -(2 << roi_shift)];
3066        let encoded = encode_j2k_code_block_scalar_with_style(
3067            &coded_coefficients,
3068            2,
3069            2,
3070            J2kSubBandType::LowLow,
3071            total_bitplanes + roi_shift,
3072            style,
3073        )
3074        .expect("encode ROI-shifted code block");
3075        let job = J2kCodeBlockDecodeJob {
3076            data: &encoded.data,
3077            segments: &encoded.segments,
3078            width: 2,
3079            height: 2,
3080            output_stride: 2,
3081            missing_bit_planes: encoded.missing_bit_planes,
3082            number_of_coding_passes: encoded.number_of_coding_passes,
3083            total_bitplanes,
3084            roi_shift,
3085            sub_band_type: J2kSubBandType::LowLow,
3086            style,
3087            strict: true,
3088            dequantization_step: 1.0,
3089        };
3090        let mut output = [0.0; 4];
3091
3092        decode_j2k_code_block_scalar(job, &mut output).expect("decode ROI-shifted code block");
3093
3094        assert_eq!(output, [0.0, 5.0, 1.0, -2.0]);
3095    }
3096
3097    #[test]
3098    fn classic_scalar_token_pack_matches_scalar_single_cleanup_block() {
3099        let style = J2kCodeBlockStyle {
3100            selective_arithmetic_coding_bypass: true,
3101            reset_context_probabilities: false,
3102            termination_on_each_pass: false,
3103            vertically_causal_context: false,
3104            segmentation_symbols: false,
3105        };
3106        let scalar =
3107            encode_j2k_code_block_scalar_with_style(&[1], 1, 1, J2kSubBandType::LowLow, 1, style)
3108                .expect("encode scalar");
3109        let token_bytes = pack_mq_test_tokens(&[(0, 1), (9, 0)]);
3110        let packed = pack_j2k_code_block_scalar_from_tier1_tokens(
3111            &token_bytes,
3112            &[J2kTier1TokenSegment {
3113                token_bit_offset: 0,
3114                token_bit_count: 12,
3115                start_coding_pass: 0,
3116                end_coding_pass: 1,
3117                use_arithmetic: true,
3118            }],
3119            scalar.number_of_coding_passes,
3120            scalar.missing_bit_planes,
3121        )
3122        .expect("pack tokens");
3123
3124        assert_eq!(packed.data, scalar.data);
3125        assert_eq!(packed.segments, scalar.segments);
3126        assert_eq!(
3127            packed.number_of_coding_passes,
3128            scalar.number_of_coding_passes
3129        );
3130        assert_eq!(packed.missing_bit_planes, scalar.missing_bit_planes);
3131    }
3132
3133    fn pack_mq_test_tokens(tokens: &[(u8, u8)]) -> Vec<u8> {
3134        let mut bytes = Vec::new();
3135        let mut current = 0u8;
3136        let mut bits = 0u8;
3137        for &(ctx, bit) in tokens {
3138            let value = (ctx & 0x1F) | ((bit & 1) << 5);
3139            for shift in (0..6).rev() {
3140                current = (current << 1) | ((value >> shift) & 1);
3141                bits += 1;
3142                if bits == 8 {
3143                    bytes.push(current);
3144                    current = 0;
3145                    bits = 0;
3146                }
3147            }
3148        }
3149        if bits != 0 {
3150            bytes.push(current << (8 - bits));
3151        }
3152        bytes
3153    }
3154
3155    #[test]
3156    fn classic_scalar_profiled_decode_matches_unprofiled_decode() {
3157        let width = 64u32;
3158        let height = 64u32;
3159        let sample_count = width as usize * height as usize;
3160        let total_bitplanes = 12;
3161        let style = J2kCodeBlockStyle {
3162            selective_arithmetic_coding_bypass: false,
3163            reset_context_probabilities: false,
3164            termination_on_each_pass: false,
3165            vertically_causal_context: false,
3166            segmentation_symbols: false,
3167        };
3168        let coefficients = (0..sample_count)
3169            .map(|idx| {
3170                let value = i32::try_from((idx * 37) % 4095).expect("sample value fits i32") - 2048;
3171                if idx % 17 == 0 {
3172                    0
3173                } else {
3174                    value
3175                }
3176            })
3177            .collect::<Vec<_>>();
3178        let encoded = encode_j2k_code_block_scalar_with_style(
3179            &coefficients,
3180            width,
3181            height,
3182            J2kSubBandType::LowLow,
3183            total_bitplanes,
3184            style,
3185        )
3186        .expect("encode classic block");
3187        let job = J2kCodeBlockDecodeJob {
3188            data: &encoded.data,
3189            segments: &encoded.segments,
3190            width,
3191            height,
3192            output_stride: width as usize,
3193            missing_bit_planes: encoded.missing_bit_planes,
3194            number_of_coding_passes: encoded.number_of_coding_passes,
3195            total_bitplanes,
3196            roi_shift: 0,
3197            sub_band_type: J2kSubBandType::LowLow,
3198            style,
3199            strict: true,
3200            dequantization_step: 1.0,
3201        };
3202        let mut expected = vec![0.0_f32; sample_count];
3203        let mut actual = vec![0.0_f32; sample_count];
3204        let mut profile = J2kCodeBlockDecodeProfile::default();
3205
3206        decode_j2k_code_block_scalar(job, &mut expected).expect("unprofiled classic decode");
3207        decode_j2k_code_block_scalar_profiled(job, &mut actual, &mut profile)
3208            .expect("profiled classic decode");
3209
3210        assert_eq!(actual, expected);
3211        assert!(profile.cleanup_us > 0);
3212    }
3213
3214    #[test]
3215    fn classic_scalar_workspace_reuse_matches_fresh_decode() {
3216        let total_bitplanes = 6;
3217        let style = J2kCodeBlockStyle {
3218            selective_arithmetic_coding_bypass: false,
3219            reset_context_probabilities: false,
3220            termination_on_each_pass: false,
3221            vertically_causal_context: false,
3222            segmentation_symbols: false,
3223        };
3224        let mut workspace = J2kCodeBlockDecodeWorkspace::default();
3225
3226        for (width, height, seed) in [(8, 8, 0x31), (4, 16, 0x47)] {
3227            let coefficients = (0..width * height)
3228                .map(|idx| {
3229                    let value = ((idx as i32 * seed) % 23) - 11;
3230                    if idx % 7 == 0 {
3231                        0
3232                    } else {
3233                        value
3234                    }
3235                })
3236                .collect::<Vec<_>>();
3237            let encoded = encode_j2k_code_block_scalar_with_style(
3238                &coefficients,
3239                width,
3240                height,
3241                J2kSubBandType::LowLow,
3242                total_bitplanes,
3243                style,
3244            )
3245            .expect("encode classic block");
3246            let job = J2kCodeBlockDecodeJob {
3247                data: &encoded.data,
3248                segments: &encoded.segments,
3249                width,
3250                height,
3251                output_stride: width as usize,
3252                missing_bit_planes: encoded.missing_bit_planes,
3253                number_of_coding_passes: encoded.number_of_coding_passes,
3254                total_bitplanes,
3255                roi_shift: 0,
3256                sub_band_type: J2kSubBandType::LowLow,
3257                style,
3258                strict: true,
3259                dequantization_step: 1.0,
3260            };
3261            let mut fresh = vec![0.0_f32; width as usize * height as usize];
3262            let mut reused = vec![0.0_f32; width as usize * height as usize];
3263
3264            decode_j2k_code_block_scalar(job, &mut fresh).expect("fresh classic decode");
3265            decode_j2k_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
3266                .expect("workspace classic decode");
3267
3268            assert_eq!(reused, fresh);
3269        }
3270    }
3271
3272    #[test]
3273    fn scalar_packetization_rejects_overflowing_ht_refinement_lengths_without_panic() {
3274        let payload = [0x12];
3275        let block = J2kPacketizationCodeBlock {
3276            data: &payload,
3277            ht_cleanup_length: u32::MAX,
3278            ht_refinement_length: 1,
3279            num_coding_passes: 3,
3280            num_zero_bitplanes: 2,
3281            previously_included: false,
3282            l_block: 3,
3283            block_coding_mode: J2kPacketizationBlockCodingMode::HighThroughput,
3284        };
3285        let subband = J2kPacketizationSubband {
3286            code_blocks: vec![block],
3287            num_cbs_x: 1,
3288            num_cbs_y: 1,
3289        };
3290        let resolution = J2kPacketizationResolution {
3291            subbands: vec![subband],
3292        };
3293        let resolutions = [resolution];
3294        let job = J2kPacketizationEncodeJob {
3295            resolution_count: 1,
3296            num_layers: 1,
3297            num_components: 1,
3298            code_block_count: 1,
3299            progression_order: J2kPacketizationProgressionOrder::Lrcp,
3300            packet_descriptors: &[],
3301            resolutions: &resolutions,
3302        };
3303
3304        let err = encode_j2k_packetization_scalar(job)
3305            .expect_err("overflowing HT packetization segment lengths rejected");
3306
3307        assert_eq!(err, "multi-pass HTJ2K packet contribution length overflow");
3308    }
3309
3310    #[derive(Default)]
3311    struct DecodeWorkCounter {
3312        classic_code_blocks: usize,
3313        ht_code_blocks: usize,
3314        idwt_output_samples: usize,
3315    }
3316
3317    impl DecodeWorkCounter {
3318        fn code_blocks(&self) -> usize {
3319            self.classic_code_blocks + self.ht_code_blocks
3320        }
3321    }
3322
3323    struct FailingHtDecoder {
3324        called: bool,
3325    }
3326
3327    impl HtCodeBlockDecoder for FailingHtDecoder {
3328        fn decode_code_block(
3329            &mut self,
3330            _job: HtCodeBlockDecodeJob<'_>,
3331            _output: &mut [f32],
3332        ) -> Result<()> {
3333            self.called = true;
3334            Err(DecodingError::CodeBlockDecodeFailure.into())
3335        }
3336    }
3337
3338    struct FailingClassicDecoder {
3339        called: bool,
3340    }
3341
3342    impl HtCodeBlockDecoder for FailingClassicDecoder {
3343        fn decode_code_block(
3344            &mut self,
3345            _job: HtCodeBlockDecodeJob<'_>,
3346            _output: &mut [f32],
3347        ) -> Result<()> {
3348            panic!("HT hook must not be used for classic J2K test")
3349        }
3350
3351        fn decode_j2k_code_block(
3352            &mut self,
3353            _job: J2kCodeBlockDecodeJob<'_>,
3354            _output: &mut [f32],
3355        ) -> Result<bool> {
3356            self.called = true;
3357            Err(DecodingError::CodeBlockDecodeFailure.into())
3358        }
3359    }
3360
3361    struct FailingClassicBatchDecoder {
3362        called: bool,
3363    }
3364
3365    #[derive(Default)]
3366    struct CapturingHtDecoder {
3367        called: bool,
3368        blocks: usize,
3369        refinement_jobs: usize,
3370        max_coding_passes: u8,
3371    }
3372
3373    impl HtCodeBlockDecoder for CapturingHtDecoder {
3374        fn decode_code_block(
3375            &mut self,
3376            job: HtCodeBlockDecodeJob<'_>,
3377            output: &mut [f32],
3378        ) -> Result<()> {
3379            self.called = true;
3380            self.blocks += 1;
3381            self.max_coding_passes = self.max_coding_passes.max(job.number_of_coding_passes);
3382            if job.refinement_length > 0 {
3383                self.refinement_jobs += 1;
3384                assert!(
3385                    job.number_of_coding_passes > 1,
3386                    "refinement bytes must correspond to refinement coding passes"
3387                );
3388            }
3389
3390            decode_ht_code_block_scalar(job, output)
3391        }
3392    }
3393
3394    #[derive(Clone)]
3395    struct CapturedHtDecodeJob {
3396        data: Vec<u8>,
3397        cleanup_length: u32,
3398        refinement_length: u32,
3399        width: u32,
3400        height: u32,
3401        output_stride: usize,
3402        missing_bit_planes: u8,
3403        number_of_coding_passes: u8,
3404        num_bitplanes: u8,
3405        roi_shift: u8,
3406        stripe_causal: bool,
3407        strict: bool,
3408        dequantization_step: f32,
3409    }
3410
3411    impl CapturedHtDecodeJob {
3412        fn from_job(job: HtCodeBlockDecodeJob<'_>) -> Self {
3413            Self {
3414                data: job.data.to_vec(),
3415                cleanup_length: job.cleanup_length,
3416                refinement_length: job.refinement_length,
3417                width: job.width,
3418                height: job.height,
3419                output_stride: job.output_stride,
3420                missing_bit_planes: job.missing_bit_planes,
3421                number_of_coding_passes: job.number_of_coding_passes,
3422                num_bitplanes: job.num_bitplanes,
3423                roi_shift: job.roi_shift,
3424                stripe_causal: job.stripe_causal,
3425                strict: job.strict,
3426                dequantization_step: job.dequantization_step,
3427            }
3428        }
3429
3430        fn borrowed(&self) -> HtCodeBlockDecodeJob<'_> {
3431            HtCodeBlockDecodeJob {
3432                data: &self.data,
3433                cleanup_length: self.cleanup_length,
3434                refinement_length: self.refinement_length,
3435                width: self.width,
3436                height: self.height,
3437                output_stride: self.output_stride,
3438                missing_bit_planes: self.missing_bit_planes,
3439                number_of_coding_passes: self.number_of_coding_passes,
3440                num_bitplanes: self.num_bitplanes,
3441                roi_shift: self.roi_shift,
3442                stripe_causal: self.stripe_causal,
3443                strict: self.strict,
3444                dequantization_step: self.dequantization_step,
3445            }
3446        }
3447    }
3448
3449    #[derive(Default)]
3450    struct FirstHtJobDecoder {
3451        job: Option<CapturedHtDecodeJob>,
3452    }
3453
3454    impl HtCodeBlockDecoder for FirstHtJobDecoder {
3455        fn decode_code_block(
3456            &mut self,
3457            job: HtCodeBlockDecodeJob<'_>,
3458            output: &mut [f32],
3459        ) -> Result<()> {
3460            if self.job.is_none() {
3461                self.job = Some(CapturedHtDecodeJob::from_job(job));
3462            }
3463            decode_ht_code_block_scalar(job, output)
3464        }
3465    }
3466
3467    struct ZeroRefinementHtDecoder;
3468
3469    impl HtCodeBlockDecoder for ZeroRefinementHtDecoder {
3470        fn decode_code_block(
3471            &mut self,
3472            job: HtCodeBlockDecodeJob<'_>,
3473            output: &mut [f32],
3474        ) -> Result<()> {
3475            let mut data = job.data.to_vec();
3476            let cleanup_len = job.cleanup_length as usize;
3477            let refinement_len = job.refinement_length as usize;
3478            data[cleanup_len..cleanup_len + refinement_len].fill(0);
3479            let zeroed = HtCodeBlockDecodeJob { data: &data, ..job };
3480
3481            decode_ht_code_block_scalar(zeroed, output)
3482        }
3483    }
3484
3485    #[derive(Default)]
3486    struct CleanupLimitedHtDecoder {
3487        blocks: usize,
3488        refinement_blocks: usize,
3489        cleanup_bytes: usize,
3490        refinement_bytes: usize,
3491    }
3492
3493    impl HtCodeBlockDecoder for CleanupLimitedHtDecoder {
3494        fn decode_code_block(
3495            &mut self,
3496            job: HtCodeBlockDecodeJob<'_>,
3497            output: &mut [f32],
3498        ) -> Result<()> {
3499            self.blocks += 1;
3500            self.cleanup_bytes += job.cleanup_length as usize;
3501            if job.refinement_length > 0 {
3502                self.refinement_blocks += 1;
3503                self.refinement_bytes += job.refinement_length as usize;
3504            }
3505
3506            decode_ht_code_block_scalar_until_phase(
3507                job,
3508                output,
3509                HtCodeBlockDecodePhaseLimit::Cleanup,
3510            )
3511        }
3512    }
3513
3514    impl HtCodeBlockDecoder for FailingClassicBatchDecoder {
3515        fn decode_code_block(
3516            &mut self,
3517            _job: HtCodeBlockDecodeJob<'_>,
3518            _output: &mut [f32],
3519        ) -> Result<()> {
3520            panic!("HT hook must not be used for classic J2K batch test")
3521        }
3522
3523        fn decode_j2k_code_block(
3524            &mut self,
3525            _job: J2kCodeBlockDecodeJob<'_>,
3526            _output: &mut [f32],
3527        ) -> Result<bool> {
3528            panic!(
3529                "per-block classic hook must not be used when the batch hook handles the sub-band"
3530            )
3531        }
3532
3533        fn decode_j2k_sub_band(
3534            &mut self,
3535            _job: J2kSubBandDecodeJob<'_>,
3536            _output: &mut [f32],
3537        ) -> Result<bool> {
3538            self.called = true;
3539            Err(DecodingError::CodeBlockDecodeFailure.into())
3540        }
3541    }
3542
3543    fn fixture() -> Vec<u8> {
3544        let pixels = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
3545        let options = EncodeOptions {
3546            reversible: true,
3547            num_decomposition_levels: 1,
3548            ..EncodeOptions::default()
3549        };
3550        encode(&pixels, 2, 2, 3, 8, false, &options).expect("encode")
3551    }
3552
3553    #[test]
3554    fn decode_into_rejects_short_output_buffer() {
3555        let bytes = fixture();
3556        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3557        let mut context = DecoderContext::default();
3558        let mut output = vec![0; 11];
3559
3560        let err = image
3561            .decode_into(&mut output, &mut context)
3562            .expect_err("short output buffer must be rejected");
3563
3564        assert_eq!(
3565            err,
3566            DecodeError::Decoding(DecodingError::OutputBufferTooSmall)
3567        );
3568    }
3569
3570    fn fixture_multi_block() -> Vec<u8> {
3571        let pixels: Vec<u8> = (0..64).collect();
3572        let options = EncodeOptions {
3573            reversible: true,
3574            num_decomposition_levels: 0,
3575            code_block_width_exp: 0,
3576            code_block_height_exp: 0,
3577            ..EncodeOptions::default()
3578        };
3579        encode(&pixels, 8, 8, 1, 8, false, &options).expect("encode multi-block classic")
3580    }
3581
3582    fn fixture_gray() -> Vec<u8> {
3583        let pixels: Vec<u8> = (0..16).collect();
3584        let options = EncodeOptions {
3585            reversible: true,
3586            num_decomposition_levels: 1,
3587            ..EncodeOptions::default()
3588        };
3589        encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode classic gray8")
3590    }
3591
3592    fn rewrite_siz_to_single_large_tile(codestream: &mut [u8], dimensions: u32) {
3593        let siz = codestream
3594            .windows(2)
3595            .position(|w| w == [0xFF, 0x51])
3596            .expect("SIZ marker");
3597        codestream[siz + 6..siz + 10].copy_from_slice(&dimensions.to_be_bytes());
3598        codestream[siz + 10..siz + 14].copy_from_slice(&dimensions.to_be_bytes());
3599        codestream[siz + 22..siz + 26].copy_from_slice(&dimensions.to_be_bytes());
3600        codestream[siz + 26..siz + 30].copy_from_slice(&dimensions.to_be_bytes());
3601    }
3602
3603    fn rewrite_siz_tile_grid(codestream: &mut [u8], dimensions: (u32, u32), tile_size: (u32, u32)) {
3604        let siz = codestream
3605            .windows(2)
3606            .position(|w| w == [0xFF, 0x51])
3607            .expect("SIZ marker");
3608        codestream[siz + 6..siz + 10].copy_from_slice(&dimensions.0.to_be_bytes());
3609        codestream[siz + 10..siz + 14].copy_from_slice(&dimensions.1.to_be_bytes());
3610        codestream[siz + 22..siz + 26].copy_from_slice(&tile_size.0.to_be_bytes());
3611        codestream[siz + 26..siz + 30].copy_from_slice(&tile_size.1.to_be_bytes());
3612    }
3613
3614    fn rewrite_siz_component_count(codestream: &mut Vec<u8>, component_count: u16) {
3615        let siz = codestream
3616            .windows(2)
3617            .position(|w| w == [0xFF, 0x51])
3618            .expect("SIZ marker");
3619        let old_component_count =
3620            u16::from_be_bytes([codestream[siz + 38], codestream[siz + 39]]) as usize;
3621        let component_start = siz + 40;
3622        let component_end = component_start + old_component_count * 3;
3623        let descriptor = codestream[component_start..component_start + 3].to_vec();
3624        let mut descriptors = Vec::with_capacity(usize::from(component_count) * 3);
3625        for _ in 0..component_count {
3626            descriptors.extend_from_slice(&descriptor);
3627        }
3628
3629        let siz_len = 38_u16
3630            .checked_add(
3631                component_count
3632                    .checked_mul(3)
3633                    .expect("SIZ component bytes fit"),
3634            )
3635            .expect("SIZ length fits");
3636        codestream[siz + 2..siz + 4].copy_from_slice(&siz_len.to_be_bytes());
3637        codestream[siz + 38..siz + 40].copy_from_slice(&component_count.to_be_bytes());
3638        codestream.splice(component_start..component_end, descriptors);
3639    }
3640
3641    #[test]
3642    fn inspect_rejects_component_count_above_j2k_spec_cap() {
3643        let mut bytes = fixture_gray();
3644        rewrite_siz_component_count(&mut bytes, MAX_J2K_SPEC_COMPONENTS + 1);
3645
3646        let err = inspect_j2k_codestream_header(&bytes)
3647            .expect_err("SIZ component count above spec cap must be rejected");
3648
3649        assert_eq!(
3650            err,
3651            J2kCodestreamHeaderError::InvalidSiz {
3652                what: "component count exceeds JPEG 2000 limit"
3653            }
3654        );
3655    }
3656
3657    #[test]
3658    fn native_decode_rejects_component_count_above_u8_before_bitmap_truncation() {
3659        let mut bytes = fixture_gray();
3660        rewrite_siz_component_count(&mut bytes, MAX_NATIVE_DECODE_COMPONENTS + 1);
3661
3662        let err = match Image::new(&bytes, &DecodeSettings::default()) {
3663            Err(err) => err,
3664            Ok(_) => {
3665                panic!("native decode must reject component counts that cannot fit RawBitmap")
3666            }
3667        };
3668
3669        assert_eq!(
3670            err,
3671            DecodeError::Validation(ValidationError::TooManyChannels)
3672        );
3673    }
3674
3675    #[test]
3676    fn tile_parse_rejects_component_tile_structural_bomb_before_allocation() {
3677        let mut bytes = fixture_gray();
3678        rewrite_siz_component_count(&mut bytes, MAX_NATIVE_DECODE_COMPONENTS);
3679        rewrite_siz_tile_grid(&mut bytes, (256, 256), (1, 1));
3680        let parsed = j2c::parse_raw(&bytes, &DecodeSettings::default()).expect("raw header parses");
3681        let mut context = j2c::DecoderContext::default();
3682        let mut ht_decoder: Option<&mut dyn HtCodeBlockDecoder> = None;
3683
3684        let err = j2c::decode(parsed.data, &parsed.header, &mut context, &mut ht_decoder)
3685            .expect_err("tile structural budget must reject before tile allocation");
3686
3687        assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
3688    }
3689
3690    #[test]
3691    fn owned_decode_rejects_large_siz_before_allocating_output() {
3692        let mut bytes = fixture_gray();
3693        rewrite_siz_to_single_large_tile(&mut bytes, 60_000);
3694        let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
3695
3696        let err = match image.decode() {
3697            Err(err) => err,
3698            Ok(_) => panic!("large owned decode must be capped"),
3699        };
3700
3701        assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
3702    }
3703
3704    #[test]
3705    fn decode_into_rejects_large_siz_before_allocating_component_storage() {
3706        let mut bytes = fixture_gray();
3707        rewrite_siz_to_single_large_tile(&mut bytes, 60_000);
3708        let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
3709        let mut context = DecoderContext::default();
3710        let mut out = [];
3711
3712        let err = match image.decode_into(&mut out, &mut context) {
3713            Err(err) => err,
3714            Ok(_) => panic!("component storage must be capped before allocation"),
3715        };
3716
3717        assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
3718    }
3719
3720    fn fixture_ht_gray() -> Vec<u8> {
3721        let pixels: Vec<u8> = (0..16).collect();
3722        let options = EncodeOptions {
3723            reversible: true,
3724            num_decomposition_levels: 1,
3725            ..EncodeOptions::default()
3726        };
3727        encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode ht gray8")
3728    }
3729
3730    fn fixture_ht_multi_block() -> Vec<u8> {
3731        let pixels: Vec<u8> = (0..64).collect();
3732        let options = EncodeOptions {
3733            reversible: true,
3734            num_decomposition_levels: 0,
3735            code_block_width_exp: 0,
3736            code_block_height_exp: 0,
3737            ..EncodeOptions::default()
3738        };
3739        encode_htj2k(&pixels, 8, 8, 1, 8, false, &options).expect("encode multi-block HT gray8")
3740    }
3741
3742    fn fixture_ht_rgb_multi_block() -> Vec<u8> {
3743        let pixels = gradient_pixels(8, 8, 3);
3744        let options = EncodeOptions {
3745            reversible: true,
3746            num_decomposition_levels: 0,
3747            code_block_width_exp: 0,
3748            code_block_height_exp: 0,
3749            ..EncodeOptions::default()
3750        };
3751        encode_htj2k(&pixels, 8, 8, 3, 8, false, &options).expect("encode multi-block HT RGB8")
3752    }
3753
3754    fn direct_ht_job_count(plan: &J2kDirectGrayscalePlan) -> usize {
3755        plan.steps
3756            .iter()
3757            .map(|step| match step {
3758                J2kDirectGrayscaleStep::HtSubBand(sub_band) => sub_band.jobs.len(),
3759                _ => 0,
3760            })
3761            .sum()
3762    }
3763
3764    fn direct_color_ht_job_count(plan: &J2kDirectColorPlan) -> usize {
3765        plan.component_plans.iter().map(direct_ht_job_count).sum()
3766    }
3767
3768    fn fixture_openhtj2k_ht_refinement() -> &'static [u8] {
3769        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_12_b11.j2k")
3770    }
3771
3772    fn fixture_openhtj2k_ht_refinement_pixels() -> &'static [u8] {
3773        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_12_b11.gray")
3774    }
3775
3776    fn fixture_openhtj2k_ht_refinement_odd() -> &'static [u8] {
3777        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_09_b11.j2k")
3778    }
3779
3780    fn fixture_openhtj2k_ht_refinement_odd_pixels() -> &'static [u8] {
3781        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_09_b11.gray")
3782    }
3783
3784    fn gradient_pixels(width: u32, height: u32, components: u8) -> Vec<u8> {
3785        let mut pixels = Vec::with_capacity(width as usize * height as usize * components as usize);
3786        for y in 0..height {
3787            for x in 0..width {
3788                for component in 0..components {
3789                    pixels.push(((x * 3 + y * 5 + u32::from(component) * 41) & 0xff) as u8);
3790                }
3791            }
3792        }
3793        pixels
3794    }
3795
3796    fn roi_fixture(classic: bool, components: u8) -> Vec<u8> {
3797        let width = 64;
3798        let height = 64;
3799        let pixels = gradient_pixels(width, height, components);
3800        let options = EncodeOptions {
3801            reversible: true,
3802            num_decomposition_levels: 2,
3803            code_block_width_exp: 0,
3804            code_block_height_exp: 0,
3805            ..EncodeOptions::default()
3806        };
3807        if classic {
3808            encode(&pixels, width, height, components, 8, false, &options)
3809                .expect("encode ROI classic fixture")
3810        } else {
3811            encode_htj2k(&pixels, width, height, components, 8, false, &options)
3812                .expect("encode ROI HT fixture")
3813        }
3814    }
3815
3816    fn crop_interleaved(
3817        full: &[u8],
3818        full_width: u32,
3819        channels: usize,
3820        roi: (u32, u32, u32, u32),
3821    ) -> Vec<u8> {
3822        let (x, y, width, height) = roi;
3823        let mut out = Vec::with_capacity(width as usize * height as usize * channels);
3824        let row_bytes = full_width as usize * channels;
3825        let roi_row_bytes = width as usize * channels;
3826        for row in y as usize..(y + height) as usize {
3827            let start = row * row_bytes + x as usize * channels;
3828            out.extend_from_slice(&full[start..start + roi_row_bytes]);
3829        }
3830        out
3831    }
3832
3833    fn count_decode_work(bytes: &[u8], roi: Option<(u32, u32, u32, u32)>) -> DecodeWorkCounter {
3834        let image = Image::new(bytes, &DecodeSettings::default()).expect("image");
3835        let mut context = DecoderContext::default();
3836        match roi {
3837            Some(roi) => {
3838                image
3839                    .decode_region_with_context(roi, &mut context)
3840                    .expect("region decode with counter");
3841            }
3842            None => {
3843                image
3844                    .decode_with_context(&mut context)
3845                    .expect("full decode with counter");
3846            }
3847        }
3848        let counters = context.tile_decode_context.debug_counters;
3849        DecodeWorkCounter {
3850            classic_code_blocks: counters.decoded_code_blocks,
3851            ht_code_blocks: 0,
3852            idwt_output_samples: counters.idwt_output_samples,
3853        }
3854    }
3855
3856    #[test]
3857    fn roi_decode_matches_full_crop_for_classic_and_htj2k_gray_and_rgb() {
3858        let cases = [
3859            (true, 1_u8, true, false),
3860            (true, 3_u8, false, false),
3861            (false, 1_u8, true, false),
3862            (false, 3_u8, false, false),
3863        ];
3864        let rois = [
3865            (20, 18, 17, 19),
3866            (0, 0, 9, 11),
3867            (63, 63, 1, 1),
3868            (7, 5, 13, 9),
3869            (0, 0, 64, 64),
3870        ];
3871
3872        for (classic, components, expect_gray, has_alpha) in cases {
3873            let bytes = roi_fixture(classic, components);
3874            let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3875            let full = image.decode().expect("full decode");
3876            let channels = components as usize;
3877            for roi in rois {
3878                let region = image.decode_region(roi).expect("region decode");
3879                assert_eq!(matches!(region.color_space, ColorSpace::Gray), expect_gray);
3880                assert_eq!(region.has_alpha, has_alpha);
3881                assert_eq!(
3882                    region.data,
3883                    crop_interleaved(&full, 64, channels, roi),
3884                    "classic={classic} components={components} roi={roi:?}"
3885                );
3886            }
3887        }
3888    }
3889
3890    #[test]
3891    fn roi_decode_prunes_code_blocks_and_idwt_work_for_classic_and_htj2k() {
3892        let roi = (48, 48, 16, 16);
3893        for classic in [true, false] {
3894            let bytes = {
3895                let pixels = gradient_pixels(128, 128, 1);
3896                let options = EncodeOptions {
3897                    reversible: true,
3898                    num_decomposition_levels: 3,
3899                    code_block_width_exp: 0,
3900                    code_block_height_exp: 0,
3901                    ..EncodeOptions::default()
3902                };
3903                if classic {
3904                    encode(&pixels, 128, 128, 1, 8, false, &options)
3905                        .expect("encode classic work fixture")
3906                } else {
3907                    encode_htj2k(&pixels, 128, 128, 1, 8, false, &options)
3908                        .expect("encode ht work fixture")
3909                }
3910            };
3911            let full = count_decode_work(&bytes, None);
3912            let region = count_decode_work(&bytes, Some(roi));
3913
3914            assert!(
3915                region.code_blocks() > 0 && region.code_blocks() < full.code_blocks(),
3916                "ROI should decode fewer code-blocks for classic={classic}; full={}, region={}",
3917                full.code_blocks(),
3918                region.code_blocks()
3919            );
3920            assert!(
3921                region.idwt_output_samples > 0
3922                    && region.idwt_output_samples < full.idwt_output_samples,
3923                "ROI should produce fewer IDWT output samples for classic={classic}; full={}, region={}",
3924                full.idwt_output_samples,
3925                region.idwt_output_samples
3926            );
3927        }
3928    }
3929
3930    #[test]
3931    fn region_decode_reuses_region_sized_component_storage() {
3932        let bytes = fixture();
3933        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3934        let mut context = DecoderContext::default();
3935
3936        let bitmap = image
3937            .decode_region_with_context((1, 0, 1, 2), &mut context)
3938            .expect("region decode");
3939
3940        assert_eq!((bitmap.width, bitmap.height), (1, 2));
3941        assert!(context
3942            .tile_decode_context
3943            .channel_data
3944            .iter()
3945            .all(|component| component.container.truncated().len() == 2));
3946    }
3947
3948    #[test]
3949    fn native_region_decode_reuses_region_sized_component_storage() {
3950        let bytes = fixture();
3951        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3952        let mut context = DecoderContext::default();
3953
3954        let bitmap = image
3955            .decode_native_region_with_context((1, 0, 1, 2), &mut context)
3956            .expect("native region decode");
3957
3958        assert_eq!((bitmap.width, bitmap.height), (1, 2));
3959        assert!(context
3960            .tile_decode_context
3961            .channel_data
3962            .iter()
3963            .all(|component| component.container.truncated().len() == 2));
3964    }
3965
3966    #[test]
3967    fn decoder_context_defaults_to_auto_cpu_parallelism() {
3968        let context = DecoderContext::default();
3969
3970        assert_eq!(context.cpu_decode_parallelism(), CpuDecodeParallelism::Auto);
3971    }
3972
3973    #[test]
3974    fn classic_j2k_auto_and_serial_cpu_parallelism_match_pixels() {
3975        let bytes = fixture_multi_block();
3976        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3977        let mut auto_context = DecoderContext::default();
3978        let mut serial_context = DecoderContext::default();
3979        serial_context.set_cpu_decode_parallelism(CpuDecodeParallelism::Serial);
3980
3981        let auto = image
3982            .decode_with_context(&mut auto_context)
3983            .expect("auto decode");
3984        let serial = image
3985            .decode_with_context(&mut serial_context)
3986            .expect("serial decode");
3987
3988        assert_eq!(auto.data, serial.data);
3989    }
3990
3991    #[test]
3992    fn htj2k_97_auto_and_serial_cpu_parallelism_match_pixels() {
3993        let width = 128_u32;
3994        let height = 128_u32;
3995        let pixels = (0..width * height)
3996            .map(|idx| ((idx * 17 + idx / width * 31) & 0xff) as u8)
3997            .collect::<Vec<_>>();
3998        let bytes = encode_htj2k(
3999            &pixels,
4000            width,
4001            height,
4002            1,
4003            8,
4004            false,
4005            &EncodeOptions {
4006                reversible: false,
4007                guard_bits: 2,
4008                num_decomposition_levels: 5,
4009                ..EncodeOptions::default()
4010            },
4011        )
4012        .expect("encode HTJ2K 9/7");
4013        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4014        let mut auto_context = DecoderContext::default();
4015        let mut serial_context = DecoderContext::default();
4016        serial_context.set_cpu_decode_parallelism(CpuDecodeParallelism::Serial);
4017
4018        let auto = image
4019            .decode_with_context(&mut auto_context)
4020            .expect("auto decode");
4021        let serial = image
4022            .decode_with_context(&mut serial_context)
4023            .expect("serial decode");
4024
4025        assert_eq!(auto.data, serial.data);
4026    }
4027
4028    #[test]
4029    fn serial_cpu_parallelism_disables_classic_sub_band_parallel_branch() {
4030        assert!(!j2c::should_decode_classic_sub_band_in_parallel(
4031            CpuDecodeParallelism::Serial,
4032            16
4033        ));
4034    }
4035
4036    #[test]
4037    fn grayscale_direct_plan_is_built_without_materializing_channel_data() {
4038        let bytes = fixture_gray();
4039        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4040        let mut context = DecoderContext::default();
4041
4042        let plan = image
4043            .build_direct_grayscale_plan_with_context(&mut context)
4044            .expect("build direct plan");
4045
4046        assert_eq!(plan.dimensions, (4, 4));
4047        assert_eq!(plan.bit_depth, 8);
4048        assert!(
4049            !plan.steps.is_empty(),
4050            "direct plan must contain executable steps"
4051        );
4052        assert!(
4053            plan.steps.iter().any(|step| matches!(
4054                step,
4055                J2kDirectGrayscaleStep::ClassicSubBand(plan) if !plan.jobs.is_empty()
4056            )),
4057            "classic J2K direct plan must contain at least one non-empty classic sub-band job"
4058        );
4059        assert!(
4060            context.tile_decode_context.channel_data.is_empty(),
4061            "building a direct plan must not materialize host component planes"
4062        );
4063    }
4064
4065    #[test]
4066    fn grayscale_direct_plan_honors_target_resolution() {
4067        let bytes = fixture_ht_gray();
4068        let image = Image::new(
4069            &bytes,
4070            &DecodeSettings {
4071                target_resolution: Some((2, 2)),
4072                ..DecodeSettings::default()
4073            },
4074        )
4075        .expect("scaled image");
4076        let mut context = DecoderContext::default();
4077
4078        let plan = image
4079            .build_direct_grayscale_plan_with_context(&mut context)
4080            .expect("build scaled direct plan");
4081
4082        assert_eq!(plan.dimensions, (2, 2));
4083        assert!(plan.steps.iter().any(|step| matches!(
4084            step,
4085            J2kDirectGrayscaleStep::HtSubBand(plan) if !plan.jobs.is_empty()
4086        )));
4087        assert!(plan.steps.iter().any(|step| matches!(
4088            step,
4089            J2kDirectGrayscaleStep::Store(store)
4090                if store.output_width == 2 && store.output_height == 2
4091        )));
4092        assert!(
4093            context.tile_decode_context.channel_data.is_empty(),
4094            "building a scaled direct plan must not materialize host component planes"
4095        );
4096    }
4097
4098    #[test]
4099    fn grayscale_direct_plan_region_prunes_unneeded_ht_code_blocks() {
4100        let bytes = fixture_ht_multi_block();
4101        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4102        let mut full_context = DecoderContext::default();
4103        let mut roi_context = DecoderContext::default();
4104
4105        let full = image
4106            .build_direct_grayscale_plan_with_context(&mut full_context)
4107            .expect("build full direct plan");
4108        let roi = image
4109            .build_direct_grayscale_plan_region_with_context(&mut roi_context, (0, 0, 2, 2))
4110            .expect("build ROI direct plan");
4111
4112        let full_jobs = direct_ht_job_count(&full);
4113        let roi_jobs = direct_ht_job_count(&roi);
4114        assert!(full_jobs > 1, "fixture must expose multiple HT jobs");
4115        assert!(
4116            roi_jobs < full_jobs,
4117            "ROI direct plan must prune HT jobs before device preparation"
4118        );
4119    }
4120
4121    #[test]
4122    fn color_direct_plan_region_prunes_unneeded_ht_code_blocks() {
4123        let bytes = fixture_ht_rgb_multi_block();
4124        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4125        let mut full_context = DecoderContext::default();
4126        let mut roi_context = DecoderContext::default();
4127
4128        let full = image
4129            .build_direct_color_plan_with_context(&mut full_context)
4130            .expect("build full RGB direct plan");
4131        let roi = image
4132            .build_direct_color_plan_region_with_context(&mut roi_context, (0, 0, 2, 2))
4133            .expect("build ROI RGB direct plan");
4134
4135        let full_jobs = direct_color_ht_job_count(&full);
4136        let roi_jobs = direct_color_ht_job_count(&roi);
4137        assert!(full_jobs > 3, "fixture must expose multiple RGB HT jobs");
4138        assert!(
4139            roi_jobs < full_jobs,
4140            "RGB ROI direct plan must prune HT jobs before device preparation"
4141        );
4142    }
4143
4144    #[test]
4145    fn color_direct_plan_honors_target_resolution() {
4146        for (name, bytes) in [
4147            ("classic", {
4148                let pixels = gradient_pixels(8, 8, 3);
4149                let options = EncodeOptions {
4150                    reversible: true,
4151                    num_decomposition_levels: 2,
4152                    ..EncodeOptions::default()
4153                };
4154                encode(&pixels, 8, 8, 3, 8, false, &options).expect("encode classic rgb8")
4155            }),
4156            ("htj2k", {
4157                let pixels = gradient_pixels(8, 8, 3);
4158                let options = EncodeOptions {
4159                    reversible: true,
4160                    num_decomposition_levels: 2,
4161                    ..EncodeOptions::default()
4162                };
4163                encode_htj2k(&pixels, 8, 8, 3, 8, false, &options).expect("encode ht rgb8")
4164            }),
4165        ] {
4166            let image = Image::new(
4167                &bytes,
4168                &DecodeSettings {
4169                    target_resolution: Some((4, 4)),
4170                    ..DecodeSettings::default()
4171                },
4172            )
4173            .expect("scaled RGB image");
4174            let mut context = DecoderContext::default();
4175
4176            let plan = image
4177                .build_direct_color_plan_with_context(&mut context)
4178                .expect("build scaled direct color plan");
4179
4180            assert_eq!(plan.dimensions, (4, 4), "{name}: output dimensions");
4181            assert_eq!(plan.component_plans.len(), 3, "{name}: component count");
4182            for component_plan in &plan.component_plans {
4183                assert_eq!(component_plan.dimensions, (4, 4), "{name}: component dims");
4184                assert!(component_plan.steps.iter().any(|step| matches!(
4185                    step,
4186                    J2kDirectGrayscaleStep::Store(store)
4187                        if store.output_width == 4 && store.output_height == 4
4188                )));
4189            }
4190            assert!(
4191                context.tile_decode_context.channel_data.is_empty(),
4192                "{name}: building a scaled color direct plan must not materialize host component planes"
4193            );
4194        }
4195    }
4196
4197    #[test]
4198    fn direct_color_cpu_rgb8_executor_matches_scaled_region_decode() {
4199        for (name, bytes) in [
4200            ("classic", {
4201                let pixels = gradient_pixels(16, 16, 3);
4202                let options = EncodeOptions {
4203                    reversible: true,
4204                    num_decomposition_levels: 2,
4205                    ..EncodeOptions::default()
4206                };
4207                encode(&pixels, 16, 16, 3, 8, false, &options).expect("encode classic rgb8")
4208            }),
4209            ("htj2k", {
4210                let pixels = gradient_pixels(16, 16, 3);
4211                let options = EncodeOptions {
4212                    reversible: true,
4213                    num_decomposition_levels: 2,
4214                    ..EncodeOptions::default()
4215                };
4216                encode_htj2k(&pixels, 16, 16, 3, 8, false, &options).expect("encode ht rgb8")
4217            }),
4218        ] {
4219            let image = Image::new(
4220                &bytes,
4221                &DecodeSettings {
4222                    target_resolution: Some((4, 4)),
4223                    ..DecodeSettings::default()
4224                },
4225            )
4226            .expect("scaled RGB image");
4227            let mut expected_context = DecoderContext::default();
4228            let expected_full = image
4229                .decode_with_context(&mut expected_context)
4230                .expect("decode scaled reference");
4231            let output_region = J2kRect {
4232                x0: 1,
4233                y0: 1,
4234                x1: 3,
4235                y1: 3,
4236            };
4237            let mut direct_context = DecoderContext::default();
4238            let plan = image
4239                .build_direct_color_plan_region_with_context(
4240                    &mut direct_context,
4241                    (
4242                        output_region.x0,
4243                        output_region.y0,
4244                        output_region.width(),
4245                        output_region.height(),
4246                    ),
4247                )
4248                .expect("build direct RGB region plan");
4249
4250            let stride = output_region.width() as usize * 3;
4251            let mut direct = vec![0_u8; stride * output_region.height() as usize];
4252            let mut scratch = J2kDirectCpuScratch::new();
4253            execute_direct_color_plan_rgb8_into(
4254                &plan,
4255                output_region,
4256                &mut scratch,
4257                &mut direct,
4258                stride,
4259            )
4260            .expect("execute direct RGB plan");
4261
4262            let mut expected = Vec::with_capacity(direct.len());
4263            let full_stride = image.width() as usize * 3;
4264            for y in output_region.y0..output_region.y1 {
4265                let start = y as usize * full_stride + output_region.x0 as usize * 3;
4266                expected.extend_from_slice(&expected_full.data[start..start + stride]);
4267            }
4268
4269            assert_eq!(direct, expected, "{name}: direct RGB output");
4270
4271            let rgba_stride = output_region.width() as usize * 4;
4272            let mut direct_rgba = vec![0_u8; rgba_stride * output_region.height() as usize];
4273            execute_direct_color_plan_rgba8_into(
4274                &plan,
4275                output_region,
4276                &mut scratch,
4277                &mut direct_rgba,
4278                rgba_stride,
4279            )
4280            .expect("execute direct RGBA plan");
4281
4282            let mut expected_rgba = Vec::with_capacity(direct_rgba.len());
4283            for rgb in expected.chunks_exact(3) {
4284                expected_rgba.extend_from_slice(rgb);
4285                expected_rgba.push(255);
4286            }
4287            assert_eq!(direct_rgba, expected_rgba, "{name}: direct RGBA output");
4288        }
4289    }
4290
4291    #[test]
4292    fn htj2k_grayscale_direct_plan_contains_ht_sub_band_steps() {
4293        let bytes = fixture_ht_gray();
4294        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4295        let mut context = DecoderContext::default();
4296
4297        let plan = image
4298            .build_direct_grayscale_plan_with_context(&mut context)
4299            .expect("build direct plan");
4300
4301        assert!(
4302            plan.steps.iter().any(|step| matches!(
4303                step,
4304                J2kDirectGrayscaleStep::HtSubBand(plan) if !plan.jobs.is_empty()
4305            )),
4306            "HTJ2K direct plan must contain at least one non-empty HT sub-band decode step"
4307        );
4308    }
4309
4310    #[test]
4311    fn ht_decoder_hook_is_used_for_htj2k_codeblocks() {
4312        let pixels: Vec<u8> = (0..16).collect();
4313        let options = EncodeOptions {
4314            reversible: true,
4315            num_decomposition_levels: 1,
4316            ..EncodeOptions::default()
4317        };
4318        let bytes = encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode ht");
4319        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4320        let mut hooked_context = DecoderContext::default();
4321        let mut hook = FailingHtDecoder { called: false };
4322        let error = match image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) {
4323            Ok(_) => panic!("hooked decode must use external HT decoder"),
4324            Err(error) => error,
4325        };
4326
4327        assert!(hook.called, "HT decoder hook must be invoked");
4328        assert_eq!(
4329            error,
4330            DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
4331        );
4332    }
4333
4334    #[test]
4335    fn openhtj2k_conformance_fixture_exercises_refinement_passes() {
4336        for fixture in [
4337            (
4338                "ds0_ht_12_b11",
4339                fixture_openhtj2k_ht_refinement(),
4340                fixture_openhtj2k_ht_refinement_pixels(),
4341                (3, 5),
4342                8,
4343                2,
4344                4,
4345            ),
4346            (
4347                "ds0_ht_09_b11",
4348                fixture_openhtj2k_ht_refinement_odd(),
4349                fixture_openhtj2k_ht_refinement_odd_pixels(),
4350                (17, 37),
4351                14,
4352                14,
4353                629,
4354            ),
4355        ] {
4356            let (
4357                name,
4358                codestream,
4359                expected_pixels,
4360                dimensions,
4361                blocks,
4362                refinement_jobs,
4363                zero_diffs,
4364            ) = fixture;
4365            let image = Image::new(codestream, &DecodeSettings::default()).expect("image");
4366            let mut context = DecoderContext::default();
4367            let mut hook = CapturingHtDecoder::default();
4368
4369            let components = image
4370                .decode_components_with_ht_decoder(&mut context, &mut hook)
4371                .expect("decode OpenHTJ2K HTJ2K fixture");
4372
4373            assert!(
4374                hook.called,
4375                "{name}: HTJ2K fixture must use HT code-block decode"
4376            );
4377            assert!(
4378                hook.refinement_jobs > 0,
4379                "{name}: OpenHTJ2K fixture must contain non-empty refinement segments"
4380            );
4381            assert!(
4382                hook.max_coding_passes > 1,
4383                "{name}: OpenHTJ2K fixture must exercise more than the cleanup pass"
4384            );
4385            assert_eq!(hook.blocks, blocks, "{name}: HT code-block count");
4386            assert_eq!(
4387                hook.refinement_jobs, refinement_jobs,
4388                "{name}: refinement job count"
4389            );
4390            assert_eq!(hook.max_coding_passes, 3, "{name}: max HT coding passes");
4391            assert_eq!(components.dimensions(), dimensions, "{name}: dimensions");
4392            assert_eq!(components.planes().len(), 1, "{name}: component planes");
4393
4394            let decoded: Vec<u8> = components.planes()[0]
4395                .samples()
4396                .iter()
4397                .map(|sample| sample.round().clamp(0.0, 255.0) as u8)
4398                .collect();
4399            assert_eq!(decoded, expected_pixels, "{name}: decoded pixels");
4400
4401            let mut zero_context = DecoderContext::default();
4402            let mut zero_hook = ZeroRefinementHtDecoder;
4403            let zeroed_components = image
4404                .decode_components_with_ht_decoder(&mut zero_context, &mut zero_hook)
4405                .expect("decode OpenHTJ2K fixture with zeroed refinement bytes");
4406            let actual_zero_diffs = components.planes()[0]
4407                .samples()
4408                .iter()
4409                .zip(zeroed_components.planes()[0].samples())
4410                .filter(|(actual, zeroed)| (*actual - *zeroed).abs() > f32::EPSILON)
4411                .count();
4412            assert_eq!(
4413                actual_zero_diffs, zero_diffs,
4414                "{name}: zeroing refinement bytes must change decoded samples"
4415            );
4416        }
4417    }
4418
4419    #[test]
4420    fn openhtj2k_refinement_phase_limited_decode_differs_and_records_ht_stats() {
4421        let image = Image::new(
4422            fixture_openhtj2k_ht_refinement_odd(),
4423            &DecodeSettings::default(),
4424        )
4425        .expect("image");
4426        let mut full_context = DecoderContext::default();
4427
4428        let (full_samples, full_decoded) = {
4429            let full_components = image
4430                .decode_components_with_context(&mut full_context)
4431                .expect("full native decode of OpenHTJ2K refinement fixture");
4432            let full_samples = full_components.planes()[0].samples().to_vec();
4433            let full_decoded: Vec<u8> = full_samples
4434                .iter()
4435                .map(|sample| sample.round().clamp(0.0, 255.0) as u8)
4436                .collect();
4437            (full_samples, full_decoded)
4438        };
4439        assert_eq!(
4440            full_decoded,
4441            fixture_openhtj2k_ht_refinement_odd_pixels(),
4442            "full decode must match the checked-in OpenHTJ2K oracle"
4443        );
4444
4445        let stats = full_context
4446            .tile_decode_context
4447            .debug_counters
4448            .ht_phase_stats;
4449        assert_eq!(stats.blocks, 14, "HT block count");
4450        assert_eq!(stats.refinement_blocks, 14, "HT refinement block count");
4451        assert!(stats.cleanup_bytes > 0, "cleanup byte total");
4452        assert!(stats.refinement_bytes > 0, "refinement byte total");
4453
4454        let mut cleanup_context = DecoderContext::default();
4455        let mut cleanup_hook = CleanupLimitedHtDecoder::default();
4456        let cleanup_components = image
4457            .decode_components_with_ht_decoder(&mut cleanup_context, &mut cleanup_hook)
4458            .expect("cleanup-limited decode of OpenHTJ2K refinement fixture");
4459        let cleanup_decoded: Vec<u8> = cleanup_components.planes()[0]
4460            .samples()
4461            .iter()
4462            .map(|sample| sample.round().clamp(0.0, 255.0) as u8)
4463            .collect();
4464        let cleanup_sample_diffs = full_samples
4465            .iter()
4466            .zip(cleanup_components.planes()[0].samples())
4467            .filter(|(full, cleanup)| (*full - *cleanup).abs() > f32::EPSILON)
4468            .count();
4469
4470        assert!(
4471            cleanup_sample_diffs > 0,
4472            "cleanup-limited decode must omit refinement effects"
4473        );
4474        assert_eq!(
4475            cleanup_decoded, full_decoded,
4476            "fixture refinement differences are below final u8 clamping"
4477        );
4478        assert_eq!(cleanup_hook.blocks, 14, "hook HT block count");
4479        assert_eq!(
4480            cleanup_hook.refinement_blocks, 14,
4481            "hook HT refinement block count"
4482        );
4483        assert!(cleanup_hook.cleanup_bytes > 0, "hook cleanup byte total");
4484        assert!(
4485            cleanup_hook.refinement_bytes > 0,
4486            "hook refinement byte total"
4487        );
4488    }
4489
4490    #[test]
4491    fn scalar_htj2k_encoder_contract_is_cleanup_only() {
4492        let coefficients = (0..64)
4493            .map(|index| {
4494                let magnitude = (index % 7) + 1;
4495                if index % 2 == 0 {
4496                    magnitude
4497                } else {
4498                    -magnitude
4499                }
4500            })
4501            .collect::<Vec<_>>();
4502
4503        let encoded =
4504            encode_ht_code_block_scalar(&coefficients, 8, 8, 8).expect("encode HT code block");
4505
4506        assert_eq!(
4507            encoded.num_coding_passes, 1,
4508            "current scalar HTJ2K encoder emits only the cleanup pass"
4509        );
4510        assert_eq!(
4511            encoded.num_zero_bitplanes, 7,
4512            "current cleanup-only HTJ2K encoder includes one bitplane"
4513        );
4514        assert!(
4515            !encoded.data.is_empty(),
4516            "non-zero cleanup-only block must still produce payload bytes"
4517        );
4518    }
4519
4520    #[test]
4521    fn scalar_htj2k_decode_workspace_matches_fresh_decode_and_reuses_capacity() {
4522        let image = Image::new(
4523            fixture_openhtj2k_ht_refinement_odd(),
4524            &DecodeSettings::default(),
4525        )
4526        .expect("image");
4527        let mut context = DecoderContext::default();
4528        let mut hook = FirstHtJobDecoder::default();
4529        image
4530            .decode_components_with_ht_decoder(&mut context, &mut hook)
4531            .expect("decode fixture while collecting HT jobs");
4532        let job = hook
4533            .job
4534            .as_ref()
4535            .expect("fixture must expose an HT decode job")
4536            .borrowed();
4537        let mut fresh = vec![0.0_f32; job.width as usize * job.height as usize];
4538        let mut reused = vec![0.0_f32; fresh.len()];
4539        let mut profiled = vec![0.0_f32; fresh.len()];
4540        let mut workspace = HtCodeBlockDecodeWorkspace::default();
4541        let mut profile = HtCodeBlockDecodeProfile::default();
4542
4543        decode_ht_code_block_scalar(job, &mut fresh).expect("fresh HT decode");
4544        decode_ht_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
4545            .expect("workspace HT decode");
4546        let first_capacity = workspace.coefficient_capacity();
4547        decode_ht_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
4548            .expect("second workspace HT decode");
4549        decode_ht_code_block_scalar_with_workspace_profiled(
4550            job,
4551            &mut profiled,
4552            &mut workspace,
4553            &mut profile,
4554        )
4555        .expect("profiled workspace HT decode");
4556
4557        assert_eq!(reused, fresh);
4558        assert_eq!(profiled, fresh);
4559        assert!(first_capacity >= fresh.len());
4560        assert_eq!(workspace.coefficient_capacity(), first_capacity);
4561        assert_eq!(profile.blocks, 1);
4562        assert!(profile.cleanup_bytes > 0);
4563    }
4564
4565    #[test]
4566    fn classic_decoder_hook_is_used_for_j2k_codeblocks() {
4567        let bytes = fixture();
4568        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4569        let mut hooked_context = DecoderContext::default();
4570        let mut hook = FailingClassicDecoder { called: false };
4571        let error = match image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) {
4572            Ok(_) => panic!("hooked decode must use external classic decoder"),
4573            Err(error) => error,
4574        };
4575
4576        assert!(hook.called, "classic decoder hook must be invoked");
4577        assert_eq!(
4578            error,
4579            DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
4580        );
4581    }
4582
4583    #[test]
4584    fn classic_sub_band_decoder_hook_is_used_for_j2k_codeblocks() {
4585        let bytes = fixture_multi_block();
4586        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4587        let mut hooked_context = DecoderContext::default();
4588        let mut hook = FailingClassicBatchDecoder { called: false };
4589        let error = match image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) {
4590            Ok(_) => panic!("hooked decode must use external classic batch decoder"),
4591            Err(error) => error,
4592        };
4593
4594        assert!(hook.called, "classic sub-band decoder hook must be invoked");
4595        assert_eq!(
4596            error,
4597            DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
4598        );
4599    }
4600
4601    // -----------------------------------------------------------------------
4602    // Sanity tests for the four scalar-reference exports
4603    // -----------------------------------------------------------------------
4604
4605    #[test]
4606    fn forward_dwt53_reference_matches_internal_path() {
4607        // 4×4 constant-ramp input; 1 decomposition level.
4608        let samples: Vec<f32> = (0..16).map(|i| i as f32).collect();
4609        let out = forward_dwt53_reference(&samples, 4, 4, 1);
4610
4611        // Internal path
4612        let internal = j2c::fdwt::forward_dwt(&samples, 4, 4, 1, true);
4613
4614        assert_eq!(out.ll, internal.ll, "LL subband mismatch");
4615        assert_eq!(out.ll_width, internal.ll_width, "LL width mismatch");
4616        assert_eq!(out.ll_height, internal.ll_height, "LL height mismatch");
4617        assert_eq!(out.levels.len(), internal.levels.len(), "level count");
4618        for (pub_lvl, int_lvl) in out.levels.iter().zip(internal.levels.iter()) {
4619            assert_eq!(pub_lvl.hl, int_lvl.hl, "HL mismatch");
4620            assert_eq!(pub_lvl.lh, int_lvl.lh, "LH mismatch");
4621            assert_eq!(pub_lvl.hh, int_lvl.hh, "HH mismatch");
4622        }
4623    }
4624
4625    #[test]
4626    fn forward_rct_reference_matches_internal_path() {
4627        // Single pixel: R=100, G=150, B=200
4628        let planes = vec![vec![100.0f32], vec![150.0f32], vec![200.0f32]];
4629        let result = forward_rct_reference(planes.clone());
4630
4631        // Internal path
4632        let mut internal = planes;
4633        j2c::forward_mct::forward_rct(&mut internal);
4634
4635        assert_eq!(result, internal, "RCT output mismatch");
4636        // Y = floor((100 + 300 + 200) / 4) = 150
4637        assert_eq!(result[0][0], 150.0, "Y component");
4638        assert_eq!(result[1][0], 50.0, "Cb component");
4639        assert_eq!(result[2][0], -50.0, "Cr component");
4640    }
4641
4642    #[test]
4643    fn forward_ict_reference_matches_internal_path() {
4644        let planes = vec![vec![100.0f32], vec![150.0f32], vec![200.0f32]];
4645        let result = forward_ict_reference(planes.clone());
4646
4647        let mut internal = planes;
4648        j2c::forward_mct::forward_ict(&mut internal);
4649
4650        assert_eq!(result, internal, "ICT output mismatch");
4651    }
4652
4653    #[test]
4654    fn forward_dwt97_reference_matches_internal_path() {
4655        let samples = (0..64)
4656            .map(|idx| {
4657                f32::from(u8::try_from((idx * 19 + idx / 3) & 0xff).expect("masked sample fits u8"))
4658                    - 128.0
4659            })
4660            .collect::<Vec<_>>();
4661        let result = forward_dwt97_reference(&samples, 8, 8, 2);
4662        let internal = j2c::fdwt::forward_dwt(&samples, 8, 8, 2, false);
4663
4664        assert_eq!(result.ll, internal.ll, "DWT 9/7 LL mismatch");
4665        assert_eq!(result.ll_width, internal.ll_width);
4666        assert_eq!(result.ll_height, internal.ll_height);
4667        assert_eq!(result.levels.len(), internal.levels.len());
4668        for (actual, expected) in result.levels.iter().zip(internal.levels.iter()) {
4669            assert_eq!(actual.hl, expected.hl, "DWT 9/7 HL mismatch");
4670            assert_eq!(actual.lh, expected.lh, "DWT 9/7 LH mismatch");
4671            assert_eq!(actual.hh, expected.hh, "DWT 9/7 HH mismatch");
4672        }
4673    }
4674
4675    #[test]
4676    fn quantize_reversible_reference_matches_internal_path() {
4677        let coefficients = vec![3.7f32, -8.2, 0.5, -0.5, 10.0];
4678        let exponent = 8u16;
4679        let mantissa = 0u16;
4680        let range_bits = 8u8;
4681
4682        let result =
4683            quantize_reversible_reference(&coefficients, exponent, mantissa, range_bits, true);
4684
4685        // Internal path
4686        let step = j2c::quantize::QuantStepSize { exponent, mantissa };
4687        let internal = j2c::quantize::quantize_subband(&coefficients, &step, range_bits, true);
4688
4689        assert_eq!(result, internal, "quantize output mismatch");
4690        // reversible: round to nearest
4691        assert_eq!(result[0], 4, "3.7 rounds to 4");
4692        assert_eq!(result[1], -8, "-8.2 rounds to -8");
4693    }
4694
4695    #[test]
4696    fn quantize_subband_reference_matches_irreversible_internal_path() {
4697        let coefficients = vec![3.7f32, -8.2, 0.5, -0.5, 10.0];
4698        let exponent = 8u16;
4699        let mantissa = 256u16;
4700        let range_bits = 8u8;
4701
4702        let result =
4703            quantize_subband_reference(&coefficients, exponent, mantissa, range_bits, false);
4704
4705        let step = j2c::quantize::QuantStepSize { exponent, mantissa };
4706        let internal = j2c::quantize::quantize_subband(&coefficients, &step, range_bits, false);
4707
4708        assert_eq!(result, internal, "irreversible quantize output mismatch");
4709    }
4710
4711    #[test]
4712    fn deinterleave_reference_matches_internal_path() {
4713        // 2-pixel RGB8 unsigned: [R0,G0,B0, R1,G1,B1]
4714        let pixels: Vec<u8> = vec![128, 64, 200, 10, 20, 30];
4715        let result = deinterleave_reference(&pixels, 2, 3, 8, false);
4716
4717        let internal = j2c::encode::deinterleave_to_f32(&pixels, 2, 3, 8, false);
4718
4719        assert_eq!(result, internal, "deinterleave output mismatch");
4720        assert_eq!(result.len(), 3, "three component planes");
4721        assert_eq!(result[0].len(), 2, "two pixels per plane");
4722        // unsigned 8-bit with level shift: val - 128
4723        assert!((result[0][0] - 0.0f32).abs() < 1e-6, "R0 level-shifted");
4724        assert!((result[1][0] - (-64.0f32)).abs() < 1e-6, "G0 level-shifted");
4725        assert!((result[2][0] - 72.0f32).abs() < 1e-6, "B0 level-shifted");
4726    }
4727}