Skip to main content

signinum_j2k_native/
lib.rs

1/*!
2Internal pure-Rust JPEG 2000 codec engine for `signinum`.
3
4This module tree was imported from the `dicom-toolkit-jpeg2000` 0.5.0 crate
5and adapted in-repo so `signinum-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 signinum_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 signinum_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 RCT reference for CUDA stage parity.
970///
971/// Applies the native CPU forward Reversible Color Transform to three
972/// component planes supplied as owned `Vec<f32>` arrays.  The transform is
973/// applied in place and the mutated planes are returned, so callers do not
974/// need to pass a mutable slice.
975pub fn forward_rct_reference(mut planes: Vec<Vec<f32>>) -> Vec<Vec<f32>> {
976    j2c::forward_mct::forward_rct(&mut planes);
977    planes
978}
979
980/// Adapter scalar reversible sub-band quantization reference for CUDA stage parity.
981///
982/// Quantizes `coefficients` using the reversible (lossless) integer path of
983/// the native CPU quantizer.  `step_exponent` and `step_mantissa` encode the
984/// JPEG 2000 `QuantStepSize` for the sub-band; `range_bits` is the nominal
985/// bit depth for the sub-band.  When `reversible` is `true` the step-size
986/// parameters are ignored and each coefficient is rounded to the nearest
987/// integer.
988pub fn quantize_reversible_reference(
989    coefficients: &[f32],
990    step_exponent: u16,
991    step_mantissa: u16,
992    range_bits: u8,
993    reversible: bool,
994) -> Vec<i32> {
995    let step = j2c::quantize::QuantStepSize {
996        exponent: step_exponent,
997        mantissa: step_mantissa,
998    };
999    j2c::quantize::quantize_subband(coefficients, &step, range_bits, reversible)
1000}
1001
1002/// Adapter scalar pixel deinterleave/level-shift reference for CUDA stage parity.
1003///
1004/// Converts interleaved pixel bytes to per-component f32 planes with the
1005/// same level-shift logic as the native CPU encode path.  The result is one
1006/// `Vec<f32>` per component, each of length `num_pixels`.
1007pub fn deinterleave_reference(
1008    pixels: &[u8],
1009    num_pixels: usize,
1010    num_components: u8,
1011    bit_depth: u8,
1012    signed: bool,
1013) -> Vec<Vec<f32>> {
1014    j2c::encode::deinterleave_to_f32(pixels, num_pixels, num_components, bit_depth, signed)
1015}
1016
1017/// Adapter scalar Tier-2 packetization helper for backend experimentation.
1018pub fn encode_j2k_packetization_scalar(
1019    job: J2kPacketizationEncodeJob<'_>,
1020) -> core::result::Result<Vec<u8>, &'static str> {
1021    let mut resolutions = job
1022        .resolutions
1023        .iter()
1024        .map(|resolution| j2c::packet_encode::ResolutionPacket {
1025            subbands: resolution
1026                .subbands
1027                .iter()
1028                .map(|subband| j2c::packet_encode::SubbandPrecinct {
1029                    code_blocks: subband
1030                        .code_blocks
1031                        .iter()
1032                        .map(|code_block| j2c::packet_encode::CodeBlockPacketData {
1033                            data: code_block.data.to_vec(),
1034                            ht_cleanup_length: code_block.ht_cleanup_length,
1035                            ht_refinement_length: code_block.ht_refinement_length,
1036                            num_coding_passes: code_block.num_coding_passes,
1037                            classic_segment_lengths: Vec::new(),
1038                            num_zero_bitplanes: code_block.num_zero_bitplanes,
1039                            previously_included: code_block.previously_included,
1040                            l_block: code_block.l_block,
1041                            block_coding_mode: match code_block.block_coding_mode {
1042                                J2kPacketizationBlockCodingMode::Classic => {
1043                                    j2c::codestream_write::BlockCodingMode::Classic
1044                                }
1045                                J2kPacketizationBlockCodingMode::HighThroughput => {
1046                                    j2c::codestream_write::BlockCodingMode::HighThroughput
1047                                }
1048                            },
1049                        })
1050                        .collect(),
1051                    num_cbs_x: subband.num_cbs_x,
1052                    num_cbs_y: subband.num_cbs_y,
1053                })
1054                .collect(),
1055        })
1056        .collect::<Vec<_>>();
1057
1058    let descriptors = job
1059        .packet_descriptors
1060        .iter()
1061        .map(|descriptor| j2c::packet_encode::PacketDescriptor {
1062            packet_index: descriptor.packet_index,
1063            state_index: descriptor.state_index,
1064            layer: descriptor.layer,
1065            resolution: descriptor.resolution,
1066            component: descriptor.component,
1067            precinct: descriptor.precinct,
1068        })
1069        .collect::<Vec<_>>();
1070
1071    j2c::packet_encode::validate_ht_segment_lengths(&resolutions)?;
1072
1073    if descriptors.is_empty() {
1074        Ok(j2c::packet_encode::form_tile_bitstream_for_progression(
1075            &mut resolutions,
1076            job.num_layers,
1077            job.num_components,
1078            job.progression_order,
1079        ))
1080    } else {
1081        j2c::packet_encode::form_tile_bitstream_with_descriptors(&mut resolutions, &descriptors)
1082    }
1083}
1084
1085/// Adapter scalar classic J2K decoder helper for backend experimentation.
1086pub fn decode_j2k_code_block_scalar(
1087    job: J2kCodeBlockDecodeJob<'_>,
1088    output: &mut [f32],
1089) -> Result<()> {
1090    let mut workspace = J2kCodeBlockDecodeWorkspace::default();
1091    decode_j2k_code_block_scalar_with_workspace(job, output, &mut workspace)
1092}
1093
1094/// Reusable scratch for scalar classic J2K code-block decoding.
1095#[derive(Default)]
1096pub struct J2kCodeBlockDecodeWorkspace {
1097    bit_plane_decode_context: j2c::bitplane::BitPlaneDecodeContext,
1098}
1099
1100/// Adapter scalar classic J2K decoder helper that reuses caller-provided scratch.
1101pub fn decode_j2k_code_block_scalar_with_workspace(
1102    job: J2kCodeBlockDecodeJob<'_>,
1103    output: &mut [f32],
1104    workspace: &mut J2kCodeBlockDecodeWorkspace,
1105) -> Result<()> {
1106    let required_len = if job.height == 0 {
1107        0
1108    } else {
1109        job.output_stride
1110            .checked_mul(job.height as usize - 1)
1111            .and_then(|prefix| prefix.checked_add(job.width as usize))
1112            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1113    };
1114    if output.len() < required_len {
1115        bail!(DecodingError::CodeBlockDecodeFailure);
1116    }
1117
1118    let style = internal_j2k_code_block_style(job.style);
1119    let sub_band_type = internal_j2k_sub_band_type(job.sub_band_type);
1120    let code_block_stride =
1121        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1122    let coded_bitplanes = add_roi_shift_to_bitplanes(
1123        job.total_bitplanes,
1124        job.roi_shift,
1125        MAX_CLASSIC_DECODE_BITPLANES,
1126    )?;
1127
1128    j2c::bitplane::decode_code_block_segments_validated(
1129        job.data,
1130        job.segments,
1131        job.width,
1132        job.height,
1133        job.missing_bit_planes,
1134        job.number_of_coding_passes,
1135        coded_bitplanes,
1136        sub_band_type,
1137        &style,
1138        job.strict,
1139        &mut workspace.bit_plane_decode_context,
1140    )?;
1141
1142    for (row_idx, coeff_row) in workspace
1143        .bit_plane_decode_context
1144        .coefficient_rows()
1145        .enumerate()
1146        .take(job.height as usize)
1147    {
1148        let row_start = row_idx * job.output_stride;
1149        let output_row = &mut output[row_start..row_start + code_block_stride];
1150        for (coefficient, sample) in coeff_row.iter().zip(output_row.iter_mut()) {
1151            let coefficient = apply_roi_maxshift_inverse_i32(coefficient.get(), job.roi_shift);
1152            *sample = coefficient as f32 * job.dequantization_step;
1153        }
1154    }
1155
1156    Ok(())
1157}
1158
1159/// Adapter scalar classic J2K pass timings for backend experimentation.
1160#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
1161pub struct J2kCodeBlockDecodeProfile {
1162    /// Significance propagation pass elapsed time in microseconds.
1163    pub sigprop_us: u128,
1164    /// Magnitude refinement pass elapsed time in microseconds.
1165    pub magref_us: u128,
1166    /// Cleanup pass elapsed time in microseconds.
1167    pub cleanup_us: u128,
1168    /// Raw bypass pass elapsed time in microseconds.
1169    pub bypass_us: u128,
1170    /// Coefficient output conversion elapsed time in microseconds.
1171    pub output_convert_us: u128,
1172}
1173
1174impl J2kCodeBlockDecodeProfile {
1175    fn add_native_stats(&mut self, stats: j2c::bitplane::J2kBlockDecodeStats) {
1176        self.sigprop_us += stats.sigprop_us;
1177        self.magref_us += stats.magref_us;
1178        self.cleanup_us += stats.cleanup_us;
1179        self.bypass_us += stats.bypass_us;
1180    }
1181}
1182
1183/// Adapter scalar classic J2K decoder helper that records pass timings.
1184pub fn decode_j2k_code_block_scalar_profiled(
1185    job: J2kCodeBlockDecodeJob<'_>,
1186    output: &mut [f32],
1187    profile: &mut J2kCodeBlockDecodeProfile,
1188) -> Result<()> {
1189    let mut workspace = J2kCodeBlockDecodeWorkspace::default();
1190    decode_j2k_code_block_scalar_with_workspace_profiled(job, output, &mut workspace, profile)
1191}
1192
1193/// Adapter scalar classic J2K decoder helper that records pass timings and reuses scratch.
1194pub fn decode_j2k_code_block_scalar_with_workspace_profiled(
1195    job: J2kCodeBlockDecodeJob<'_>,
1196    output: &mut [f32],
1197    workspace: &mut J2kCodeBlockDecodeWorkspace,
1198    profile: &mut J2kCodeBlockDecodeProfile,
1199) -> Result<()> {
1200    let required_len = if job.height == 0 {
1201        0
1202    } else {
1203        job.output_stride
1204            .checked_mul(job.height as usize - 1)
1205            .and_then(|prefix| prefix.checked_add(job.width as usize))
1206            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1207    };
1208    if output.len() < required_len {
1209        bail!(DecodingError::CodeBlockDecodeFailure);
1210    }
1211
1212    let style = internal_j2k_code_block_style(job.style);
1213    let sub_band_type = internal_j2k_sub_band_type(job.sub_band_type);
1214    let code_block_stride =
1215        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1216    let coded_bitplanes = add_roi_shift_to_bitplanes(
1217        job.total_bitplanes,
1218        job.roi_shift,
1219        MAX_CLASSIC_DECODE_BITPLANES,
1220    )?;
1221    let mut stats = j2c::bitplane::J2kBlockDecodeStats::default();
1222
1223    j2c::bitplane::decode_code_block_segments_validated_profiled(
1224        job.data,
1225        job.segments,
1226        job.width,
1227        job.height,
1228        job.missing_bit_planes,
1229        job.number_of_coding_passes,
1230        coded_bitplanes,
1231        sub_band_type,
1232        &style,
1233        job.strict,
1234        &mut workspace.bit_plane_decode_context,
1235        &mut stats,
1236        true,
1237    )?;
1238    profile.add_native_stats(stats);
1239
1240    let output_convert_started = profile::profile_now(true);
1241    for (row_idx, coeff_row) in workspace
1242        .bit_plane_decode_context
1243        .coefficient_rows()
1244        .enumerate()
1245        .take(job.height as usize)
1246    {
1247        let row_start = row_idx * job.output_stride;
1248        let output_row = &mut output[row_start..row_start + code_block_stride];
1249        for (coefficient, sample) in coeff_row.iter().zip(output_row.iter_mut()) {
1250            let coefficient = apply_roi_maxshift_inverse_i32(coefficient.get(), job.roi_shift);
1251            *sample = coefficient as f32 * job.dequantization_step;
1252        }
1253    }
1254    profile.output_convert_us += profile::elapsed_us(output_convert_started);
1255
1256    Ok(())
1257}
1258
1259/// Adapter scalar classic J2K batched decoder helper for backend experimentation.
1260pub fn decode_j2k_sub_band_scalar(job: J2kSubBandDecodeJob<'_>, output: &mut [f32]) -> Result<()> {
1261    let required_len = if job.height == 0 {
1262        0
1263    } else {
1264        usize::try_from(job.width)
1265            .ok()
1266            .and_then(|width| width.checked_mul(job.height as usize))
1267            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1268    };
1269    if output.len() < required_len {
1270        bail!(DecodingError::CodeBlockDecodeFailure);
1271    }
1272
1273    let sub_band_width =
1274        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1275
1276    for batch_job in job.jobs {
1277        let code_block = batch_job.code_block;
1278        if code_block.output_stride != sub_band_width {
1279            bail!(DecodingError::CodeBlockDecodeFailure);
1280        }
1281        if batch_job
1282            .output_x
1283            .checked_add(code_block.width)
1284            .is_none_or(|x1| x1 > job.width)
1285            || batch_job
1286                .output_y
1287                .checked_add(code_block.height)
1288                .is_none_or(|y1| y1 > job.height)
1289        {
1290            bail!(DecodingError::CodeBlockDecodeFailure);
1291        }
1292
1293        let base_idx = usize::try_from(batch_job.output_y)
1294            .ok()
1295            .and_then(|y| y.checked_mul(sub_band_width))
1296            .and_then(|row| row.checked_add(batch_job.output_x as usize))
1297            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1298        let block_output_len = if code_block.height == 0 {
1299            0
1300        } else {
1301            code_block
1302                .output_stride
1303                .checked_mul(code_block.height as usize - 1)
1304                .and_then(|prefix| prefix.checked_add(code_block.width as usize))
1305                .ok_or(DecodingError::CodeBlockDecodeFailure)?
1306        };
1307        let end_idx = base_idx
1308            .checked_add(block_output_len)
1309            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1310        if end_idx > output.len() {
1311            bail!(DecodingError::CodeBlockDecodeFailure);
1312        }
1313
1314        decode_j2k_code_block_scalar(code_block, &mut output[base_idx..end_idx])?;
1315    }
1316
1317    Ok(())
1318}
1319
1320/// Adapter scalar HTJ2K decoder helper for backend experimentation.
1321pub fn decode_ht_code_block_scalar(
1322    job: HtCodeBlockDecodeJob<'_>,
1323    output: &mut [f32],
1324) -> Result<()> {
1325    decode_ht_code_block_scalar_for_phase::<{ j2c::ht_block_decode::PHASE_LIMIT_MAGREF }>(
1326        job, output,
1327    )
1328}
1329
1330/// Adapter scalar HTJ2K decoder helper that stops after the selected phase.
1331pub fn decode_ht_code_block_scalar_until_phase(
1332    job: HtCodeBlockDecodeJob<'_>,
1333    output: &mut [f32],
1334    phase_limit: HtCodeBlockDecodePhaseLimit,
1335) -> Result<()> {
1336    match phase_limit {
1337        HtCodeBlockDecodePhaseLimit::Cleanup => decode_ht_code_block_scalar_for_phase::<
1338            { j2c::ht_block_decode::PHASE_LIMIT_CLEANUP },
1339        >(job, output),
1340        HtCodeBlockDecodePhaseLimit::SignificancePropagation => {
1341            decode_ht_code_block_scalar_for_phase::<{ j2c::ht_block_decode::PHASE_LIMIT_SIGPROP }>(
1342                job, output,
1343            )
1344        }
1345        HtCodeBlockDecodePhaseLimit::MagnitudeRefinement => {
1346            decode_ht_code_block_scalar_for_phase::<{ j2c::ht_block_decode::PHASE_LIMIT_MAGREF }>(
1347                job, output,
1348            )
1349        }
1350    }
1351}
1352
1353/// Adapter reusable scalar HTJ2K decode workspace for backend experimentation.
1354#[derive(Default)]
1355pub struct HtCodeBlockDecodeWorkspace {
1356    coefficients: Vec<u32>,
1357    scratch: j2c::ht_block_decode::HtBlockDecodeScratch,
1358}
1359
1360impl HtCodeBlockDecodeWorkspace {
1361    /// Current coefficient buffer capacity retained by this workspace.
1362    pub fn coefficient_capacity(&self) -> usize {
1363        self.coefficients.capacity()
1364    }
1365}
1366
1367/// Adapter scalar HTJ2K phase timings for backend experimentation.
1368#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
1369pub struct HtCodeBlockDecodeProfile {
1370    /// Number of decoded HT code blocks.
1371    pub blocks: u128,
1372    /// Number of decoded HT code blocks with refinement data.
1373    pub refinement_blocks: u128,
1374    /// Total cleanup segment bytes consumed by decoded HT code blocks.
1375    pub cleanup_bytes: u128,
1376    /// Total refinement segment bytes consumed by decoded HT code blocks.
1377    pub refinement_bytes: u128,
1378    /// Cleanup phase elapsed time in microseconds.
1379    pub cleanup_us: u128,
1380    /// Magnitude/sign phase elapsed time in microseconds.
1381    pub mag_sgn_us: u128,
1382    /// Sigma build phase elapsed time in microseconds.
1383    pub sigma_us: u128,
1384    /// Significance propagation phase elapsed time in microseconds.
1385    pub sigprop_us: u128,
1386    /// Magnitude refinement phase elapsed time in microseconds.
1387    pub magref_us: u128,
1388}
1389
1390impl HtCodeBlockDecodeProfile {
1391    fn add_native_stats(&mut self, stats: j2c::ht_block_decode::HtBlockDecodeStats) {
1392        self.blocks += stats.blocks;
1393        self.refinement_blocks += stats.refinement_blocks;
1394        self.cleanup_bytes += stats.cleanup_bytes;
1395        self.refinement_bytes += stats.refinement_bytes;
1396        self.cleanup_us += stats.ht_cleanup_us;
1397        self.mag_sgn_us += stats.ht_mag_sgn_us;
1398        self.sigma_us += stats.ht_sigma_us;
1399        self.sigprop_us += stats.ht_sigprop_us;
1400        self.magref_us += stats.ht_magref_us;
1401    }
1402}
1403
1404/// Adapter scalar HTJ2K decoder helper that reuses caller-owned scratch buffers.
1405pub fn decode_ht_code_block_scalar_with_workspace(
1406    job: HtCodeBlockDecodeJob<'_>,
1407    output: &mut [f32],
1408    workspace: &mut HtCodeBlockDecodeWorkspace,
1409) -> Result<()> {
1410    decode_ht_code_block_scalar_for_phase_with_workspace::<
1411        { j2c::ht_block_decode::PHASE_LIMIT_MAGREF },
1412    >(job, output, workspace)
1413}
1414
1415/// Adapter scalar HTJ2K decoder helper that reuses scratch and records phase timings.
1416pub fn decode_ht_code_block_scalar_with_workspace_profiled(
1417    job: HtCodeBlockDecodeJob<'_>,
1418    output: &mut [f32],
1419    workspace: &mut HtCodeBlockDecodeWorkspace,
1420    profile: &mut HtCodeBlockDecodeProfile,
1421) -> Result<()> {
1422    decode_ht_code_block_scalar_for_phase_with_workspace_profiled::<
1423        { j2c::ht_block_decode::PHASE_LIMIT_MAGREF },
1424    >(job, output, workspace, profile)
1425}
1426
1427fn decode_ht_code_block_scalar_for_phase<const PHASE_LIMIT: u8>(
1428    job: HtCodeBlockDecodeJob<'_>,
1429    output: &mut [f32],
1430) -> Result<()> {
1431    let mut workspace = HtCodeBlockDecodeWorkspace::default();
1432    decode_ht_code_block_scalar_for_phase_with_workspace::<PHASE_LIMIT>(job, output, &mut workspace)
1433}
1434
1435fn decode_ht_code_block_scalar_for_phase_with_workspace<const PHASE_LIMIT: u8>(
1436    job: HtCodeBlockDecodeJob<'_>,
1437    output: &mut [f32],
1438    workspace: &mut HtCodeBlockDecodeWorkspace,
1439) -> Result<()> {
1440    let required_len = if job.height == 0 {
1441        0
1442    } else {
1443        job.output_stride
1444            .checked_mul(job.height as usize - 1)
1445            .and_then(|prefix| prefix.checked_add(job.width as usize))
1446            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1447    };
1448    if output.len() < required_len {
1449        bail!(DecodingError::CodeBlockDecodeFailure);
1450    }
1451    let code_block_stride =
1452        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1453    let code_block_len = code_block_stride
1454        .checked_mul(job.height as usize)
1455        .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1456
1457    let segments = j2c::ht_block_decode::HtCodeBlockSegments::from_combined_payload(
1458        job.data,
1459        job.cleanup_length,
1460        job.refinement_length,
1461    )?;
1462    let coded_bitplanes = add_roi_shift_to_bitplanes(job.num_bitplanes, job.roi_shift, 31)?;
1463    workspace.coefficients.clear();
1464    workspace.coefficients.resize(code_block_len, 0);
1465    j2c::ht_block_decode::decode_segments_validated_with_scratch_for_phase::<PHASE_LIMIT>(
1466        &segments,
1467        job.missing_bit_planes,
1468        coded_bitplanes,
1469        job.number_of_coding_passes,
1470        job.stripe_causal,
1471        job.strict,
1472        &mut workspace.coefficients,
1473        job.width,
1474        job.height,
1475        job.width,
1476        &mut workspace.scratch,
1477        None,
1478        false,
1479    )?;
1480
1481    for (row_idx, coeff_row) in workspace
1482        .coefficients
1483        .chunks_exact(code_block_stride)
1484        .enumerate()
1485        .take(job.height as usize)
1486    {
1487        let row_start = row_idx * job.output_stride;
1488        let output_row = &mut output[row_start..row_start + code_block_stride];
1489        for (coefficient, sample) in coeff_row.iter().copied().zip(output_row.iter_mut()) {
1490            let coefficient =
1491                j2c::ht_block_decode::coefficient_to_i32(coefficient, coded_bitplanes);
1492            let coefficient = apply_roi_maxshift_inverse_i32(coefficient, job.roi_shift);
1493            *sample = coefficient as f32 * job.dequantization_step;
1494        }
1495    }
1496
1497    Ok(())
1498}
1499
1500fn decode_ht_code_block_scalar_for_phase_with_workspace_profiled<const PHASE_LIMIT: u8>(
1501    job: HtCodeBlockDecodeJob<'_>,
1502    output: &mut [f32],
1503    workspace: &mut HtCodeBlockDecodeWorkspace,
1504    profile: &mut HtCodeBlockDecodeProfile,
1505) -> Result<()> {
1506    let required_len = if job.height == 0 {
1507        0
1508    } else {
1509        job.output_stride
1510            .checked_mul(job.height as usize - 1)
1511            .and_then(|prefix| prefix.checked_add(job.width as usize))
1512            .ok_or(DecodingError::CodeBlockDecodeFailure)?
1513    };
1514    if output.len() < required_len {
1515        bail!(DecodingError::CodeBlockDecodeFailure);
1516    }
1517    let code_block_stride =
1518        usize::try_from(job.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
1519    let code_block_len = code_block_stride
1520        .checked_mul(job.height as usize)
1521        .ok_or(DecodingError::CodeBlockDecodeFailure)?;
1522
1523    let segments = j2c::ht_block_decode::HtCodeBlockSegments::from_combined_payload(
1524        job.data,
1525        job.cleanup_length,
1526        job.refinement_length,
1527    )?;
1528    let coded_bitplanes = add_roi_shift_to_bitplanes(job.num_bitplanes, job.roi_shift, 31)?;
1529    workspace.coefficients.clear();
1530    workspace.coefficients.resize(code_block_len, 0);
1531    let mut stats = j2c::ht_block_decode::HtBlockDecodeStats::default();
1532    j2c::ht_block_decode::decode_segments_validated_with_scratch_for_phase::<PHASE_LIMIT>(
1533        &segments,
1534        job.missing_bit_planes,
1535        coded_bitplanes,
1536        job.number_of_coding_passes,
1537        job.stripe_causal,
1538        job.strict,
1539        &mut workspace.coefficients,
1540        job.width,
1541        job.height,
1542        job.width,
1543        &mut workspace.scratch,
1544        Some(&mut stats),
1545        true,
1546    )?;
1547    profile.add_native_stats(stats);
1548
1549    for (row_idx, coeff_row) in workspace
1550        .coefficients
1551        .chunks_exact(code_block_stride)
1552        .enumerate()
1553        .take(job.height as usize)
1554    {
1555        let row_start = row_idx * job.output_stride;
1556        let output_row = &mut output[row_start..row_start + code_block_stride];
1557        for (coefficient, sample) in coeff_row.iter().copied().zip(output_row.iter_mut()) {
1558            let coefficient =
1559                j2c::ht_block_decode::coefficient_to_i32(coefficient, coded_bitplanes);
1560            let coefficient = apply_roi_maxshift_inverse_i32(coefficient, job.roi_shift);
1561            *sample = coefficient as f32 * job.dequantization_step;
1562        }
1563    }
1564
1565    Ok(())
1566}
1567
1568/// Adapter HTJ2K SigProp benchmark state for backend experimentation.
1569pub struct HtSigPropBenchmarkState(j2c::ht_block_decode::HtSigPropBenchmarkState);
1570
1571impl HtSigPropBenchmarkState {
1572    /// Coefficient buffer length required by `decode_ht_sigprop_benchmark_state`.
1573    pub fn output_len(&self) -> usize {
1574        self.0.output_len()
1575    }
1576}
1577
1578/// Adapter helper that precomputes cleanup-derived SigProp inputs for benchmarks.
1579pub fn prepare_ht_sigprop_benchmark_state(
1580    job: HtCodeBlockDecodeJob<'_>,
1581) -> Result<HtSigPropBenchmarkState> {
1582    let segments = j2c::ht_block_decode::HtCodeBlockSegments::from_combined_payload(
1583        job.data,
1584        job.cleanup_length,
1585        job.refinement_length,
1586    )?;
1587    let state = j2c::ht_block_decode::prepare_sigprop_benchmark_state(
1588        &segments,
1589        job.missing_bit_planes,
1590        job.num_bitplanes,
1591        job.number_of_coding_passes,
1592        job.stripe_causal,
1593        job.strict,
1594        job.width,
1595        job.height,
1596        job.width,
1597    )?;
1598    Ok(HtSigPropBenchmarkState(state))
1599}
1600
1601/// Adapter helper that runs only the HTJ2K significance-propagation phase.
1602pub fn decode_ht_sigprop_benchmark_state(
1603    state: &mut HtSigPropBenchmarkState,
1604    output: &mut [u32],
1605) -> Result<()> {
1606    j2c::ht_block_decode::decode_sigprop_benchmark_state(&mut state.0, output)
1607}
1608
1609/// Adapter HTJ2K VLC table 0 for backend experimentation.
1610pub fn ht_vlc_table0() -> &'static [u16; 1024] {
1611    &j2c::ht_tables::VLC_TABLE0
1612}
1613
1614/// Adapter HTJ2K VLC table 1 for backend experimentation.
1615pub fn ht_vlc_table1() -> &'static [u16; 1024] {
1616    &j2c::ht_tables::VLC_TABLE1
1617}
1618
1619/// Adapter HTJ2K UVLC table 0 for backend experimentation.
1620pub fn ht_uvlc_table0() -> &'static [u16; 320] {
1621    &j2c::ht_tables::UVLC_TABLE0
1622}
1623
1624/// Adapter HTJ2K UVLC table 1 for backend experimentation.
1625pub fn ht_uvlc_table1() -> &'static [u16; 256] {
1626    &j2c::ht_tables::UVLC_TABLE1
1627}
1628
1629/// Adapter HTJ2K cleanup encoder VLC table 0 for backend experimentation.
1630pub fn ht_vlc_encode_table0() -> &'static [u16; 2048] {
1631    &j2c::ht_encode_tables::HT_VLC_ENCODE_TABLE0
1632}
1633
1634/// Adapter HTJ2K cleanup encoder VLC table 1 for backend experimentation.
1635pub fn ht_vlc_encode_table1() -> &'static [u16; 2048] {
1636    &j2c::ht_encode_tables::HT_VLC_ENCODE_TABLE1
1637}
1638
1639/// Adapter HTJ2K cleanup encoder UVLC table for backend experimentation.
1640pub fn ht_uvlc_encode_table() -> &'static [HtUvlcTableEntry; 75] {
1641    &j2c::ht_encode_tables::HT_UVLC_ENCODE_TABLE
1642}
1643
1644/// Adapter HTJ2K cleanup encoder UVLC table packed for byte-addressed backends.
1645pub fn ht_uvlc_encode_table_bytes() -> &'static [u8] {
1646    &j2c::ht_encode_tables::HT_UVLC_ENCODE_TABLE_BYTES
1647}
1648
1649/// JP2 signature box: 00 00 00 0C 6A 50 20 20
1650pub(crate) const JP2_MAGIC: &[u8] = b"\x00\x00\x00\x0C\x6A\x50\x20\x20";
1651/// Codestream signature: FF 4F FF 51 (SOC + SIZ markers)
1652pub(crate) const CODESTREAM_MAGIC: &[u8] = b"\xFF\x4F\xFF\x51";
1653
1654/// Settings to apply during decoding.
1655#[derive(Debug, Copy, Clone)]
1656pub struct DecodeSettings {
1657    /// Whether palette indices should be resolved.
1658    ///
1659    /// JPEG2000 images can be stored in two different ways. First, by storing
1660    /// RGB values (depending on the color space) for each pixel. Secondly, by
1661    /// only storing a single index for each channel, and then resolving the
1662    /// actual color using the index.
1663    ///
1664    /// If you disable this option, in case you have an image with palette
1665    /// indices, they will not be resolved, but instead a grayscale image
1666    /// will be returned, with each pixel value corresponding to the palette
1667    /// index of the location.
1668    pub resolve_palette_indices: bool,
1669    /// Whether strict mode should be enabled when decoding.
1670    ///
1671    /// It is recommended to leave this flag disabled, unless you have a
1672    /// specific reason not to.
1673    pub strict: bool,
1674    /// A hint for the target resolution that the image should be decoded at.
1675    pub target_resolution: Option<(u32, u32)>,
1676}
1677
1678impl Default for DecodeSettings {
1679    fn default() -> Self {
1680        Self {
1681            resolve_palette_indices: true,
1682            strict: false,
1683            target_resolution: None,
1684        }
1685    }
1686}
1687
1688/// A JPEG2000 image or codestream.
1689pub struct Image<'a> {
1690    /// The tile-part payload used by the legacy JPEG 2000 decoder.
1691    pub(crate) codestream: &'a [u8],
1692    /// The header of the J2C codestream.
1693    pub(crate) header: Header<'a>,
1694    /// The JP2 boxes of the image. In the case of a raw codestream, we
1695    /// will synthesize the necessary boxes.
1696    pub(crate) boxes: ImageBoxes,
1697    /// Settings that should be applied during decoding.
1698    pub(crate) settings: DecodeSettings,
1699    /// Whether the image has an alpha channel.
1700    pub(crate) has_alpha: bool,
1701    /// The color space of the image.
1702    pub(crate) color_space: ColorSpace,
1703}
1704
1705impl<'a> Image<'a> {
1706    /// Try to create a new JPEG2000 image from the given data.
1707    pub fn new(data: &'a [u8], settings: &DecodeSettings) -> Result<Self> {
1708        if data.starts_with(JP2_MAGIC) {
1709            jp2::parse(data, *settings)
1710        } else if data.starts_with(CODESTREAM_MAGIC) {
1711            j2c::parse(data, settings)
1712        } else {
1713            err!(FormatError::InvalidSignature)
1714        }
1715    }
1716
1717    /// Whether the image has an alpha channel.
1718    pub fn has_alpha(&self) -> bool {
1719        self.has_alpha
1720    }
1721
1722    /// The color space of the image.
1723    pub fn color_space(&self) -> &ColorSpace {
1724        &self.color_space
1725    }
1726
1727    /// The width of the image.
1728    pub fn width(&self) -> u32 {
1729        self.header.size_data.image_width()
1730    }
1731
1732    /// The height of the image.
1733    pub fn height(&self) -> u32 {
1734        self.header.size_data.image_height()
1735    }
1736
1737    /// The original bit depth of the image. You usually don't need to do anything
1738    /// with this parameter, it just exists for informational purposes.
1739    pub fn original_bit_depth(&self) -> u8 {
1740        // Note that this only works if all components have the same precision.
1741        self.header.component_infos[0].size_info.precision
1742    }
1743
1744    /// Whether decode finishes with additional host-side component mutation or reordering.
1745    pub fn supports_direct_device_plane_reuse(&self) -> bool {
1746        if self.settings.resolve_palette_indices && self.boxes.palette.is_some() {
1747            return false;
1748        }
1749        if self.boxes.channel_definition.is_some() {
1750            return false;
1751        }
1752        !matches!(
1753            self.boxes
1754                .color_specification
1755                .as_ref()
1756                .map(|spec| &spec.color_space),
1757            Some(jp2::colr::ColorSpace::Enumerated(
1758                EnumeratedColorspace::Sycc | EnumeratedColorspace::CieLab(_)
1759            ))
1760        )
1761    }
1762
1763    /// Decode the image and return its decoded result as a `Vec<u8>`, with each
1764    /// channel interleaved.
1765    pub fn decode(&self) -> Result<Vec<u8>> {
1766        let bitmap = self.decode_with_context(&mut DecoderContext::default())?;
1767        Ok(bitmap.data)
1768    }
1769
1770    /// Decode the image and return its decoded result using a caller-provided
1771    /// decoder context so allocations can be reused across repeated decodes.
1772    pub fn decode_with_context(&self, decoder_context: &mut DecoderContext<'a>) -> Result<Bitmap> {
1773        let buffer_size = checked_decode_byte_len3(
1774            self.width() as usize,
1775            self.height() as usize,
1776            self.color_space.num_channels() as usize + if self.has_alpha { 1 } else { 0 },
1777        )?;
1778        let mut buf = vec![0; buffer_size];
1779        self.decode_into(&mut buf, decoder_context)?;
1780
1781        Ok(Bitmap {
1782            color_space: self.color_space.clone(),
1783            data: buf,
1784            has_alpha: self.has_alpha,
1785            width: self.width(),
1786            height: self.height(),
1787            original_bit_depth: self.original_bit_depth(),
1788        })
1789    }
1790
1791    /// Decode the image into borrowed component planes using a caller-provided
1792    /// decoder context so allocations can be reused across repeated decodes.
1793    pub fn decode_components_with_context<'ctx>(
1794        &self,
1795        decoder_context: &'ctx mut DecoderContext<'a>,
1796    ) -> Result<DecodedComponents<'ctx>> {
1797        let decoded_image = self.prepare_decoded_image(decoder_context)?;
1798        let planes = decoded_image
1799            .decoded_components
1800            .iter()
1801            .map(|component| ComponentPlane {
1802                samples: component.container.truncated(),
1803                bit_depth: component.bit_depth,
1804            })
1805            .collect();
1806
1807        Ok(DecodedComponents {
1808            dimensions: (self.width(), self.height()),
1809            color_space: self.color_space.clone(),
1810            has_alpha: self.has_alpha,
1811            planes,
1812        })
1813    }
1814
1815    /// Build a adapter grayscale direct device plan without materializing host component planes.
1816    pub fn build_direct_grayscale_plan_with_context(
1817        &self,
1818        decoder_context: &mut DecoderContext<'a>,
1819    ) -> Result<J2kDirectGrayscalePlan> {
1820        if !matches!(self.color_space, ColorSpace::Gray) || self.has_alpha {
1821            bail!(DecodingError::UnsupportedFeature(
1822                "direct grayscale plan only supports grayscale images without alpha"
1823            ));
1824        }
1825
1826        j2c::build_direct_grayscale_plan(self.codestream, &self.header, decoder_context)
1827    }
1828
1829    /// Build a adapter grayscale direct device plan for an output-space region.
1830    pub fn build_direct_grayscale_plan_region_with_context(
1831        &self,
1832        decoder_context: &mut DecoderContext<'a>,
1833        output_region: (u32, u32, u32, u32),
1834    ) -> Result<J2kDirectGrayscalePlan> {
1835        if !matches!(self.color_space, ColorSpace::Gray) || self.has_alpha {
1836            bail!(DecodingError::UnsupportedFeature(
1837                "direct grayscale plan only supports grayscale images without alpha"
1838            ));
1839        }
1840
1841        decoder_context.set_output_region(Some(output_region));
1842        let result =
1843            j2c::build_direct_grayscale_plan(self.codestream, &self.header, decoder_context);
1844        decoder_context.set_output_region(None);
1845        result
1846    }
1847
1848    /// Build a adapter RGB direct device plan without materializing host component planes.
1849    pub fn build_direct_color_plan_with_context(
1850        &self,
1851        decoder_context: &mut DecoderContext<'a>,
1852    ) -> Result<J2kDirectColorPlan> {
1853        if !matches!(self.color_space, ColorSpace::RGB) || self.has_alpha {
1854            bail!(DecodingError::UnsupportedFeature(
1855                "direct color plan only supports RGB images without alpha"
1856            ));
1857        }
1858
1859        j2c::build_direct_color_plan(self.codestream, &self.header, decoder_context)
1860    }
1861
1862    /// Build a adapter RGB direct device plan for an output-space region.
1863    pub fn build_direct_color_plan_region_with_context(
1864        &self,
1865        decoder_context: &mut DecoderContext<'a>,
1866        output_region: (u32, u32, u32, u32),
1867    ) -> Result<J2kDirectColorPlan> {
1868        if !matches!(self.color_space, ColorSpace::RGB) || self.has_alpha {
1869            bail!(DecodingError::UnsupportedFeature(
1870                "direct color plan only supports RGB images without alpha"
1871            ));
1872        }
1873
1874        decoder_context.set_output_region(Some(output_region));
1875        let result = j2c::build_direct_color_plan(self.codestream, &self.header, decoder_context);
1876        decoder_context.set_output_region(None);
1877        result
1878    }
1879
1880    /// Decode borrowed component planes while delegating HTJ2K code-block decode.
1881    pub fn decode_components_with_ht_decoder<'ctx>(
1882        &self,
1883        decoder_context: &'ctx mut DecoderContext<'a>,
1884        ht_decoder: &mut dyn HtCodeBlockDecoder,
1885    ) -> Result<DecodedComponents<'ctx>> {
1886        let decoded_image =
1887            self.prepare_decoded_image_with_ht_decoder(decoder_context, ht_decoder)?;
1888        let planes = decoded_image
1889            .decoded_components
1890            .iter()
1891            .map(|component| ComponentPlane {
1892                samples: component.container.truncated(),
1893                bit_depth: component.bit_depth,
1894            })
1895            .collect();
1896
1897        Ok(DecodedComponents {
1898            dimensions: (self.width(), self.height()),
1899            color_space: self.color_space.clone(),
1900            has_alpha: self.has_alpha,
1901            planes,
1902        })
1903    }
1904
1905    /// Decode borrowed component planes for a requested region using a
1906    /// caller-provided decoder context.
1907    pub fn decode_region_components_with_context<'ctx>(
1908        &self,
1909        roi: (u32, u32, u32, u32),
1910        decoder_context: &'ctx mut DecoderContext<'a>,
1911    ) -> Result<DecodedComponents<'ctx>> {
1912        validate_roi((self.width(), self.height()), roi)?;
1913        let (_x, _y, width, height) = roi;
1914        let decoded_image = self.prepare_decoded_image_with_region(decoder_context, Some(roi))?;
1915        let planes = decoded_image
1916            .decoded_components
1917            .iter()
1918            .map(|component| ComponentPlane {
1919                samples: component.container.truncated(),
1920                bit_depth: component.bit_depth,
1921            })
1922            .collect();
1923
1924        Ok(DecodedComponents {
1925            dimensions: (width, height),
1926            color_space: self.color_space.clone(),
1927            has_alpha: self.has_alpha,
1928            planes,
1929        })
1930    }
1931
1932    /// Decode borrowed component planes for a requested region while
1933    /// delegating code-block/transform stages through the adapter backend hook.
1934    pub fn decode_region_components_with_ht_decoder<'ctx>(
1935        &self,
1936        decoder_context: &'ctx mut DecoderContext<'a>,
1937        roi: (u32, u32, u32, u32),
1938        ht_decoder: &mut dyn HtCodeBlockDecoder,
1939    ) -> Result<DecodedComponents<'ctx>> {
1940        validate_roi((self.width(), self.height()), roi)?;
1941        let (_x, _y, width, height) = roi;
1942        let decoded_image = self.prepare_decoded_image_with_region_and_ht_decoder(
1943            decoder_context,
1944            Some(roi),
1945            Some(ht_decoder),
1946        )?;
1947        let planes = decoded_image
1948            .decoded_components
1949            .iter()
1950            .map(|component| ComponentPlane {
1951                samples: component.container.truncated(),
1952                bit_depth: component.bit_depth,
1953            })
1954            .collect();
1955
1956        Ok(DecodedComponents {
1957            dimensions: (width, height),
1958            color_space: self.color_space.clone(),
1959            has_alpha: self.has_alpha,
1960            planes,
1961        })
1962    }
1963
1964    /// Decode a region of the image and return it as an 8-bit interleaved bitmap.
1965    pub fn decode_region(&self, roi: (u32, u32, u32, u32)) -> Result<Bitmap> {
1966        self.decode_region_with_context(roi, &mut DecoderContext::default())
1967    }
1968
1969    /// Decode a region of the image and return it as an 8-bit interleaved bitmap
1970    /// using a caller-provided decoder context.
1971    pub fn decode_region_with_context(
1972        &self,
1973        roi: (u32, u32, u32, u32),
1974        decoder_context: &mut DecoderContext<'a>,
1975    ) -> Result<Bitmap> {
1976        validate_roi((self.width(), self.height()), roi)?;
1977        let mut decoded_image =
1978            self.prepare_decoded_image_with_region(decoder_context, Some(roi))?;
1979        let (_x, _y, width, height) = roi;
1980        let channels =
1981            self.color_space.num_channels() as usize + if self.has_alpha { 1 } else { 0 };
1982        let data_len = checked_decode_byte_len3(width as usize, height as usize, channels)?;
1983        let mut data = vec![0; data_len];
1984        interleave_and_convert_region(
1985            &mut decoded_image,
1986            width as usize,
1987            (0, 0, width, height),
1988            &mut data,
1989        );
1990        Ok(Bitmap {
1991            color_space: self.color_space.clone(),
1992            data,
1993            has_alpha: self.has_alpha,
1994            width,
1995            height,
1996            original_bit_depth: self.original_bit_depth(),
1997        })
1998    }
1999
2000    /// Decode the image at native bit depth without scaling to 8-bit.
2001    ///
2002    /// For images with bit depth ≤ 8, returns pixel data as `Vec<u8>`.
2003    /// For images with bit depth > 8 (e.g., 12-bit or 16-bit), returns
2004    /// pixel data as little-endian `u16` values packed into `Vec<u8>`.
2005    ///
2006    /// This is essential for medical imaging (DICOM) where 12-bit and 16-bit
2007    /// images must preserve their full dynamic range.
2008    pub fn decode_native(&self) -> Result<RawBitmap> {
2009        let mut decoder_context = DecoderContext::default();
2010        self.decode_native_with_context(&mut decoder_context)
2011    }
2012
2013    /// Extract reversible 5/3 wavelet coefficients for coefficient-domain
2014    /// classic JPEG 2000 to HTJ2K recoding.
2015    ///
2016    /// This decodes classic Tier-1 code-blocks into dequantized reversible
2017    /// wavelet coefficients, but does not run inverse DWT or color conversion.
2018    pub fn decode_reversible_53_coefficients(&self) -> Result<Reversible53CoefficientImage> {
2019        let mut decoder_context = DecoderContext::default();
2020        self.decode_reversible_53_coefficients_with_context(&mut decoder_context)
2021    }
2022
2023    /// Extract reversible 5/3 wavelet coefficients using a caller-provided
2024    /// decoder context.
2025    pub fn decode_reversible_53_coefficients_with_context(
2026        &self,
2027        decoder_context: &mut DecoderContext<'a>,
2028    ) -> Result<Reversible53CoefficientImage> {
2029        j2c::recode::extract_reversible_53_coefficients(
2030            self.codestream,
2031            &self.header,
2032            decoder_context,
2033        )
2034    }
2035
2036    /// Decode a region of the image at native bit depth.
2037    pub fn decode_native_region(&self, roi: (u32, u32, u32, u32)) -> Result<RawBitmap> {
2038        self.decode_native_region_with_context(roi, &mut DecoderContext::default())
2039    }
2040
2041    /// Decode the image at native bit depth using a caller-provided decoder
2042    /// context so allocations can be reused across repeated decodes.
2043    pub fn decode_native_with_context(
2044        &self,
2045        decoder_context: &mut DecoderContext<'a>,
2046    ) -> Result<RawBitmap> {
2047        self.decode_with_output_region(decoder_context, None)?;
2048
2049        let components = &decoder_context.tile_decode_context.channel_data;
2050        let bit_depth = self.original_bit_depth();
2051        let num_components =
2052            u8::try_from(components.len()).map_err(|_| ValidationError::TooManyChannels)?;
2053        let width = self.width();
2054        let height = self.height();
2055        let pixel_count = checked_decode_sample_count(width, height)?;
2056
2057        if bit_depth <= 8 {
2058            let max_val = ((1u32 << bit_depth) - 1) as f32;
2059            let capacity = checked_decode_byte_len2(pixel_count, num_components as usize)?;
2060            let mut data = Vec::with_capacity(capacity);
2061            for i in 0..pixel_count {
2062                for component in components.iter() {
2063                    let v = math::round_f32(component.container.truncated()[i]);
2064                    let clamped = if v < 0.0 {
2065                        0.0
2066                    } else if v > max_val {
2067                        max_val
2068                    } else {
2069                        v
2070                    };
2071                    data.push(clamped as u8);
2072                }
2073            }
2074            Ok(RawBitmap {
2075                data,
2076                width,
2077                height,
2078                bit_depth,
2079                num_components,
2080                bytes_per_sample: 1,
2081            })
2082        } else {
2083            let max_val = ((1u32 << bit_depth) - 1) as f32;
2084            let capacity = checked_decode_byte_len3(pixel_count, num_components as usize, 2)?;
2085            let mut data = Vec::with_capacity(capacity);
2086            for i in 0..pixel_count {
2087                for component in components.iter() {
2088                    let v = math::round_f32(component.container.truncated()[i]);
2089                    let clamped = if v < 0.0 {
2090                        0.0
2091                    } else if v > max_val {
2092                        max_val
2093                    } else {
2094                        v
2095                    };
2096                    let val = clamped as u16;
2097                    data.extend_from_slice(&val.to_le_bytes());
2098                }
2099            }
2100            Ok(RawBitmap {
2101                data,
2102                width,
2103                height,
2104                bit_depth,
2105                num_components,
2106                bytes_per_sample: 2,
2107            })
2108        }
2109    }
2110
2111    /// Decode a region of the image at native bit depth using a caller-provided
2112    /// decoder context.
2113    pub fn decode_native_region_with_context(
2114        &self,
2115        roi: (u32, u32, u32, u32),
2116        decoder_context: &mut DecoderContext<'a>,
2117    ) -> Result<RawBitmap> {
2118        validate_roi((self.width(), self.height()), roi)?;
2119        self.decode_with_output_region(decoder_context, Some(roi))?;
2120
2121        let components = &decoder_context.tile_decode_context.channel_data;
2122        let bit_depth = self.original_bit_depth();
2123        let num_components =
2124            u8::try_from(components.len()).map_err(|_| ValidationError::TooManyChannels)?;
2125        let bytes_per_sample = if bit_depth <= 8 { 1 } else { 2 };
2126        let (_x, _y, width, height) = roi;
2127        let capacity = checked_decode_byte_len4(
2128            width as usize,
2129            height as usize,
2130            num_components as usize,
2131            bytes_per_sample,
2132        )?;
2133        let mut data = Vec::with_capacity(capacity);
2134        let max_val = ((1u32 << bit_depth) - 1) as f32;
2135
2136        for row in 0..height as usize {
2137            for col in 0..width as usize {
2138                let idx = row * width as usize + col;
2139                for component in components {
2140                    let v = math::round_f32(component.container.truncated()[idx]);
2141                    let clamped = if v < 0.0 {
2142                        0.0
2143                    } else if v > max_val {
2144                        max_val
2145                    } else {
2146                        v
2147                    };
2148                    if bit_depth <= 8 {
2149                        data.push(clamped as u8);
2150                    } else {
2151                        data.extend_from_slice(&(clamped as u16).to_le_bytes());
2152                    }
2153                }
2154            }
2155        }
2156
2157        Ok(RawBitmap {
2158            data,
2159            width,
2160            height,
2161            bit_depth,
2162            num_components,
2163            bytes_per_sample: bytes_per_sample as u8,
2164        })
2165    }
2166
2167    /// Decode the image into the given buffer.
2168    ///
2169    /// This method does the same as [`Image::decode`], but you can provide
2170    /// a custom buffer for the output, as well as a decoder context. Doing
2171    /// so allows the internal decode engine to reuse memory allocations, so
2172    /// this is especially recommended if you plan on converting multiple
2173    /// images in the same session.
2174    ///
2175    /// The buffer must have the correct size.
2176    pub fn decode_into(
2177        &self,
2178        buf: &mut [u8],
2179        decoder_context: &mut DecoderContext<'a>,
2180    ) -> Result<()> {
2181        let mut decoded_image = self.prepare_decoded_image(decoder_context)?;
2182        validate_interleaved_output_buffer(&decoded_image, buf)?;
2183        interleave_and_convert(&mut decoded_image, buf)?;
2184
2185        Ok(())
2186    }
2187
2188    fn prepare_decoded_image<'ctx>(
2189        &self,
2190        decoder_context: &'ctx mut DecoderContext<'a>,
2191    ) -> Result<DecodedImage<'ctx>> {
2192        self.prepare_decoded_image_with_region(decoder_context, None)
2193    }
2194
2195    fn prepare_decoded_image_with_ht_decoder<'ctx>(
2196        &self,
2197        decoder_context: &'ctx mut DecoderContext<'a>,
2198        ht_decoder: &mut dyn HtCodeBlockDecoder,
2199    ) -> Result<DecodedImage<'ctx>> {
2200        self.prepare_decoded_image_with_region_and_ht_decoder(
2201            decoder_context,
2202            None,
2203            Some(ht_decoder),
2204        )
2205    }
2206
2207    fn prepare_decoded_image_with_region<'ctx>(
2208        &self,
2209        decoder_context: &'ctx mut DecoderContext<'a>,
2210        output_region: Option<(u32, u32, u32, u32)>,
2211    ) -> Result<DecodedImage<'ctx>> {
2212        self.prepare_decoded_image_with_region_and_ht_decoder(decoder_context, output_region, None)
2213    }
2214
2215    fn prepare_decoded_image_with_region_and_ht_decoder<'ctx>(
2216        &self,
2217        decoder_context: &'ctx mut DecoderContext<'a>,
2218        output_region: Option<(u32, u32, u32, u32)>,
2219        ht_decoder: Option<&mut dyn HtCodeBlockDecoder>,
2220    ) -> Result<DecodedImage<'ctx>> {
2221        let settings = &self.settings;
2222        self.decode_with_output_region_and_ht_decoder(decoder_context, output_region, ht_decoder)?;
2223        let mut decoded_image = DecodedImage {
2224            decoded_components: &mut decoder_context.tile_decode_context.channel_data,
2225            boxes: self.boxes.clone(),
2226        };
2227
2228        if settings.resolve_palette_indices {
2229            let components = core::mem::take(decoded_image.decoded_components);
2230            *decoded_image.decoded_components =
2231                resolve_palette_indices(components, &decoded_image.boxes)?;
2232        }
2233
2234        if let Some(cdef) = &decoded_image.boxes.channel_definition {
2235            validate_channel_definition(cdef, decoded_image.decoded_components.len())?;
2236            let mut components = decoded_image
2237                .decoded_components
2238                .iter()
2239                .cloned()
2240                .zip(
2241                    cdef.channel_definitions
2242                        .iter()
2243                        .map(|c| match c._association {
2244                            ChannelAssociation::WholeImage => u16::MAX,
2245                            ChannelAssociation::Colour(c) => c,
2246                        }),
2247                )
2248                .collect::<Vec<_>>();
2249            components.sort_by_key(|component| component.1);
2250            *decoded_image.decoded_components = components.into_iter().map(|c| c.0).collect();
2251        }
2252
2253        let bit_depth = decoded_image.decoded_components[0].bit_depth;
2254        convert_color_space(&mut decoded_image, bit_depth)?;
2255        Ok(decoded_image)
2256    }
2257
2258    fn decode_with_output_region(
2259        &self,
2260        decoder_context: &mut DecoderContext<'a>,
2261        output_region: Option<(u32, u32, u32, u32)>,
2262    ) -> Result<()> {
2263        self.decode_with_output_region_and_ht_decoder(decoder_context, output_region, None)
2264    }
2265
2266    fn decode_with_output_region_and_ht_decoder(
2267        &self,
2268        decoder_context: &mut DecoderContext<'a>,
2269        output_region: Option<(u32, u32, u32, u32)>,
2270        mut ht_decoder: Option<&mut dyn HtCodeBlockDecoder>,
2271    ) -> Result<()> {
2272        decoder_context.set_output_region(output_region);
2273        let decode_result = j2c::decode(
2274            self.codestream,
2275            &self.header,
2276            decoder_context,
2277            &mut ht_decoder,
2278        );
2279        decoder_context.set_output_region(None);
2280        decode_result
2281    }
2282}
2283
2284fn validate_channel_definition(
2285    cdef: &jp2::cdef::ChannelDefinitionBox,
2286    component_count: usize,
2287) -> Result<()> {
2288    if cdef.channel_definitions.len() != component_count {
2289        bail!(ValidationError::InvalidChannelDefinition);
2290    }
2291
2292    let mut seen_color_associations = vec![false; component_count];
2293    for definition in &cdef.channel_definitions {
2294        if let ChannelAssociation::Colour(association) = definition._association {
2295            let Some(index) = association.checked_sub(1).map(usize::from) else {
2296                bail!(ValidationError::InvalidChannelDefinition);
2297            };
2298            if index >= component_count || seen_color_associations[index] {
2299                bail!(ValidationError::InvalidChannelDefinition);
2300            }
2301            seen_color_associations[index] = true;
2302        }
2303    }
2304
2305    Ok(())
2306}
2307
2308pub(crate) fn resolve_alpha_and_color_space(
2309    boxes: &ImageBoxes,
2310    header: &Header<'_>,
2311    settings: &DecodeSettings,
2312) -> Result<(ColorSpace, bool)> {
2313    let mut num_components = header.component_infos.len();
2314
2315    // Override number of components with what is actually in the palette box
2316    // in case we resolve them.
2317    if settings.resolve_palette_indices {
2318        if let Some(palette_box) = &boxes.palette {
2319            num_components = palette_box.columns.len();
2320        }
2321    }
2322
2323    let mut has_alpha = false;
2324
2325    if let Some(cdef) = &boxes.channel_definition {
2326        let last = cdef.channel_definitions.last().unwrap();
2327        has_alpha = last.channel_type == ChannelType::Opacity;
2328    }
2329
2330    let mut color_space = get_color_space(boxes, num_components)?;
2331
2332    // If we didn't resolve palette indices, we need to assume grayscale image.
2333    if !settings.resolve_palette_indices && boxes.palette.is_some() {
2334        has_alpha = false;
2335        color_space = ColorSpace::Gray;
2336    }
2337
2338    let actual_num_components = header.component_infos.len();
2339
2340    // Validate the number of channels.
2341    if boxes.palette.is_none()
2342        && actual_num_components
2343            != (color_space.num_channels() + if has_alpha { 1 } else { 0 }) as usize
2344    {
2345        if !settings.strict
2346            && actual_num_components == color_space.num_channels() as usize + 1
2347            && !has_alpha
2348        {
2349            // See OPENJPEG test case orb-blue10-lin-j2k. Assume that we have an
2350            // alpha channel in this case.
2351            has_alpha = true;
2352        } else {
2353            // Color space is invalid, attempt to repair.
2354            if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
2355                color_space = ColorSpace::Gray;
2356            } else if actual_num_components == 3 {
2357                color_space = ColorSpace::RGB;
2358            } else if actual_num_components == 4 {
2359                if has_alpha {
2360                    color_space = ColorSpace::RGB;
2361                } else {
2362                    color_space = ColorSpace::CMYK;
2363                }
2364            } else {
2365                bail!(ValidationError::TooManyChannels);
2366            }
2367        }
2368    }
2369
2370    Ok((color_space, has_alpha))
2371}
2372
2373/// The color space of the image.
2374#[derive(Debug, Clone)]
2375pub enum ColorSpace {
2376    /// A grayscale image.
2377    Gray,
2378    /// An RGB image.
2379    RGB,
2380    /// A CMYK image.
2381    CMYK,
2382    /// An unknown color space.
2383    Unknown {
2384        /// The number of channels of the color space.
2385        num_channels: u8,
2386    },
2387    /// An image based on an ICC profile.
2388    Icc {
2389        /// The raw data of the ICC profile.
2390        profile: Vec<u8>,
2391        /// The number of channels used by the ICC profile.
2392        num_channels: u8,
2393    },
2394}
2395
2396impl ColorSpace {
2397    /// Return the number of expected channels for the color space.
2398    pub fn num_channels(&self) -> u8 {
2399        match self {
2400            Self::Gray => 1,
2401            Self::RGB => 3,
2402            Self::CMYK => 4,
2403            Self::Unknown { num_channels } => *num_channels,
2404            Self::Icc {
2405                num_channels: num_components,
2406                ..
2407            } => *num_components,
2408        }
2409    }
2410}
2411
2412/// A bitmap storing the decoded result of the image.
2413pub struct Bitmap {
2414    /// The color space of the image.
2415    pub color_space: ColorSpace,
2416    /// The raw pixel data of the image. The result will always be in
2417    /// 8-bit (in case the original image had a different bit-depth, this
2418    /// decode path scales it to 8-bit).
2419    ///
2420    /// The size is guaranteed to equal
2421    /// `width * height * (num_channels + (if has_alpha { 1 } else { 0 })`.
2422    /// Pixels are interleaved on a per-channel basis, the alpha channel always
2423    /// appearing as the last channel, if available.
2424    pub data: Vec<u8>,
2425    /// Whether the image has an alpha channel.
2426    pub has_alpha: bool,
2427    /// The width of the image.
2428    pub width: u32,
2429    /// The height of the image.
2430    pub height: u32,
2431    /// The original bit depth of the image. You usually don't need to do anything
2432    /// with this parameter, it just exists for informational purposes.
2433    pub original_bit_depth: u8,
2434}
2435
2436/// Raw decoded pixel data at native bit depth (no 8-bit scaling).
2437///
2438/// For bit depths ≤ 8, `data` contains one byte per sample.
2439/// For bit depths > 8 (e.g., 12 or 16), `data` contains two bytes per sample
2440/// in little-endian byte order (`u16` LE).
2441///
2442/// Samples are interleaved: for a 3-component image, the layout is
2443/// `[R0, G0, B0, R1, G1, B1, ...]`.
2444pub struct RawBitmap {
2445    /// The raw pixel data at native bit depth.
2446    pub data: Vec<u8>,
2447    /// The width of the image in pixels.
2448    pub width: u32,
2449    /// The height of the image in pixels.
2450    pub height: u32,
2451    /// The original bit depth per sample (e.g., 8, 12, 16).
2452    pub bit_depth: u8,
2453    /// The number of components (e.g., 1 for grayscale, 3 for RGB).
2454    pub num_components: u8,
2455    /// Bytes per sample: 1 for bit_depth ≤ 8, 2 for bit_depth > 8.
2456    pub bytes_per_sample: u8,
2457}
2458
2459/// A borrowed decoded component plane.
2460pub struct ComponentPlane<'a> {
2461    samples: &'a [f32],
2462    bit_depth: u8,
2463}
2464
2465impl<'a> ComponentPlane<'a> {
2466    /// Component samples in row-major order.
2467    pub fn samples(&self) -> &'a [f32] {
2468        self.samples
2469    }
2470
2471    /// Bit depth of this component plane.
2472    pub fn bit_depth(&self) -> u8 {
2473        self.bit_depth
2474    }
2475}
2476
2477/// Borrowed decoded component planes for an image.
2478pub struct DecodedComponents<'a> {
2479    dimensions: (u32, u32),
2480    color_space: ColorSpace,
2481    has_alpha: bool,
2482    planes: Vec<ComponentPlane<'a>>,
2483}
2484
2485impl<'a> DecodedComponents<'a> {
2486    /// Dimensions of the decoded image represented by these planes.
2487    pub fn dimensions(&self) -> (u32, u32) {
2488        self.dimensions
2489    }
2490
2491    /// Color space after JPEG 2000 color conversion has been applied.
2492    pub fn color_space(&self) -> &ColorSpace {
2493        &self.color_space
2494    }
2495
2496    /// Whether the decoded image has an alpha channel.
2497    pub fn has_alpha(&self) -> bool {
2498        self.has_alpha
2499    }
2500
2501    /// Borrowed decoded component planes in display order.
2502    pub fn planes(&self) -> &[ComponentPlane<'a>] {
2503        &self.planes
2504    }
2505}
2506
2507fn validate_interleaved_output_buffer(image: &DecodedImage<'_>, buf: &[u8]) -> Result<()> {
2508    let required_len = interleaved_output_len(image)?;
2509    if buf.len() < required_len {
2510        bail!(DecodingError::OutputBufferTooSmall);
2511    }
2512    Ok(())
2513}
2514
2515fn interleaved_output_len(image: &DecodedImage<'_>) -> Result<usize> {
2516    let Some(first) = image.decoded_components.first() else {
2517        bail!(DecodingError::CodeBlockDecodeFailure);
2518    };
2519    first
2520        .container
2521        .truncated()
2522        .len()
2523        .checked_mul(image.decoded_components.len())
2524        .ok_or(ValidationError::ImageTooLarge.into())
2525}
2526
2527fn interleave_and_convert(image: &mut DecodedImage<'_>, buf: &mut [u8]) -> Result<()> {
2528    let components = &mut *image.decoded_components;
2529    let num_components = components.len();
2530
2531    let mut all_same_bit_depth = Some(components[0].bit_depth);
2532
2533    for component in components.iter().skip(1) {
2534        if Some(component.bit_depth) != all_same_bit_depth {
2535            all_same_bit_depth = None;
2536        }
2537    }
2538
2539    let max_len = components[0].container.truncated().len();
2540
2541    let mut output_iter = buf.iter_mut();
2542
2543    if all_same_bit_depth == Some(8) && num_components <= 4 {
2544        // Fast path for the common case.
2545        match num_components {
2546            // Gray-scale.
2547            1 => {
2548                for (output, input) in output_iter.zip(
2549                    components[0]
2550                        .container
2551                        .iter()
2552                        .map(|v| math::round_f32(*v) as u8),
2553                ) {
2554                    *output = input;
2555                }
2556            }
2557            // Gray-scale with alpha.
2558            2 => {
2559                let c0 = &components[0];
2560                let c1 = &components[1];
2561
2562                let c0 = &c0.container[..max_len];
2563                let c1 = &c1.container[..max_len];
2564
2565                for i in 0..max_len {
2566                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
2567                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
2568                }
2569            }
2570            // RGB
2571            3 => {
2572                let c0 = &components[0];
2573                let c1 = &components[1];
2574                let c2 = &components[2];
2575
2576                let c0 = &c0.container[..max_len];
2577                let c1 = &c1.container[..max_len];
2578                let c2 = &c2.container[..max_len];
2579
2580                for i in 0..max_len {
2581                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
2582                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
2583                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
2584                }
2585            }
2586            // RGBA or CMYK.
2587            4 => {
2588                let c0 = &components[0];
2589                let c1 = &components[1];
2590                let c2 = &components[2];
2591                let c3 = &components[3];
2592
2593                let c0 = &c0.container[..max_len];
2594                let c1 = &c1.container[..max_len];
2595                let c2 = &c2.container[..max_len];
2596                let c3 = &c3.container[..max_len];
2597
2598                for i in 0..max_len {
2599                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
2600                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
2601                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
2602                    *output_iter.next().unwrap() = math::round_f32(c3[i]) as u8;
2603                }
2604            }
2605            _ => bail!(ValidationError::TooManyChannels),
2606        }
2607    } else {
2608        // Slow path that also requires us to scale to 8 bit.
2609        let mul_factor = ((1 << 8) - 1) as f32;
2610
2611        for sample in 0..max_len {
2612            for channel in components.iter() {
2613                *output_iter.next().unwrap() = math::round_f32(
2614                    (channel.container[sample] / ((1_u32 << channel.bit_depth) - 1) as f32)
2615                        * mul_factor,
2616                ) as u8;
2617            }
2618        }
2619    }
2620
2621    Ok(())
2622}
2623
2624fn interleave_and_convert_region(
2625    image: &mut DecodedImage<'_>,
2626    image_width: usize,
2627    roi: (u32, u32, u32, u32),
2628    buf: &mut [u8],
2629) {
2630    let components = &mut *image.decoded_components;
2631    let num_components = components.len();
2632    let (x, y, width, height) = roi;
2633    let mut output_iter = buf.iter_mut();
2634
2635    let mut all_same_bit_depth = Some(components[0].bit_depth);
2636    for component in components.iter().skip(1) {
2637        if Some(component.bit_depth) != all_same_bit_depth {
2638            all_same_bit_depth = None;
2639        }
2640    }
2641
2642    if all_same_bit_depth == Some(8) && num_components <= 4 {
2643        for row in y as usize..(y + height) as usize {
2644            let row_base = row * image_width;
2645            for col in x as usize..(x + width) as usize {
2646                let idx = row_base + col;
2647                for component in components.iter() {
2648                    *output_iter.next().unwrap() = math::round_f32(component.container[idx]) as u8;
2649                }
2650            }
2651        }
2652    } else {
2653        let mul_factor = ((1 << 8) - 1) as f32;
2654        for row in y as usize..(y + height) as usize {
2655            let row_base = row * image_width;
2656            for col in x as usize..(x + width) as usize {
2657                let idx = row_base + col;
2658                for component in components.iter() {
2659                    *output_iter.next().unwrap() = math::round_f32(
2660                        (component.container[idx] / ((1_u32 << component.bit_depth) - 1) as f32)
2661                            * mul_factor,
2662                    ) as u8;
2663                }
2664            }
2665        }
2666    }
2667}
2668
2669fn validate_roi(dims: (u32, u32), roi: (u32, u32, u32, u32)) -> Result<()> {
2670    let (image_width, image_height) = dims;
2671    let (x, y, width, height) = roi;
2672    let x_end = x
2673        .checked_add(width)
2674        .ok_or(ValidationError::InvalidDimensions)?;
2675    let y_end = y
2676        .checked_add(height)
2677        .ok_or(ValidationError::InvalidDimensions)?;
2678    if x_end > image_width || y_end > image_height {
2679        return Err(ValidationError::InvalidDimensions.into());
2680    }
2681    Ok(())
2682}
2683
2684fn convert_color_space(image: &mut DecodedImage<'_>, bit_depth: u8) -> Result<()> {
2685    if let Some(jp2::colr::ColorSpace::Enumerated(e)) = &image
2686        .boxes
2687        .color_specification
2688        .as_ref()
2689        .map(|i| &i.color_space)
2690    {
2691        match e {
2692            EnumeratedColorspace::Sycc => {
2693                dispatch!(Level::new(), simd => {
2694                    sycc_to_rgb(simd, image.decoded_components, bit_depth)
2695                })?;
2696            }
2697            EnumeratedColorspace::CieLab(cielab) => {
2698                dispatch!(Level::new(), simd => {
2699                    cielab_to_rgb(simd, image.decoded_components, bit_depth, cielab)
2700                })?;
2701            }
2702            _ => {}
2703        }
2704    }
2705
2706    Ok(())
2707}
2708
2709fn get_color_space(boxes: &ImageBoxes, num_components: usize) -> Result<ColorSpace> {
2710    let cs = match boxes
2711        .color_specification
2712        .as_ref()
2713        .map(|c| &c.color_space)
2714        .unwrap_or(&jp2::colr::ColorSpace::Unknown)
2715    {
2716        jp2::colr::ColorSpace::Enumerated(e) => {
2717            match e {
2718                EnumeratedColorspace::Cmyk => ColorSpace::CMYK,
2719                EnumeratedColorspace::Srgb => ColorSpace::RGB,
2720                EnumeratedColorspace::RommRgb => {
2721                    // Use an ICC profile to process the RommRGB color space.
2722                    ColorSpace::Icc {
2723                        profile: include_bytes!("../assets/ProPhoto-v2-micro.icc").to_vec(),
2724                        num_channels: 3,
2725                    }
2726                }
2727                EnumeratedColorspace::EsRgb => ColorSpace::RGB,
2728                EnumeratedColorspace::Greyscale => ColorSpace::Gray,
2729                EnumeratedColorspace::Sycc => ColorSpace::RGB,
2730                EnumeratedColorspace::CieLab(_) => ColorSpace::Icc {
2731                    profile: include_bytes!("../assets/LAB.icc").to_vec(),
2732                    num_channels: 3,
2733                },
2734                _ => bail!(FormatError::Unsupported),
2735            }
2736        }
2737        jp2::colr::ColorSpace::Icc(icc) => {
2738            if let Some(metadata) = ICCMetadata::from_data(icc) {
2739                ColorSpace::Icc {
2740                    profile: icc.clone(),
2741                    num_channels: metadata.color_space.num_components(),
2742                }
2743            } else {
2744                // See OPENJPEG test orb-blue10-lin-jp2.jp2. They seem to
2745                // assume RGB in this case (even though the image has 4
2746                // components with no opacity channel, they assume RGBA instead
2747                // of CMYK).
2748                ColorSpace::RGB
2749            }
2750        }
2751        jp2::colr::ColorSpace::Unknown => match num_components {
2752            1 => ColorSpace::Gray,
2753            3 => ColorSpace::RGB,
2754            4 => ColorSpace::CMYK,
2755            _ => ColorSpace::Unknown {
2756                num_channels: num_components as u8,
2757            },
2758        },
2759    };
2760
2761    Ok(cs)
2762}
2763
2764fn resolve_palette_indices(
2765    components: Vec<ComponentData>,
2766    boxes: &ImageBoxes,
2767) -> Result<Vec<ComponentData>> {
2768    let Some(palette) = boxes.palette.as_ref() else {
2769        // Nothing to resolve.
2770        return Ok(components);
2771    };
2772
2773    let Some(mapping) = boxes.component_mapping.as_ref() else {
2774        bail!(ColorError::PaletteResolutionFailed);
2775    };
2776    if mapping.entries.is_empty() {
2777        bail!(ColorError::PaletteResolutionFailed);
2778    }
2779
2780    let mut resolved = Vec::with_capacity(mapping.entries.len());
2781
2782    for entry in &mapping.entries {
2783        let component_idx = entry.component_index as usize;
2784        let component = components
2785            .get(component_idx)
2786            .ok_or(ColorError::PaletteResolutionFailed)?;
2787
2788        match entry.mapping_type {
2789            ComponentMappingType::Direct => resolved.push(component.clone()),
2790            ComponentMappingType::Palette { column } => {
2791                let column_idx = column as usize;
2792                let column_info = palette
2793                    .columns
2794                    .get(column_idx)
2795                    .ok_or(ColorError::PaletteResolutionFailed)?;
2796
2797                let mut mapped =
2798                    Vec::with_capacity(component.container.truncated().len() + SIMD_WIDTH);
2799
2800                for &sample in component.container.truncated() {
2801                    let index = math::round_f32(sample) as i64;
2802                    let value = palette
2803                        .map(index as usize, column_idx)
2804                        .ok_or(ColorError::PaletteResolutionFailed)?;
2805                    mapped.push(value as f32);
2806                }
2807
2808                resolved.push(ComponentData {
2809                    container: math::SimdBuffer::new(mapped),
2810                    bit_depth: column_info.bit_depth,
2811                });
2812            }
2813        }
2814    }
2815
2816    Ok(resolved)
2817}
2818
2819#[inline(always)]
2820fn cielab_to_rgb<S: Simd>(
2821    simd: S,
2822    components: &mut [ComponentData],
2823    bit_depth: u8,
2824    lab: &CieLab,
2825) -> Result<()> {
2826    let (head, _) = components
2827        .split_at_mut_checked(3)
2828        .ok_or(ColorError::LabConversionFailed)?;
2829
2830    let [l, a, b] = head else {
2831        bail!(ColorError::LabConversionFailed);
2832    };
2833
2834    let prec0 = l.bit_depth;
2835    let prec1 = a.bit_depth;
2836    let prec2 = b.bit_depth;
2837
2838    // Prevent underflows/divisions by zero further below.
2839    if prec0 < 4 || prec1 < 4 || prec2 < 4 {
2840        bail!(ColorError::LabConversionFailed);
2841    }
2842
2843    let rl = lab.rl.unwrap_or(100);
2844    let ra = lab.ra.unwrap_or(170);
2845    let rb = lab.ra.unwrap_or(200);
2846    let ol = lab.ol.unwrap_or(0);
2847    let oa = lab.oa.unwrap_or(1 << (bit_depth - 1));
2848    let ob = lab
2849        .ob
2850        .unwrap_or((1 << (bit_depth - 2)) + (1 << (bit_depth - 3)));
2851
2852    // Copied from OpenJPEG.
2853    let min_l = -(rl as f32 * ol as f32) / ((1 << prec0) - 1) as f32;
2854    let max_l = min_l + rl as f32;
2855    let min_a = -(ra as f32 * oa as f32) / ((1 << prec1) - 1) as f32;
2856    let max_a = min_a + ra as f32;
2857    let min_b = -(rb as f32 * ob as f32) / ((1 << prec2) - 1) as f32;
2858    let max_b = min_b + rb as f32;
2859
2860    let bit_max = (1_u32 << bit_depth) - 1;
2861
2862    // Note that we are not doing the actual conversion with the ICC profile yet,
2863    // just decoding the raw LAB values.
2864    // We leave applying the ICC profile to the user.
2865    let divisor_l = ((1 << prec0) - 1) as f32;
2866    let divisor_a = ((1 << prec1) - 1) as f32;
2867    let divisor_b = ((1 << prec2) - 1) as f32;
2868
2869    let scale_l_final = bit_max as f32 / 100.0;
2870    let scale_ab_final = bit_max as f32 / 255.0;
2871
2872    let l_offset = min_l * scale_l_final;
2873    let l_scale = (max_l - min_l) / divisor_l * scale_l_final;
2874    let a_offset = (min_a + 128.0) * scale_ab_final;
2875    let a_scale = (max_a - min_a) / divisor_a * scale_ab_final;
2876    let b_offset = (min_b + 128.0) * scale_ab_final;
2877    let b_scale = (max_b - min_b) / divisor_b * scale_ab_final;
2878
2879    let l_offset_v = f32x8::splat(simd, l_offset);
2880    let l_scale_v = f32x8::splat(simd, l_scale);
2881    let a_offset_v = f32x8::splat(simd, a_offset);
2882    let a_scale_v = f32x8::splat(simd, a_scale);
2883    let b_offset_v = f32x8::splat(simd, b_offset);
2884    let b_scale_v = f32x8::splat(simd, b_scale);
2885
2886    // Note that we are not doing the actual conversion with the ICC profile yet,
2887    // just decoding the raw LAB values.
2888    // We leave applying the ICC profile to the user.
2889    for ((l_chunk, a_chunk), b_chunk) in l
2890        .container
2891        .chunks_exact_mut(SIMD_WIDTH)
2892        .zip(a.container.chunks_exact_mut(SIMD_WIDTH))
2893        .zip(b.container.chunks_exact_mut(SIMD_WIDTH))
2894    {
2895        let l_v = f32x8::from_slice(simd, l_chunk);
2896        let a_v = f32x8::from_slice(simd, a_chunk);
2897        let b_v = f32x8::from_slice(simd, b_chunk);
2898
2899        l_v.mul_add(l_scale_v, l_offset_v).store(l_chunk);
2900        a_v.mul_add(a_scale_v, a_offset_v).store(a_chunk);
2901        b_v.mul_add(b_scale_v, b_offset_v).store(b_chunk);
2902    }
2903
2904    Ok(())
2905}
2906
2907#[inline(always)]
2908fn sycc_to_rgb<S: Simd>(simd: S, components: &mut [ComponentData], bit_depth: u8) -> Result<()> {
2909    let offset = (1_u32 << (bit_depth as u32 - 1)) as f32;
2910    let max_value = ((1_u32 << bit_depth as u32) - 1) as f32;
2911
2912    let (head, _) = components
2913        .split_at_mut_checked(3)
2914        .ok_or(ColorError::SyccConversionFailed)?;
2915
2916    let [y, cb, cr] = head else {
2917        bail!(ColorError::SyccConversionFailed);
2918    };
2919
2920    let offset_v = f32x8::splat(simd, offset);
2921    let max_v = f32x8::splat(simd, max_value);
2922    let zero_v = f32x8::splat(simd, 0.0);
2923    let cr_to_r = f32x8::splat(simd, 1.402);
2924    let cb_to_g = f32x8::splat(simd, -0.344136);
2925    let cr_to_g = f32x8::splat(simd, -0.714136);
2926    let cb_to_b = f32x8::splat(simd, 1.772);
2927
2928    for ((y_chunk, cb_chunk), cr_chunk) in y
2929        .container
2930        .chunks_exact_mut(SIMD_WIDTH)
2931        .zip(cb.container.chunks_exact_mut(SIMD_WIDTH))
2932        .zip(cr.container.chunks_exact_mut(SIMD_WIDTH))
2933    {
2934        let y_v = f32x8::from_slice(simd, y_chunk);
2935        let cb_v = f32x8::from_slice(simd, cb_chunk) - offset_v;
2936        let cr_v = f32x8::from_slice(simd, cr_chunk) - offset_v;
2937
2938        // r = y + 1.402 * cr
2939        let r = cr_v.mul_add(cr_to_r, y_v);
2940        // g = y - 0.344136 * cb - 0.714136 * cr
2941        let g = cr_v.mul_add(cr_to_g, cb_v.mul_add(cb_to_g, y_v));
2942        // b = y + 1.772 * cb
2943        let b = cb_v.mul_add(cb_to_b, y_v);
2944
2945        r.min(max_v).max(zero_v).store(y_chunk);
2946        g.min(max_v).max(zero_v).store(cb_chunk);
2947        b.min(max_v).max(zero_v).store(cr_chunk);
2948    }
2949
2950    Ok(())
2951}
2952
2953#[cfg(test)]
2954mod tests {
2955    use super::*;
2956
2957    #[test]
2958    fn ht_uvlc_encode_table_bytes_match_entry_packing_order() {
2959        let entries = ht_uvlc_encode_table();
2960        let bytes = ht_uvlc_encode_table_bytes();
2961
2962        assert_eq!(bytes.len(), entries.len() * 6);
2963        for (index, entry) in entries.iter().enumerate() {
2964            let offset = index * 6;
2965            assert_eq!(
2966                &bytes[offset..offset + 6],
2967                &[
2968                    entry.pre,
2969                    entry.pre_len,
2970                    entry.suf,
2971                    entry.suf_len,
2972                    entry.ext,
2973                    entry.ext_len
2974                ],
2975            );
2976        }
2977    }
2978
2979    #[test]
2980    fn roi_maxshift_inverse_preserves_background_and_unshifts_roi_coefficients() {
2981        assert_eq!(apply_roi_maxshift_inverse_i32(127, 7), 127);
2982        assert_eq!(apply_roi_maxshift_inverse_i32(-127, 7), -127);
2983        assert_eq!(apply_roi_maxshift_inverse_i32(128, 7), 1);
2984        assert_eq!(apply_roi_maxshift_inverse_i32(-128, 7), -1);
2985        assert_eq!(apply_roi_maxshift_inverse_i32(255, 7), 1);
2986        assert_eq!(apply_roi_maxshift_inverse_i32(-255, 7), -1);
2987        assert_eq!(apply_roi_maxshift_inverse_i32(256, 7), 2);
2988        assert_eq!(apply_roi_maxshift_inverse_i32(-256, 7), -2);
2989        assert_eq!(apply_roi_maxshift_inverse_i32(42, 0), 42);
2990    }
2991
2992    #[test]
2993    fn classic_scalar_decode_applies_nonzero_roi_maxshift() {
2994        let roi_shift = 3;
2995        let total_bitplanes = 3;
2996        let style = J2kCodeBlockStyle {
2997            selective_arithmetic_coding_bypass: false,
2998            reset_context_probabilities: false,
2999            termination_on_each_pass: false,
3000            vertically_causal_context: false,
3001            segmentation_symbols: false,
3002        };
3003        let coded_coefficients = [0, 5, 1 << roi_shift, -(2 << roi_shift)];
3004        let encoded = encode_j2k_code_block_scalar_with_style(
3005            &coded_coefficients,
3006            2,
3007            2,
3008            J2kSubBandType::LowLow,
3009            total_bitplanes + roi_shift,
3010            style,
3011        )
3012        .expect("encode ROI-shifted code block");
3013        let job = J2kCodeBlockDecodeJob {
3014            data: &encoded.data,
3015            segments: &encoded.segments,
3016            width: 2,
3017            height: 2,
3018            output_stride: 2,
3019            missing_bit_planes: encoded.missing_bit_planes,
3020            number_of_coding_passes: encoded.number_of_coding_passes,
3021            total_bitplanes,
3022            roi_shift,
3023            sub_band_type: J2kSubBandType::LowLow,
3024            style,
3025            strict: true,
3026            dequantization_step: 1.0,
3027        };
3028        let mut output = [0.0; 4];
3029
3030        decode_j2k_code_block_scalar(job, &mut output).expect("decode ROI-shifted code block");
3031
3032        assert_eq!(output, [0.0, 5.0, 1.0, -2.0]);
3033    }
3034
3035    #[test]
3036    fn classic_scalar_token_pack_matches_scalar_single_cleanup_block() {
3037        let style = J2kCodeBlockStyle {
3038            selective_arithmetic_coding_bypass: true,
3039            reset_context_probabilities: false,
3040            termination_on_each_pass: false,
3041            vertically_causal_context: false,
3042            segmentation_symbols: false,
3043        };
3044        let scalar =
3045            encode_j2k_code_block_scalar_with_style(&[1], 1, 1, J2kSubBandType::LowLow, 1, style)
3046                .expect("encode scalar");
3047        let token_bytes = pack_mq_test_tokens(&[(0, 1), (9, 0)]);
3048        let packed = pack_j2k_code_block_scalar_from_tier1_tokens(
3049            &token_bytes,
3050            &[J2kTier1TokenSegment {
3051                token_bit_offset: 0,
3052                token_bit_count: 12,
3053                start_coding_pass: 0,
3054                end_coding_pass: 1,
3055                use_arithmetic: true,
3056            }],
3057            scalar.number_of_coding_passes,
3058            scalar.missing_bit_planes,
3059        )
3060        .expect("pack tokens");
3061
3062        assert_eq!(packed.data, scalar.data);
3063        assert_eq!(packed.segments, scalar.segments);
3064        assert_eq!(
3065            packed.number_of_coding_passes,
3066            scalar.number_of_coding_passes
3067        );
3068        assert_eq!(packed.missing_bit_planes, scalar.missing_bit_planes);
3069    }
3070
3071    fn pack_mq_test_tokens(tokens: &[(u8, u8)]) -> Vec<u8> {
3072        let mut bytes = Vec::new();
3073        let mut current = 0u8;
3074        let mut bits = 0u8;
3075        for &(ctx, bit) in tokens {
3076            let value = (ctx & 0x1F) | ((bit & 1) << 5);
3077            for shift in (0..6).rev() {
3078                current = (current << 1) | ((value >> shift) & 1);
3079                bits += 1;
3080                if bits == 8 {
3081                    bytes.push(current);
3082                    current = 0;
3083                    bits = 0;
3084                }
3085            }
3086        }
3087        if bits != 0 {
3088            bytes.push(current << (8 - bits));
3089        }
3090        bytes
3091    }
3092
3093    #[test]
3094    fn classic_scalar_profiled_decode_matches_unprofiled_decode() {
3095        let width = 64u32;
3096        let height = 64u32;
3097        let sample_count = width as usize * height as usize;
3098        let total_bitplanes = 12;
3099        let style = J2kCodeBlockStyle {
3100            selective_arithmetic_coding_bypass: false,
3101            reset_context_probabilities: false,
3102            termination_on_each_pass: false,
3103            vertically_causal_context: false,
3104            segmentation_symbols: false,
3105        };
3106        let coefficients = (0..sample_count)
3107            .map(|idx| {
3108                let value = i32::try_from((idx * 37) % 4095).expect("sample value fits i32") - 2048;
3109                if idx % 17 == 0 {
3110                    0
3111                } else {
3112                    value
3113                }
3114            })
3115            .collect::<Vec<_>>();
3116        let encoded = encode_j2k_code_block_scalar_with_style(
3117            &coefficients,
3118            width,
3119            height,
3120            J2kSubBandType::LowLow,
3121            total_bitplanes,
3122            style,
3123        )
3124        .expect("encode classic block");
3125        let job = J2kCodeBlockDecodeJob {
3126            data: &encoded.data,
3127            segments: &encoded.segments,
3128            width,
3129            height,
3130            output_stride: width as usize,
3131            missing_bit_planes: encoded.missing_bit_planes,
3132            number_of_coding_passes: encoded.number_of_coding_passes,
3133            total_bitplanes,
3134            roi_shift: 0,
3135            sub_band_type: J2kSubBandType::LowLow,
3136            style,
3137            strict: true,
3138            dequantization_step: 1.0,
3139        };
3140        let mut expected = vec![0.0_f32; sample_count];
3141        let mut actual = vec![0.0_f32; sample_count];
3142        let mut profile = J2kCodeBlockDecodeProfile::default();
3143
3144        decode_j2k_code_block_scalar(job, &mut expected).expect("unprofiled classic decode");
3145        decode_j2k_code_block_scalar_profiled(job, &mut actual, &mut profile)
3146            .expect("profiled classic decode");
3147
3148        assert_eq!(actual, expected);
3149        assert!(profile.cleanup_us > 0);
3150    }
3151
3152    #[test]
3153    fn classic_scalar_workspace_reuse_matches_fresh_decode() {
3154        let total_bitplanes = 6;
3155        let style = J2kCodeBlockStyle {
3156            selective_arithmetic_coding_bypass: false,
3157            reset_context_probabilities: false,
3158            termination_on_each_pass: false,
3159            vertically_causal_context: false,
3160            segmentation_symbols: false,
3161        };
3162        let mut workspace = J2kCodeBlockDecodeWorkspace::default();
3163
3164        for (width, height, seed) in [(8, 8, 0x31), (4, 16, 0x47)] {
3165            let coefficients = (0..width * height)
3166                .map(|idx| {
3167                    let value = ((idx as i32 * seed) % 23) - 11;
3168                    if idx % 7 == 0 {
3169                        0
3170                    } else {
3171                        value
3172                    }
3173                })
3174                .collect::<Vec<_>>();
3175            let encoded = encode_j2k_code_block_scalar_with_style(
3176                &coefficients,
3177                width,
3178                height,
3179                J2kSubBandType::LowLow,
3180                total_bitplanes,
3181                style,
3182            )
3183            .expect("encode classic block");
3184            let job = J2kCodeBlockDecodeJob {
3185                data: &encoded.data,
3186                segments: &encoded.segments,
3187                width,
3188                height,
3189                output_stride: width as usize,
3190                missing_bit_planes: encoded.missing_bit_planes,
3191                number_of_coding_passes: encoded.number_of_coding_passes,
3192                total_bitplanes,
3193                roi_shift: 0,
3194                sub_band_type: J2kSubBandType::LowLow,
3195                style,
3196                strict: true,
3197                dequantization_step: 1.0,
3198            };
3199            let mut fresh = vec![0.0_f32; width as usize * height as usize];
3200            let mut reused = vec![0.0_f32; width as usize * height as usize];
3201
3202            decode_j2k_code_block_scalar(job, &mut fresh).expect("fresh classic decode");
3203            decode_j2k_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
3204                .expect("workspace classic decode");
3205
3206            assert_eq!(reused, fresh);
3207        }
3208    }
3209
3210    #[test]
3211    fn scalar_packetization_rejects_overflowing_ht_refinement_lengths_without_panic() {
3212        let payload = [0x12];
3213        let block = J2kPacketizationCodeBlock {
3214            data: &payload,
3215            ht_cleanup_length: u32::MAX,
3216            ht_refinement_length: 1,
3217            num_coding_passes: 3,
3218            num_zero_bitplanes: 2,
3219            previously_included: false,
3220            l_block: 3,
3221            block_coding_mode: J2kPacketizationBlockCodingMode::HighThroughput,
3222        };
3223        let subband = J2kPacketizationSubband {
3224            code_blocks: vec![block],
3225            num_cbs_x: 1,
3226            num_cbs_y: 1,
3227        };
3228        let resolution = J2kPacketizationResolution {
3229            subbands: vec![subband],
3230        };
3231        let resolutions = [resolution];
3232        let job = J2kPacketizationEncodeJob {
3233            resolution_count: 1,
3234            num_layers: 1,
3235            num_components: 1,
3236            code_block_count: 1,
3237            progression_order: J2kPacketizationProgressionOrder::Lrcp,
3238            packet_descriptors: &[],
3239            resolutions: &resolutions,
3240        };
3241
3242        let err = encode_j2k_packetization_scalar(job)
3243            .expect_err("overflowing HT packetization segment lengths rejected");
3244
3245        assert_eq!(err, "multi-pass HTJ2K packet contribution length overflow");
3246    }
3247
3248    #[derive(Default)]
3249    struct DecodeWorkCounter {
3250        classic_code_blocks: usize,
3251        ht_code_blocks: usize,
3252        idwt_output_samples: usize,
3253    }
3254
3255    impl DecodeWorkCounter {
3256        fn code_blocks(&self) -> usize {
3257            self.classic_code_blocks + self.ht_code_blocks
3258        }
3259    }
3260
3261    struct FailingHtDecoder {
3262        called: bool,
3263    }
3264
3265    impl HtCodeBlockDecoder for FailingHtDecoder {
3266        fn decode_code_block(
3267            &mut self,
3268            _job: HtCodeBlockDecodeJob<'_>,
3269            _output: &mut [f32],
3270        ) -> Result<()> {
3271            self.called = true;
3272            Err(DecodingError::CodeBlockDecodeFailure.into())
3273        }
3274    }
3275
3276    struct FailingClassicDecoder {
3277        called: bool,
3278    }
3279
3280    impl HtCodeBlockDecoder for FailingClassicDecoder {
3281        fn decode_code_block(
3282            &mut self,
3283            _job: HtCodeBlockDecodeJob<'_>,
3284            _output: &mut [f32],
3285        ) -> Result<()> {
3286            panic!("HT hook must not be used for classic J2K test")
3287        }
3288
3289        fn decode_j2k_code_block(
3290            &mut self,
3291            _job: J2kCodeBlockDecodeJob<'_>,
3292            _output: &mut [f32],
3293        ) -> Result<bool> {
3294            self.called = true;
3295            Err(DecodingError::CodeBlockDecodeFailure.into())
3296        }
3297    }
3298
3299    struct FailingClassicBatchDecoder {
3300        called: bool,
3301    }
3302
3303    #[derive(Default)]
3304    struct CapturingHtDecoder {
3305        called: bool,
3306        blocks: usize,
3307        refinement_jobs: usize,
3308        max_coding_passes: u8,
3309    }
3310
3311    impl HtCodeBlockDecoder for CapturingHtDecoder {
3312        fn decode_code_block(
3313            &mut self,
3314            job: HtCodeBlockDecodeJob<'_>,
3315            output: &mut [f32],
3316        ) -> Result<()> {
3317            self.called = true;
3318            self.blocks += 1;
3319            self.max_coding_passes = self.max_coding_passes.max(job.number_of_coding_passes);
3320            if job.refinement_length > 0 {
3321                self.refinement_jobs += 1;
3322                assert!(
3323                    job.number_of_coding_passes > 1,
3324                    "refinement bytes must correspond to refinement coding passes"
3325                );
3326            }
3327
3328            decode_ht_code_block_scalar(job, output)
3329        }
3330    }
3331
3332    #[derive(Clone)]
3333    struct CapturedHtDecodeJob {
3334        data: Vec<u8>,
3335        cleanup_length: u32,
3336        refinement_length: u32,
3337        width: u32,
3338        height: u32,
3339        output_stride: usize,
3340        missing_bit_planes: u8,
3341        number_of_coding_passes: u8,
3342        num_bitplanes: u8,
3343        roi_shift: u8,
3344        stripe_causal: bool,
3345        strict: bool,
3346        dequantization_step: f32,
3347    }
3348
3349    impl CapturedHtDecodeJob {
3350        fn from_job(job: HtCodeBlockDecodeJob<'_>) -> Self {
3351            Self {
3352                data: job.data.to_vec(),
3353                cleanup_length: job.cleanup_length,
3354                refinement_length: job.refinement_length,
3355                width: job.width,
3356                height: job.height,
3357                output_stride: job.output_stride,
3358                missing_bit_planes: job.missing_bit_planes,
3359                number_of_coding_passes: job.number_of_coding_passes,
3360                num_bitplanes: job.num_bitplanes,
3361                roi_shift: job.roi_shift,
3362                stripe_causal: job.stripe_causal,
3363                strict: job.strict,
3364                dequantization_step: job.dequantization_step,
3365            }
3366        }
3367
3368        fn borrowed(&self) -> HtCodeBlockDecodeJob<'_> {
3369            HtCodeBlockDecodeJob {
3370                data: &self.data,
3371                cleanup_length: self.cleanup_length,
3372                refinement_length: self.refinement_length,
3373                width: self.width,
3374                height: self.height,
3375                output_stride: self.output_stride,
3376                missing_bit_planes: self.missing_bit_planes,
3377                number_of_coding_passes: self.number_of_coding_passes,
3378                num_bitplanes: self.num_bitplanes,
3379                roi_shift: self.roi_shift,
3380                stripe_causal: self.stripe_causal,
3381                strict: self.strict,
3382                dequantization_step: self.dequantization_step,
3383            }
3384        }
3385    }
3386
3387    #[derive(Default)]
3388    struct FirstHtJobDecoder {
3389        job: Option<CapturedHtDecodeJob>,
3390    }
3391
3392    impl HtCodeBlockDecoder for FirstHtJobDecoder {
3393        fn decode_code_block(
3394            &mut self,
3395            job: HtCodeBlockDecodeJob<'_>,
3396            output: &mut [f32],
3397        ) -> Result<()> {
3398            if self.job.is_none() {
3399                self.job = Some(CapturedHtDecodeJob::from_job(job));
3400            }
3401            decode_ht_code_block_scalar(job, output)
3402        }
3403    }
3404
3405    struct ZeroRefinementHtDecoder;
3406
3407    impl HtCodeBlockDecoder for ZeroRefinementHtDecoder {
3408        fn decode_code_block(
3409            &mut self,
3410            job: HtCodeBlockDecodeJob<'_>,
3411            output: &mut [f32],
3412        ) -> Result<()> {
3413            let mut data = job.data.to_vec();
3414            let cleanup_len = job.cleanup_length as usize;
3415            let refinement_len = job.refinement_length as usize;
3416            data[cleanup_len..cleanup_len + refinement_len].fill(0);
3417            let zeroed = HtCodeBlockDecodeJob { data: &data, ..job };
3418
3419            decode_ht_code_block_scalar(zeroed, output)
3420        }
3421    }
3422
3423    #[derive(Default)]
3424    struct CleanupLimitedHtDecoder {
3425        blocks: usize,
3426        refinement_blocks: usize,
3427        cleanup_bytes: usize,
3428        refinement_bytes: usize,
3429    }
3430
3431    impl HtCodeBlockDecoder for CleanupLimitedHtDecoder {
3432        fn decode_code_block(
3433            &mut self,
3434            job: HtCodeBlockDecodeJob<'_>,
3435            output: &mut [f32],
3436        ) -> Result<()> {
3437            self.blocks += 1;
3438            self.cleanup_bytes += job.cleanup_length as usize;
3439            if job.refinement_length > 0 {
3440                self.refinement_blocks += 1;
3441                self.refinement_bytes += job.refinement_length as usize;
3442            }
3443
3444            decode_ht_code_block_scalar_until_phase(
3445                job,
3446                output,
3447                HtCodeBlockDecodePhaseLimit::Cleanup,
3448            )
3449        }
3450    }
3451
3452    impl HtCodeBlockDecoder for FailingClassicBatchDecoder {
3453        fn decode_code_block(
3454            &mut self,
3455            _job: HtCodeBlockDecodeJob<'_>,
3456            _output: &mut [f32],
3457        ) -> Result<()> {
3458            panic!("HT hook must not be used for classic J2K batch test")
3459        }
3460
3461        fn decode_j2k_code_block(
3462            &mut self,
3463            _job: J2kCodeBlockDecodeJob<'_>,
3464            _output: &mut [f32],
3465        ) -> Result<bool> {
3466            panic!(
3467                "per-block classic hook must not be used when the batch hook handles the sub-band"
3468            )
3469        }
3470
3471        fn decode_j2k_sub_band(
3472            &mut self,
3473            _job: J2kSubBandDecodeJob<'_>,
3474            _output: &mut [f32],
3475        ) -> Result<bool> {
3476            self.called = true;
3477            Err(DecodingError::CodeBlockDecodeFailure.into())
3478        }
3479    }
3480
3481    fn fixture() -> Vec<u8> {
3482        let pixels = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
3483        let options = EncodeOptions {
3484            reversible: true,
3485            num_decomposition_levels: 1,
3486            ..EncodeOptions::default()
3487        };
3488        encode(&pixels, 2, 2, 3, 8, false, &options).expect("encode")
3489    }
3490
3491    #[test]
3492    fn decode_into_rejects_short_output_buffer() {
3493        let bytes = fixture();
3494        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3495        let mut context = DecoderContext::default();
3496        let mut output = vec![0; 11];
3497
3498        let err = image
3499            .decode_into(&mut output, &mut context)
3500            .expect_err("short output buffer must be rejected");
3501
3502        assert_eq!(
3503            err,
3504            DecodeError::Decoding(DecodingError::OutputBufferTooSmall)
3505        );
3506    }
3507
3508    fn fixture_multi_block() -> Vec<u8> {
3509        let pixels: Vec<u8> = (0..64).collect();
3510        let options = EncodeOptions {
3511            reversible: true,
3512            num_decomposition_levels: 0,
3513            code_block_width_exp: 0,
3514            code_block_height_exp: 0,
3515            ..EncodeOptions::default()
3516        };
3517        encode(&pixels, 8, 8, 1, 8, false, &options).expect("encode multi-block classic")
3518    }
3519
3520    fn fixture_gray() -> Vec<u8> {
3521        let pixels: Vec<u8> = (0..16).collect();
3522        let options = EncodeOptions {
3523            reversible: true,
3524            num_decomposition_levels: 1,
3525            ..EncodeOptions::default()
3526        };
3527        encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode classic gray8")
3528    }
3529
3530    fn rewrite_siz_to_single_large_tile(codestream: &mut [u8], dimensions: u32) {
3531        let siz = codestream
3532            .windows(2)
3533            .position(|w| w == [0xFF, 0x51])
3534            .expect("SIZ marker");
3535        codestream[siz + 6..siz + 10].copy_from_slice(&dimensions.to_be_bytes());
3536        codestream[siz + 10..siz + 14].copy_from_slice(&dimensions.to_be_bytes());
3537        codestream[siz + 22..siz + 26].copy_from_slice(&dimensions.to_be_bytes());
3538        codestream[siz + 26..siz + 30].copy_from_slice(&dimensions.to_be_bytes());
3539    }
3540
3541    fn rewrite_siz_tile_grid(codestream: &mut [u8], dimensions: (u32, u32), tile_size: (u32, u32)) {
3542        let siz = codestream
3543            .windows(2)
3544            .position(|w| w == [0xFF, 0x51])
3545            .expect("SIZ marker");
3546        codestream[siz + 6..siz + 10].copy_from_slice(&dimensions.0.to_be_bytes());
3547        codestream[siz + 10..siz + 14].copy_from_slice(&dimensions.1.to_be_bytes());
3548        codestream[siz + 22..siz + 26].copy_from_slice(&tile_size.0.to_be_bytes());
3549        codestream[siz + 26..siz + 30].copy_from_slice(&tile_size.1.to_be_bytes());
3550    }
3551
3552    fn rewrite_siz_component_count(codestream: &mut Vec<u8>, component_count: u16) {
3553        let siz = codestream
3554            .windows(2)
3555            .position(|w| w == [0xFF, 0x51])
3556            .expect("SIZ marker");
3557        let old_component_count =
3558            u16::from_be_bytes([codestream[siz + 38], codestream[siz + 39]]) as usize;
3559        let component_start = siz + 40;
3560        let component_end = component_start + old_component_count * 3;
3561        let descriptor = codestream[component_start..component_start + 3].to_vec();
3562        let mut descriptors = Vec::with_capacity(usize::from(component_count) * 3);
3563        for _ in 0..component_count {
3564            descriptors.extend_from_slice(&descriptor);
3565        }
3566
3567        let siz_len = 38_u16
3568            .checked_add(
3569                component_count
3570                    .checked_mul(3)
3571                    .expect("SIZ component bytes fit"),
3572            )
3573            .expect("SIZ length fits");
3574        codestream[siz + 2..siz + 4].copy_from_slice(&siz_len.to_be_bytes());
3575        codestream[siz + 38..siz + 40].copy_from_slice(&component_count.to_be_bytes());
3576        codestream.splice(component_start..component_end, descriptors);
3577    }
3578
3579    #[test]
3580    fn inspect_rejects_component_count_above_j2k_spec_cap() {
3581        let mut bytes = fixture_gray();
3582        rewrite_siz_component_count(&mut bytes, MAX_J2K_SPEC_COMPONENTS + 1);
3583
3584        let err = inspect_j2k_codestream_header(&bytes)
3585            .expect_err("SIZ component count above spec cap must be rejected");
3586
3587        assert_eq!(
3588            err,
3589            J2kCodestreamHeaderError::InvalidSiz {
3590                what: "component count exceeds JPEG 2000 limit"
3591            }
3592        );
3593    }
3594
3595    #[test]
3596    fn native_decode_rejects_component_count_above_u8_before_bitmap_truncation() {
3597        let mut bytes = fixture_gray();
3598        rewrite_siz_component_count(&mut bytes, MAX_NATIVE_DECODE_COMPONENTS + 1);
3599
3600        let err = match Image::new(&bytes, &DecodeSettings::default()) {
3601            Err(err) => err,
3602            Ok(_) => {
3603                panic!("native decode must reject component counts that cannot fit RawBitmap")
3604            }
3605        };
3606
3607        assert_eq!(
3608            err,
3609            DecodeError::Validation(ValidationError::TooManyChannels)
3610        );
3611    }
3612
3613    #[test]
3614    fn tile_parse_rejects_component_tile_structural_bomb_before_allocation() {
3615        let mut bytes = fixture_gray();
3616        rewrite_siz_component_count(&mut bytes, MAX_NATIVE_DECODE_COMPONENTS);
3617        rewrite_siz_tile_grid(&mut bytes, (256, 256), (1, 1));
3618        let parsed = j2c::parse_raw(&bytes, &DecodeSettings::default()).expect("raw header parses");
3619        let mut context = j2c::DecoderContext::default();
3620        let mut ht_decoder: Option<&mut dyn HtCodeBlockDecoder> = None;
3621
3622        let err = j2c::decode(parsed.data, &parsed.header, &mut context, &mut ht_decoder)
3623            .expect_err("tile structural budget must reject before tile allocation");
3624
3625        assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
3626    }
3627
3628    #[test]
3629    fn owned_decode_rejects_large_siz_before_allocating_output() {
3630        let mut bytes = fixture_gray();
3631        rewrite_siz_to_single_large_tile(&mut bytes, 60_000);
3632        let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
3633
3634        let err = match image.decode() {
3635            Err(err) => err,
3636            Ok(_) => panic!("large owned decode must be capped"),
3637        };
3638
3639        assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
3640    }
3641
3642    #[test]
3643    fn decode_into_rejects_large_siz_before_allocating_component_storage() {
3644        let mut bytes = fixture_gray();
3645        rewrite_siz_to_single_large_tile(&mut bytes, 60_000);
3646        let image = Image::new(&bytes, &DecodeSettings::default()).expect("large SIZ parses");
3647        let mut context = DecoderContext::default();
3648        let mut out = [];
3649
3650        let err = match image.decode_into(&mut out, &mut context) {
3651            Err(err) => err,
3652            Ok(_) => panic!("component storage must be capped before allocation"),
3653        };
3654
3655        assert_eq!(err, DecodeError::Validation(ValidationError::ImageTooLarge));
3656    }
3657
3658    fn fixture_ht_gray() -> Vec<u8> {
3659        let pixels: Vec<u8> = (0..16).collect();
3660        let options = EncodeOptions {
3661            reversible: true,
3662            num_decomposition_levels: 1,
3663            ..EncodeOptions::default()
3664        };
3665        encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode ht gray8")
3666    }
3667
3668    fn fixture_ht_multi_block() -> Vec<u8> {
3669        let pixels: Vec<u8> = (0..64).collect();
3670        let options = EncodeOptions {
3671            reversible: true,
3672            num_decomposition_levels: 0,
3673            code_block_width_exp: 0,
3674            code_block_height_exp: 0,
3675            ..EncodeOptions::default()
3676        };
3677        encode_htj2k(&pixels, 8, 8, 1, 8, false, &options).expect("encode multi-block HT gray8")
3678    }
3679
3680    fn fixture_ht_rgb_multi_block() -> Vec<u8> {
3681        let pixels = gradient_pixels(8, 8, 3);
3682        let options = EncodeOptions {
3683            reversible: true,
3684            num_decomposition_levels: 0,
3685            code_block_width_exp: 0,
3686            code_block_height_exp: 0,
3687            ..EncodeOptions::default()
3688        };
3689        encode_htj2k(&pixels, 8, 8, 3, 8, false, &options).expect("encode multi-block HT RGB8")
3690    }
3691
3692    fn direct_ht_job_count(plan: &J2kDirectGrayscalePlan) -> usize {
3693        plan.steps
3694            .iter()
3695            .map(|step| match step {
3696                J2kDirectGrayscaleStep::HtSubBand(sub_band) => sub_band.jobs.len(),
3697                _ => 0,
3698            })
3699            .sum()
3700    }
3701
3702    fn direct_color_ht_job_count(plan: &J2kDirectColorPlan) -> usize {
3703        plan.component_plans.iter().map(direct_ht_job_count).sum()
3704    }
3705
3706    fn fixture_openhtj2k_ht_refinement() -> &'static [u8] {
3707        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_12_b11.j2k")
3708    }
3709
3710    fn fixture_openhtj2k_ht_refinement_pixels() -> &'static [u8] {
3711        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_12_b11.gray")
3712    }
3713
3714    fn fixture_openhtj2k_ht_refinement_odd() -> &'static [u8] {
3715        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_09_b11.j2k")
3716    }
3717
3718    fn fixture_openhtj2k_ht_refinement_odd_pixels() -> &'static [u8] {
3719        include_bytes!("../fixtures/htj2k/openhtj2k_ds0_ht_09_b11.gray")
3720    }
3721
3722    fn gradient_pixels(width: u32, height: u32, components: u8) -> Vec<u8> {
3723        let mut pixels = Vec::with_capacity(width as usize * height as usize * components as usize);
3724        for y in 0..height {
3725            for x in 0..width {
3726                for component in 0..components {
3727                    pixels.push(((x * 3 + y * 5 + u32::from(component) * 41) & 0xff) as u8);
3728                }
3729            }
3730        }
3731        pixels
3732    }
3733
3734    fn roi_fixture(classic: bool, components: u8) -> Vec<u8> {
3735        let width = 64;
3736        let height = 64;
3737        let pixels = gradient_pixels(width, height, components);
3738        let options = EncodeOptions {
3739            reversible: true,
3740            num_decomposition_levels: 2,
3741            code_block_width_exp: 0,
3742            code_block_height_exp: 0,
3743            ..EncodeOptions::default()
3744        };
3745        if classic {
3746            encode(&pixels, width, height, components, 8, false, &options)
3747                .expect("encode ROI classic fixture")
3748        } else {
3749            encode_htj2k(&pixels, width, height, components, 8, false, &options)
3750                .expect("encode ROI HT fixture")
3751        }
3752    }
3753
3754    fn crop_interleaved(
3755        full: &[u8],
3756        full_width: u32,
3757        channels: usize,
3758        roi: (u32, u32, u32, u32),
3759    ) -> Vec<u8> {
3760        let (x, y, width, height) = roi;
3761        let mut out = Vec::with_capacity(width as usize * height as usize * channels);
3762        let row_bytes = full_width as usize * channels;
3763        let roi_row_bytes = width as usize * channels;
3764        for row in y as usize..(y + height) as usize {
3765            let start = row * row_bytes + x as usize * channels;
3766            out.extend_from_slice(&full[start..start + roi_row_bytes]);
3767        }
3768        out
3769    }
3770
3771    fn count_decode_work(bytes: &[u8], roi: Option<(u32, u32, u32, u32)>) -> DecodeWorkCounter {
3772        let image = Image::new(bytes, &DecodeSettings::default()).expect("image");
3773        let mut context = DecoderContext::default();
3774        match roi {
3775            Some(roi) => {
3776                image
3777                    .decode_region_with_context(roi, &mut context)
3778                    .expect("region decode with counter");
3779            }
3780            None => {
3781                image
3782                    .decode_with_context(&mut context)
3783                    .expect("full decode with counter");
3784            }
3785        }
3786        let counters = context.tile_decode_context.debug_counters;
3787        DecodeWorkCounter {
3788            classic_code_blocks: counters.decoded_code_blocks,
3789            ht_code_blocks: 0,
3790            idwt_output_samples: counters.idwt_output_samples,
3791        }
3792    }
3793
3794    #[test]
3795    fn roi_decode_matches_full_crop_for_classic_and_htj2k_gray_and_rgb() {
3796        let cases = [
3797            (true, 1_u8, true, false),
3798            (true, 3_u8, false, false),
3799            (false, 1_u8, true, false),
3800            (false, 3_u8, false, false),
3801        ];
3802        let rois = [
3803            (20, 18, 17, 19),
3804            (0, 0, 9, 11),
3805            (63, 63, 1, 1),
3806            (7, 5, 13, 9),
3807            (0, 0, 64, 64),
3808        ];
3809
3810        for (classic, components, expect_gray, has_alpha) in cases {
3811            let bytes = roi_fixture(classic, components);
3812            let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3813            let full = image.decode().expect("full decode");
3814            let channels = components as usize;
3815            for roi in rois {
3816                let region = image.decode_region(roi).expect("region decode");
3817                assert_eq!(matches!(region.color_space, ColorSpace::Gray), expect_gray);
3818                assert_eq!(region.has_alpha, has_alpha);
3819                assert_eq!(
3820                    region.data,
3821                    crop_interleaved(&full, 64, channels, roi),
3822                    "classic={classic} components={components} roi={roi:?}"
3823                );
3824            }
3825        }
3826    }
3827
3828    #[test]
3829    fn roi_decode_prunes_code_blocks_and_idwt_work_for_classic_and_htj2k() {
3830        let roi = (48, 48, 16, 16);
3831        for classic in [true, false] {
3832            let bytes = {
3833                let pixels = gradient_pixels(128, 128, 1);
3834                let options = EncodeOptions {
3835                    reversible: true,
3836                    num_decomposition_levels: 3,
3837                    code_block_width_exp: 0,
3838                    code_block_height_exp: 0,
3839                    ..EncodeOptions::default()
3840                };
3841                if classic {
3842                    encode(&pixels, 128, 128, 1, 8, false, &options)
3843                        .expect("encode classic work fixture")
3844                } else {
3845                    encode_htj2k(&pixels, 128, 128, 1, 8, false, &options)
3846                        .expect("encode ht work fixture")
3847                }
3848            };
3849            let full = count_decode_work(&bytes, None);
3850            let region = count_decode_work(&bytes, Some(roi));
3851
3852            assert!(
3853                region.code_blocks() > 0 && region.code_blocks() < full.code_blocks(),
3854                "ROI should decode fewer code-blocks for classic={classic}; full={}, region={}",
3855                full.code_blocks(),
3856                region.code_blocks()
3857            );
3858            assert!(
3859                region.idwt_output_samples > 0
3860                    && region.idwt_output_samples < full.idwt_output_samples,
3861                "ROI should produce fewer IDWT output samples for classic={classic}; full={}, region={}",
3862                full.idwt_output_samples,
3863                region.idwt_output_samples
3864            );
3865        }
3866    }
3867
3868    #[test]
3869    fn region_decode_reuses_region_sized_component_storage() {
3870        let bytes = fixture();
3871        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3872        let mut context = DecoderContext::default();
3873
3874        let bitmap = image
3875            .decode_region_with_context((1, 0, 1, 2), &mut context)
3876            .expect("region decode");
3877
3878        assert_eq!((bitmap.width, bitmap.height), (1, 2));
3879        assert!(context
3880            .tile_decode_context
3881            .channel_data
3882            .iter()
3883            .all(|component| component.container.truncated().len() == 2));
3884    }
3885
3886    #[test]
3887    fn native_region_decode_reuses_region_sized_component_storage() {
3888        let bytes = fixture();
3889        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3890        let mut context = DecoderContext::default();
3891
3892        let bitmap = image
3893            .decode_native_region_with_context((1, 0, 1, 2), &mut context)
3894            .expect("native region decode");
3895
3896        assert_eq!((bitmap.width, bitmap.height), (1, 2));
3897        assert!(context
3898            .tile_decode_context
3899            .channel_data
3900            .iter()
3901            .all(|component| component.container.truncated().len() == 2));
3902    }
3903
3904    #[test]
3905    fn decoder_context_defaults_to_auto_cpu_parallelism() {
3906        let context = DecoderContext::default();
3907
3908        assert_eq!(context.cpu_decode_parallelism(), CpuDecodeParallelism::Auto);
3909    }
3910
3911    #[test]
3912    fn classic_j2k_auto_and_serial_cpu_parallelism_match_pixels() {
3913        let bytes = fixture_multi_block();
3914        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3915        let mut auto_context = DecoderContext::default();
3916        let mut serial_context = DecoderContext::default();
3917        serial_context.set_cpu_decode_parallelism(CpuDecodeParallelism::Serial);
3918
3919        let auto = image
3920            .decode_with_context(&mut auto_context)
3921            .expect("auto decode");
3922        let serial = image
3923            .decode_with_context(&mut serial_context)
3924            .expect("serial decode");
3925
3926        assert_eq!(auto.data, serial.data);
3927    }
3928
3929    #[test]
3930    fn htj2k_97_auto_and_serial_cpu_parallelism_match_pixels() {
3931        let width = 128_u32;
3932        let height = 128_u32;
3933        let pixels = (0..width * height)
3934            .map(|idx| ((idx * 17 + idx / width * 31) & 0xff) as u8)
3935            .collect::<Vec<_>>();
3936        let bytes = encode_htj2k(
3937            &pixels,
3938            width,
3939            height,
3940            1,
3941            8,
3942            false,
3943            &EncodeOptions {
3944                reversible: false,
3945                guard_bits: 2,
3946                num_decomposition_levels: 5,
3947                ..EncodeOptions::default()
3948            },
3949        )
3950        .expect("encode HTJ2K 9/7");
3951        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3952        let mut auto_context = DecoderContext::default();
3953        let mut serial_context = DecoderContext::default();
3954        serial_context.set_cpu_decode_parallelism(CpuDecodeParallelism::Serial);
3955
3956        let auto = image
3957            .decode_with_context(&mut auto_context)
3958            .expect("auto decode");
3959        let serial = image
3960            .decode_with_context(&mut serial_context)
3961            .expect("serial decode");
3962
3963        assert_eq!(auto.data, serial.data);
3964    }
3965
3966    #[test]
3967    fn serial_cpu_parallelism_disables_classic_sub_band_parallel_branch() {
3968        assert!(!j2c::should_decode_classic_sub_band_in_parallel(
3969            CpuDecodeParallelism::Serial,
3970            16
3971        ));
3972    }
3973
3974    #[test]
3975    fn grayscale_direct_plan_is_built_without_materializing_channel_data() {
3976        let bytes = fixture_gray();
3977        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
3978        let mut context = DecoderContext::default();
3979
3980        let plan = image
3981            .build_direct_grayscale_plan_with_context(&mut context)
3982            .expect("build direct plan");
3983
3984        assert_eq!(plan.dimensions, (4, 4));
3985        assert_eq!(plan.bit_depth, 8);
3986        assert!(
3987            !plan.steps.is_empty(),
3988            "direct plan must contain executable steps"
3989        );
3990        assert!(
3991            plan.steps.iter().any(|step| matches!(
3992                step,
3993                J2kDirectGrayscaleStep::ClassicSubBand(plan) if !plan.jobs.is_empty()
3994            )),
3995            "classic J2K direct plan must contain at least one non-empty classic sub-band job"
3996        );
3997        assert!(
3998            context.tile_decode_context.channel_data.is_empty(),
3999            "building a direct plan must not materialize host component planes"
4000        );
4001    }
4002
4003    #[test]
4004    fn grayscale_direct_plan_honors_target_resolution() {
4005        let bytes = fixture_ht_gray();
4006        let image = Image::new(
4007            &bytes,
4008            &DecodeSettings {
4009                target_resolution: Some((2, 2)),
4010                ..DecodeSettings::default()
4011            },
4012        )
4013        .expect("scaled image");
4014        let mut context = DecoderContext::default();
4015
4016        let plan = image
4017            .build_direct_grayscale_plan_with_context(&mut context)
4018            .expect("build scaled direct plan");
4019
4020        assert_eq!(plan.dimensions, (2, 2));
4021        assert!(plan.steps.iter().any(|step| matches!(
4022            step,
4023            J2kDirectGrayscaleStep::HtSubBand(plan) if !plan.jobs.is_empty()
4024        )));
4025        assert!(plan.steps.iter().any(|step| matches!(
4026            step,
4027            J2kDirectGrayscaleStep::Store(store)
4028                if store.output_width == 2 && store.output_height == 2
4029        )));
4030        assert!(
4031            context.tile_decode_context.channel_data.is_empty(),
4032            "building a scaled direct plan must not materialize host component planes"
4033        );
4034    }
4035
4036    #[test]
4037    fn grayscale_direct_plan_region_prunes_unneeded_ht_code_blocks() {
4038        let bytes = fixture_ht_multi_block();
4039        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4040        let mut full_context = DecoderContext::default();
4041        let mut roi_context = DecoderContext::default();
4042
4043        let full = image
4044            .build_direct_grayscale_plan_with_context(&mut full_context)
4045            .expect("build full direct plan");
4046        let roi = image
4047            .build_direct_grayscale_plan_region_with_context(&mut roi_context, (0, 0, 2, 2))
4048            .expect("build ROI direct plan");
4049
4050        let full_jobs = direct_ht_job_count(&full);
4051        let roi_jobs = direct_ht_job_count(&roi);
4052        assert!(full_jobs > 1, "fixture must expose multiple HT jobs");
4053        assert!(
4054            roi_jobs < full_jobs,
4055            "ROI direct plan must prune HT jobs before device preparation"
4056        );
4057    }
4058
4059    #[test]
4060    fn color_direct_plan_region_prunes_unneeded_ht_code_blocks() {
4061        let bytes = fixture_ht_rgb_multi_block();
4062        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4063        let mut full_context = DecoderContext::default();
4064        let mut roi_context = DecoderContext::default();
4065
4066        let full = image
4067            .build_direct_color_plan_with_context(&mut full_context)
4068            .expect("build full RGB direct plan");
4069        let roi = image
4070            .build_direct_color_plan_region_with_context(&mut roi_context, (0, 0, 2, 2))
4071            .expect("build ROI RGB direct plan");
4072
4073        let full_jobs = direct_color_ht_job_count(&full);
4074        let roi_jobs = direct_color_ht_job_count(&roi);
4075        assert!(full_jobs > 3, "fixture must expose multiple RGB HT jobs");
4076        assert!(
4077            roi_jobs < full_jobs,
4078            "RGB ROI direct plan must prune HT jobs before device preparation"
4079        );
4080    }
4081
4082    #[test]
4083    fn color_direct_plan_honors_target_resolution() {
4084        for (name, bytes) in [
4085            ("classic", {
4086                let pixels = gradient_pixels(8, 8, 3);
4087                let options = EncodeOptions {
4088                    reversible: true,
4089                    num_decomposition_levels: 2,
4090                    ..EncodeOptions::default()
4091                };
4092                encode(&pixels, 8, 8, 3, 8, false, &options).expect("encode classic rgb8")
4093            }),
4094            ("htj2k", {
4095                let pixels = gradient_pixels(8, 8, 3);
4096                let options = EncodeOptions {
4097                    reversible: true,
4098                    num_decomposition_levels: 2,
4099                    ..EncodeOptions::default()
4100                };
4101                encode_htj2k(&pixels, 8, 8, 3, 8, false, &options).expect("encode ht rgb8")
4102            }),
4103        ] {
4104            let image = Image::new(
4105                &bytes,
4106                &DecodeSettings {
4107                    target_resolution: Some((4, 4)),
4108                    ..DecodeSettings::default()
4109                },
4110            )
4111            .expect("scaled RGB image");
4112            let mut context = DecoderContext::default();
4113
4114            let plan = image
4115                .build_direct_color_plan_with_context(&mut context)
4116                .expect("build scaled direct color plan");
4117
4118            assert_eq!(plan.dimensions, (4, 4), "{name}: output dimensions");
4119            assert_eq!(plan.component_plans.len(), 3, "{name}: component count");
4120            for component_plan in &plan.component_plans {
4121                assert_eq!(component_plan.dimensions, (4, 4), "{name}: component dims");
4122                assert!(component_plan.steps.iter().any(|step| matches!(
4123                    step,
4124                    J2kDirectGrayscaleStep::Store(store)
4125                        if store.output_width == 4 && store.output_height == 4
4126                )));
4127            }
4128            assert!(
4129                context.tile_decode_context.channel_data.is_empty(),
4130                "{name}: building a scaled color direct plan must not materialize host component planes"
4131            );
4132        }
4133    }
4134
4135    #[test]
4136    fn direct_color_cpu_rgb8_executor_matches_scaled_region_decode() {
4137        for (name, bytes) in [
4138            ("classic", {
4139                let pixels = gradient_pixels(16, 16, 3);
4140                let options = EncodeOptions {
4141                    reversible: true,
4142                    num_decomposition_levels: 2,
4143                    ..EncodeOptions::default()
4144                };
4145                encode(&pixels, 16, 16, 3, 8, false, &options).expect("encode classic rgb8")
4146            }),
4147            ("htj2k", {
4148                let pixels = gradient_pixels(16, 16, 3);
4149                let options = EncodeOptions {
4150                    reversible: true,
4151                    num_decomposition_levels: 2,
4152                    ..EncodeOptions::default()
4153                };
4154                encode_htj2k(&pixels, 16, 16, 3, 8, false, &options).expect("encode ht rgb8")
4155            }),
4156        ] {
4157            let image = Image::new(
4158                &bytes,
4159                &DecodeSettings {
4160                    target_resolution: Some((4, 4)),
4161                    ..DecodeSettings::default()
4162                },
4163            )
4164            .expect("scaled RGB image");
4165            let mut expected_context = DecoderContext::default();
4166            let expected_full = image
4167                .decode_with_context(&mut expected_context)
4168                .expect("decode scaled reference");
4169            let output_region = J2kRect {
4170                x0: 1,
4171                y0: 1,
4172                x1: 3,
4173                y1: 3,
4174            };
4175            let mut direct_context = DecoderContext::default();
4176            let plan = image
4177                .build_direct_color_plan_region_with_context(
4178                    &mut direct_context,
4179                    (
4180                        output_region.x0,
4181                        output_region.y0,
4182                        output_region.width(),
4183                        output_region.height(),
4184                    ),
4185                )
4186                .expect("build direct RGB region plan");
4187
4188            let stride = output_region.width() as usize * 3;
4189            let mut direct = vec![0_u8; stride * output_region.height() as usize];
4190            let mut scratch = J2kDirectCpuScratch::new();
4191            execute_direct_color_plan_rgb8_into(
4192                &plan,
4193                output_region,
4194                &mut scratch,
4195                &mut direct,
4196                stride,
4197            )
4198            .expect("execute direct RGB plan");
4199
4200            let mut expected = Vec::with_capacity(direct.len());
4201            let full_stride = image.width() as usize * 3;
4202            for y in output_region.y0..output_region.y1 {
4203                let start = y as usize * full_stride + output_region.x0 as usize * 3;
4204                expected.extend_from_slice(&expected_full.data[start..start + stride]);
4205            }
4206
4207            assert_eq!(direct, expected, "{name}: direct RGB output");
4208
4209            let rgba_stride = output_region.width() as usize * 4;
4210            let mut direct_rgba = vec![0_u8; rgba_stride * output_region.height() as usize];
4211            execute_direct_color_plan_rgba8_into(
4212                &plan,
4213                output_region,
4214                &mut scratch,
4215                &mut direct_rgba,
4216                rgba_stride,
4217            )
4218            .expect("execute direct RGBA plan");
4219
4220            let mut expected_rgba = Vec::with_capacity(direct_rgba.len());
4221            for rgb in expected.chunks_exact(3) {
4222                expected_rgba.extend_from_slice(rgb);
4223                expected_rgba.push(255);
4224            }
4225            assert_eq!(direct_rgba, expected_rgba, "{name}: direct RGBA output");
4226        }
4227    }
4228
4229    #[test]
4230    fn htj2k_grayscale_direct_plan_contains_ht_sub_band_steps() {
4231        let bytes = fixture_ht_gray();
4232        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4233        let mut context = DecoderContext::default();
4234
4235        let plan = image
4236            .build_direct_grayscale_plan_with_context(&mut context)
4237            .expect("build direct plan");
4238
4239        assert!(
4240            plan.steps.iter().any(|step| matches!(
4241                step,
4242                J2kDirectGrayscaleStep::HtSubBand(plan) if !plan.jobs.is_empty()
4243            )),
4244            "HTJ2K direct plan must contain at least one non-empty HT sub-band decode step"
4245        );
4246    }
4247
4248    #[test]
4249    fn ht_decoder_hook_is_used_for_htj2k_codeblocks() {
4250        let pixels: Vec<u8> = (0..16).collect();
4251        let options = EncodeOptions {
4252            reversible: true,
4253            num_decomposition_levels: 1,
4254            ..EncodeOptions::default()
4255        };
4256        let bytes = encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode ht");
4257        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4258        let mut hooked_context = DecoderContext::default();
4259        let mut hook = FailingHtDecoder { called: false };
4260        let error = match image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) {
4261            Ok(_) => panic!("hooked decode must use external HT decoder"),
4262            Err(error) => error,
4263        };
4264
4265        assert!(hook.called, "HT decoder hook must be invoked");
4266        assert_eq!(
4267            error,
4268            DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
4269        );
4270    }
4271
4272    #[test]
4273    fn openhtj2k_conformance_fixture_exercises_refinement_passes() {
4274        for fixture in [
4275            (
4276                "ds0_ht_12_b11",
4277                fixture_openhtj2k_ht_refinement(),
4278                fixture_openhtj2k_ht_refinement_pixels(),
4279                (3, 5),
4280                8,
4281                2,
4282                4,
4283            ),
4284            (
4285                "ds0_ht_09_b11",
4286                fixture_openhtj2k_ht_refinement_odd(),
4287                fixture_openhtj2k_ht_refinement_odd_pixels(),
4288                (17, 37),
4289                14,
4290                14,
4291                629,
4292            ),
4293        ] {
4294            let (
4295                name,
4296                codestream,
4297                expected_pixels,
4298                dimensions,
4299                blocks,
4300                refinement_jobs,
4301                zero_diffs,
4302            ) = fixture;
4303            let image = Image::new(codestream, &DecodeSettings::default()).expect("image");
4304            let mut context = DecoderContext::default();
4305            let mut hook = CapturingHtDecoder::default();
4306
4307            let components = image
4308                .decode_components_with_ht_decoder(&mut context, &mut hook)
4309                .expect("decode OpenHTJ2K HTJ2K fixture");
4310
4311            assert!(
4312                hook.called,
4313                "{name}: HTJ2K fixture must use HT code-block decode"
4314            );
4315            assert!(
4316                hook.refinement_jobs > 0,
4317                "{name}: OpenHTJ2K fixture must contain non-empty refinement segments"
4318            );
4319            assert!(
4320                hook.max_coding_passes > 1,
4321                "{name}: OpenHTJ2K fixture must exercise more than the cleanup pass"
4322            );
4323            assert_eq!(hook.blocks, blocks, "{name}: HT code-block count");
4324            assert_eq!(
4325                hook.refinement_jobs, refinement_jobs,
4326                "{name}: refinement job count"
4327            );
4328            assert_eq!(hook.max_coding_passes, 3, "{name}: max HT coding passes");
4329            assert_eq!(components.dimensions(), dimensions, "{name}: dimensions");
4330            assert_eq!(components.planes().len(), 1, "{name}: component planes");
4331
4332            let decoded: Vec<u8> = components.planes()[0]
4333                .samples()
4334                .iter()
4335                .map(|sample| sample.round().clamp(0.0, 255.0) as u8)
4336                .collect();
4337            assert_eq!(decoded, expected_pixels, "{name}: decoded pixels");
4338
4339            let mut zero_context = DecoderContext::default();
4340            let mut zero_hook = ZeroRefinementHtDecoder;
4341            let zeroed_components = image
4342                .decode_components_with_ht_decoder(&mut zero_context, &mut zero_hook)
4343                .expect("decode OpenHTJ2K fixture with zeroed refinement bytes");
4344            let actual_zero_diffs = components.planes()[0]
4345                .samples()
4346                .iter()
4347                .zip(zeroed_components.planes()[0].samples())
4348                .filter(|(actual, zeroed)| (*actual - *zeroed).abs() > f32::EPSILON)
4349                .count();
4350            assert_eq!(
4351                actual_zero_diffs, zero_diffs,
4352                "{name}: zeroing refinement bytes must change decoded samples"
4353            );
4354        }
4355    }
4356
4357    #[test]
4358    fn openhtj2k_refinement_phase_limited_decode_differs_and_records_ht_stats() {
4359        let image = Image::new(
4360            fixture_openhtj2k_ht_refinement_odd(),
4361            &DecodeSettings::default(),
4362        )
4363        .expect("image");
4364        let mut full_context = DecoderContext::default();
4365
4366        let (full_samples, full_decoded) = {
4367            let full_components = image
4368                .decode_components_with_context(&mut full_context)
4369                .expect("full native decode of OpenHTJ2K refinement fixture");
4370            let full_samples = full_components.planes()[0].samples().to_vec();
4371            let full_decoded: Vec<u8> = full_samples
4372                .iter()
4373                .map(|sample| sample.round().clamp(0.0, 255.0) as u8)
4374                .collect();
4375            (full_samples, full_decoded)
4376        };
4377        assert_eq!(
4378            full_decoded,
4379            fixture_openhtj2k_ht_refinement_odd_pixels(),
4380            "full decode must match the checked-in OpenHTJ2K oracle"
4381        );
4382
4383        let stats = full_context
4384            .tile_decode_context
4385            .debug_counters
4386            .ht_phase_stats;
4387        assert_eq!(stats.blocks, 14, "HT block count");
4388        assert_eq!(stats.refinement_blocks, 14, "HT refinement block count");
4389        assert!(stats.cleanup_bytes > 0, "cleanup byte total");
4390        assert!(stats.refinement_bytes > 0, "refinement byte total");
4391
4392        let mut cleanup_context = DecoderContext::default();
4393        let mut cleanup_hook = CleanupLimitedHtDecoder::default();
4394        let cleanup_components = image
4395            .decode_components_with_ht_decoder(&mut cleanup_context, &mut cleanup_hook)
4396            .expect("cleanup-limited decode of OpenHTJ2K refinement fixture");
4397        let cleanup_decoded: Vec<u8> = cleanup_components.planes()[0]
4398            .samples()
4399            .iter()
4400            .map(|sample| sample.round().clamp(0.0, 255.0) as u8)
4401            .collect();
4402        let cleanup_sample_diffs = full_samples
4403            .iter()
4404            .zip(cleanup_components.planes()[0].samples())
4405            .filter(|(full, cleanup)| (*full - *cleanup).abs() > f32::EPSILON)
4406            .count();
4407
4408        assert!(
4409            cleanup_sample_diffs > 0,
4410            "cleanup-limited decode must omit refinement effects"
4411        );
4412        assert_eq!(
4413            cleanup_decoded, full_decoded,
4414            "fixture refinement differences are below final u8 clamping"
4415        );
4416        assert_eq!(cleanup_hook.blocks, 14, "hook HT block count");
4417        assert_eq!(
4418            cleanup_hook.refinement_blocks, 14,
4419            "hook HT refinement block count"
4420        );
4421        assert!(cleanup_hook.cleanup_bytes > 0, "hook cleanup byte total");
4422        assert!(
4423            cleanup_hook.refinement_bytes > 0,
4424            "hook refinement byte total"
4425        );
4426    }
4427
4428    #[test]
4429    fn scalar_htj2k_encoder_contract_is_cleanup_only() {
4430        let coefficients = (0..64)
4431            .map(|index| {
4432                let magnitude = (index % 7) + 1;
4433                if index % 2 == 0 {
4434                    magnitude
4435                } else {
4436                    -magnitude
4437                }
4438            })
4439            .collect::<Vec<_>>();
4440
4441        let encoded =
4442            encode_ht_code_block_scalar(&coefficients, 8, 8, 8).expect("encode HT code block");
4443
4444        assert_eq!(
4445            encoded.num_coding_passes, 1,
4446            "current scalar HTJ2K encoder emits only the cleanup pass"
4447        );
4448        assert_eq!(
4449            encoded.num_zero_bitplanes, 7,
4450            "current cleanup-only HTJ2K encoder includes one bitplane"
4451        );
4452        assert!(
4453            !encoded.data.is_empty(),
4454            "non-zero cleanup-only block must still produce payload bytes"
4455        );
4456    }
4457
4458    #[test]
4459    fn scalar_htj2k_decode_workspace_matches_fresh_decode_and_reuses_capacity() {
4460        let image = Image::new(
4461            fixture_openhtj2k_ht_refinement_odd(),
4462            &DecodeSettings::default(),
4463        )
4464        .expect("image");
4465        let mut context = DecoderContext::default();
4466        let mut hook = FirstHtJobDecoder::default();
4467        image
4468            .decode_components_with_ht_decoder(&mut context, &mut hook)
4469            .expect("decode fixture while collecting HT jobs");
4470        let job = hook
4471            .job
4472            .as_ref()
4473            .expect("fixture must expose an HT decode job")
4474            .borrowed();
4475        let mut fresh = vec![0.0_f32; job.width as usize * job.height as usize];
4476        let mut reused = vec![0.0_f32; fresh.len()];
4477        let mut profiled = vec![0.0_f32; fresh.len()];
4478        let mut workspace = HtCodeBlockDecodeWorkspace::default();
4479        let mut profile = HtCodeBlockDecodeProfile::default();
4480
4481        decode_ht_code_block_scalar(job, &mut fresh).expect("fresh HT decode");
4482        decode_ht_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
4483            .expect("workspace HT decode");
4484        let first_capacity = workspace.coefficient_capacity();
4485        decode_ht_code_block_scalar_with_workspace(job, &mut reused, &mut workspace)
4486            .expect("second workspace HT decode");
4487        decode_ht_code_block_scalar_with_workspace_profiled(
4488            job,
4489            &mut profiled,
4490            &mut workspace,
4491            &mut profile,
4492        )
4493        .expect("profiled workspace HT decode");
4494
4495        assert_eq!(reused, fresh);
4496        assert_eq!(profiled, fresh);
4497        assert!(first_capacity >= fresh.len());
4498        assert_eq!(workspace.coefficient_capacity(), first_capacity);
4499        assert_eq!(profile.blocks, 1);
4500        assert!(profile.cleanup_bytes > 0);
4501    }
4502
4503    #[test]
4504    fn classic_decoder_hook_is_used_for_j2k_codeblocks() {
4505        let bytes = fixture();
4506        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4507        let mut hooked_context = DecoderContext::default();
4508        let mut hook = FailingClassicDecoder { called: false };
4509        let error = match image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) {
4510            Ok(_) => panic!("hooked decode must use external classic decoder"),
4511            Err(error) => error,
4512        };
4513
4514        assert!(hook.called, "classic decoder hook must be invoked");
4515        assert_eq!(
4516            error,
4517            DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
4518        );
4519    }
4520
4521    #[test]
4522    fn classic_sub_band_decoder_hook_is_used_for_j2k_codeblocks() {
4523        let bytes = fixture_multi_block();
4524        let image = Image::new(&bytes, &DecodeSettings::default()).expect("image");
4525        let mut hooked_context = DecoderContext::default();
4526        let mut hook = FailingClassicBatchDecoder { called: false };
4527        let error = match image.decode_components_with_ht_decoder(&mut hooked_context, &mut hook) {
4528            Ok(_) => panic!("hooked decode must use external classic batch decoder"),
4529            Err(error) => error,
4530        };
4531
4532        assert!(hook.called, "classic sub-band decoder hook must be invoked");
4533        assert_eq!(
4534            error,
4535            DecodeError::Decoding(DecodingError::CodeBlockDecodeFailure)
4536        );
4537    }
4538
4539    // -----------------------------------------------------------------------
4540    // Sanity tests for the four scalar-reference exports
4541    // -----------------------------------------------------------------------
4542
4543    #[test]
4544    fn forward_dwt53_reference_matches_internal_path() {
4545        // 4×4 constant-ramp input; 1 decomposition level.
4546        let samples: Vec<f32> = (0..16).map(|i| i as f32).collect();
4547        let out = forward_dwt53_reference(&samples, 4, 4, 1);
4548
4549        // Internal path
4550        let internal = j2c::fdwt::forward_dwt(&samples, 4, 4, 1, true);
4551
4552        assert_eq!(out.ll, internal.ll, "LL subband mismatch");
4553        assert_eq!(out.ll_width, internal.ll_width, "LL width mismatch");
4554        assert_eq!(out.ll_height, internal.ll_height, "LL height mismatch");
4555        assert_eq!(out.levels.len(), internal.levels.len(), "level count");
4556        for (pub_lvl, int_lvl) in out.levels.iter().zip(internal.levels.iter()) {
4557            assert_eq!(pub_lvl.hl, int_lvl.hl, "HL mismatch");
4558            assert_eq!(pub_lvl.lh, int_lvl.lh, "LH mismatch");
4559            assert_eq!(pub_lvl.hh, int_lvl.hh, "HH mismatch");
4560        }
4561    }
4562
4563    #[test]
4564    fn forward_rct_reference_matches_internal_path() {
4565        // Single pixel: R=100, G=150, B=200
4566        let planes = vec![vec![100.0f32], vec![150.0f32], vec![200.0f32]];
4567        let result = forward_rct_reference(planes.clone());
4568
4569        // Internal path
4570        let mut internal = planes;
4571        j2c::forward_mct::forward_rct(&mut internal);
4572
4573        assert_eq!(result, internal, "RCT output mismatch");
4574        // Y = floor((100 + 300 + 200) / 4) = 150
4575        assert_eq!(result[0][0], 150.0, "Y component");
4576        assert_eq!(result[1][0], 50.0, "Cb component");
4577        assert_eq!(result[2][0], -50.0, "Cr component");
4578    }
4579
4580    #[test]
4581    fn quantize_reversible_reference_matches_internal_path() {
4582        let coefficients = vec![3.7f32, -8.2, 0.5, -0.5, 10.0];
4583        let exponent = 8u16;
4584        let mantissa = 0u16;
4585        let range_bits = 8u8;
4586
4587        let result =
4588            quantize_reversible_reference(&coefficients, exponent, mantissa, range_bits, true);
4589
4590        // Internal path
4591        let step = j2c::quantize::QuantStepSize { exponent, mantissa };
4592        let internal = j2c::quantize::quantize_subband(&coefficients, &step, range_bits, true);
4593
4594        assert_eq!(result, internal, "quantize output mismatch");
4595        // reversible: round to nearest
4596        assert_eq!(result[0], 4, "3.7 rounds to 4");
4597        assert_eq!(result[1], -8, "-8.2 rounds to -8");
4598    }
4599
4600    #[test]
4601    fn deinterleave_reference_matches_internal_path() {
4602        // 2-pixel RGB8 unsigned: [R0,G0,B0, R1,G1,B1]
4603        let pixels: Vec<u8> = vec![128, 64, 200, 10, 20, 30];
4604        let result = deinterleave_reference(&pixels, 2, 3, 8, false);
4605
4606        let internal = j2c::encode::deinterleave_to_f32(&pixels, 2, 3, 8, false);
4607
4608        assert_eq!(result, internal, "deinterleave output mismatch");
4609        assert_eq!(result.len(), 3, "three component planes");
4610        assert_eq!(result[0].len(), 2, "two pixels per plane");
4611        // unsigned 8-bit with level shift: val - 128
4612        assert!((result[0][0] - 0.0f32).abs() < 1e-6, "R0 level-shifted");
4613        assert!((result[1][0] - (-64.0f32)).abs() < 1e-6, "G0 level-shifted");
4614        assert!((result[2][0] - 72.0f32).abs() < 1e-6, "B0 level-shifted");
4615    }
4616}