Skip to main content

dicom_toolkit_jpeg2000/j2c/
encode.rs

1//! Top-level JPEG 2000 encode orchestration.
2//!
3//! Coordinates the full encoding pipeline:
4//!   pixels → MCT → DWT → quantize → EBCOT T1 → T2 → codestream
5//!
6//! Supports both lossless (5-3 reversible) and lossy (9-7 irreversible) encoding.
7
8use alloc::vec;
9use alloc::vec::Vec;
10
11use super::bitplane_encode;
12use super::build::SubBandType;
13use super::codestream_write::{self, BlockCodingMode, EncodeParams};
14use super::fdwt::{self, DwtDecomposition};
15use super::forward_mct;
16use super::ht_block_encode;
17use super::packet_encode::{self, CodeBlockPacketData, ResolutionPacket, SubbandPrecinct};
18use super::quantize::{self, QuantStepSize};
19use crate::{DecodeSettings, Image};
20
21/// Encoding options for JPEG 2000.
22#[derive(Debug, Clone)]
23pub struct EncodeOptions {
24    /// Number of decomposition levels (default: 5).
25    pub num_decomposition_levels: u8,
26    /// Use reversible (lossless) transform (default: true).
27    pub reversible: bool,
28    /// Code-block width exponent minus 2 (default: 4, meaning 2^6=64).
29    pub code_block_width_exp: u8,
30    /// Code-block height exponent minus 2 (default: 4, meaning 2^6=64).
31    pub code_block_height_exp: u8,
32    /// Number of guard bits (default: 1 for reversible, 2 for irreversible).
33    pub guard_bits: u8,
34    /// Encode using HT block coding (HTJ2K / Part 15) instead of classic EBCOT.
35    pub use_ht_block_coding: bool,
36}
37
38impl Default for EncodeOptions {
39    fn default() -> Self {
40        Self {
41            num_decomposition_levels: 5,
42            reversible: true,
43            code_block_width_exp: 4,
44            code_block_height_exp: 4,
45            guard_bits: 1,
46            use_ht_block_coding: false,
47        }
48    }
49}
50
51/// Encode pixel data into a JPEG 2000 codestream.
52///
53/// # Arguments
54/// * `pixels` — Raw pixel data. For 8-bit: one byte per sample. For >8-bit: two bytes per sample (little-endian u16).
55/// * `width` — Image width in pixels.
56/// * `height` — Image height in pixels.
57/// * `num_components` — Number of components (1 for grayscale, 3 for RGB).
58/// * `bit_depth` — Bits per sample (e.g., 8, 12, 16).
59/// * `signed` — Whether samples are signed.
60/// * `options` — Encoding parameters.
61///
62/// # Returns
63/// The encoded JPEG 2000 codestream bytes (`.j2c` format).
64pub fn encode(
65    pixels: &[u8],
66    width: u32,
67    height: u32,
68    num_components: u8,
69    bit_depth: u8,
70    signed: bool,
71    options: &EncodeOptions,
72) -> Result<Vec<u8>, &'static str> {
73    let block_coding_mode = block_coding_mode(options);
74    let codestream = encode_impl(
75        pixels,
76        width,
77        height,
78        num_components,
79        bit_depth,
80        signed,
81        options,
82        block_coding_mode,
83    )?;
84
85    if block_coding_mode == BlockCodingMode::HighThroughput {
86        validate_htj2k_codestream(
87            &codestream,
88            pixels,
89            width,
90            height,
91            num_components,
92            bit_depth,
93            signed,
94            options.reversible,
95        )?;
96    }
97
98    Ok(codestream)
99}
100
101/// Encode pixel data into an HTJ2K codestream.
102///
103/// Lossless HTJ2K output is self-validated before it is returned.
104pub fn encode_htj2k(
105    pixels: &[u8],
106    width: u32,
107    height: u32,
108    num_components: u8,
109    bit_depth: u8,
110    signed: bool,
111    options: &EncodeOptions,
112) -> Result<Vec<u8>, &'static str> {
113    let mut options = options.clone();
114    options.use_ht_block_coding = true;
115    encode(
116        pixels,
117        width,
118        height,
119        num_components,
120        bit_depth,
121        signed,
122        &options,
123    )
124}
125
126fn block_coding_mode(options: &EncodeOptions) -> BlockCodingMode {
127    if options.use_ht_block_coding {
128        BlockCodingMode::HighThroughput
129    } else {
130        BlockCodingMode::Classic
131    }
132}
133
134fn validate_htj2k_codestream(
135    codestream: &[u8],
136    pixels: &[u8],
137    width: u32,
138    height: u32,
139    num_components: u8,
140    bit_depth: u8,
141    signed: bool,
142    reversible: bool,
143) -> Result<(), &'static str> {
144    let image = Image::new(codestream, &DecodeSettings::default())
145        .map_err(|_| "generated HTJ2K codestream failed self-validation")?;
146    let decoded = image
147        .decode_native()
148        .map_err(|_| "generated HTJ2K codestream failed self-validation")?;
149
150    if decoded.width != width
151        || decoded.height != height
152        || decoded.bit_depth != bit_depth
153        || decoded.num_components != num_components
154    {
155        return Err("generated HTJ2K codestream failed self-validation");
156    }
157
158    if reversible && !native_samples_equal(pixels, &decoded.data, bit_depth, signed) {
159        return Err("generated HTJ2K codestream did not roundtrip");
160    }
161
162    Ok(())
163}
164
165fn native_samples_equal(expected: &[u8], actual: &[u8], bit_depth: u8, signed: bool) -> bool {
166    if expected.len() != actual.len() {
167        return false;
168    }
169
170    let bytes_per_sample = if bit_depth <= 8 { 1 } else { 2 };
171    let sample_count = expected.len() / bytes_per_sample;
172    (0..sample_count).all(|sample_index| {
173        decode_native_sample(expected, sample_index, bit_depth, signed)
174            == decode_native_sample(actual, sample_index, bit_depth, signed)
175    })
176}
177
178fn decode_native_sample(bytes: &[u8], sample_index: usize, bit_depth: u8, signed: bool) -> i32 {
179    let byte_offset = sample_index * if bit_depth <= 8 { 1 } else { 2 };
180    let mask = (1u32 << u32::from(bit_depth)) - 1;
181    let raw = if bit_depth <= 8 {
182        u32::from(bytes[byte_offset])
183    } else {
184        u32::from(u16::from_le_bytes([
185            bytes[byte_offset],
186            bytes[byte_offset + 1],
187        ]))
188    } & mask;
189
190    if signed {
191        let shift = 32 - u32::from(bit_depth);
192        ((raw << shift) as i32) >> shift
193    } else {
194        raw as i32
195    }
196}
197
198fn encode_impl(
199    pixels: &[u8],
200    width: u32,
201    height: u32,
202    num_components: u8,
203    bit_depth: u8,
204    signed: bool,
205    options: &EncodeOptions,
206    block_coding_mode: BlockCodingMode,
207) -> Result<Vec<u8>, &'static str> {
208    if width == 0 || height == 0 {
209        return Err("invalid dimensions");
210    }
211    if num_components == 0 || num_components > 4 {
212        return Err("unsupported component count");
213    }
214    if bit_depth == 0 || bit_depth > 16 {
215        return Err("unsupported bit depth");
216    }
217
218    let num_pixels = (width * height) as usize;
219    let bytes_per_sample = if bit_depth <= 8 { 1 } else { 2 };
220    let expected_len = num_pixels * num_components as usize * bytes_per_sample;
221    if pixels.len() < expected_len {
222        return Err("pixel data too short");
223    }
224
225    #[cfg(feature = "openjph-htj2k")]
226    if block_coding_mode == BlockCodingMode::HighThroughput {
227        let mut openjph_options = options.clone();
228        openjph_options.num_decomposition_levels = options
229            .num_decomposition_levels
230            .min(max_decomposition_levels(width, height));
231        return crate::openjph_htj2k::encode(
232            pixels,
233            width,
234            height,
235            num_components,
236            bit_depth,
237            signed,
238            &openjph_options,
239        );
240    }
241
242    // Step 1: Convert pixel bytes to f32 component arrays
243    let mut components = deinterleave_to_f32(pixels, num_pixels, num_components, bit_depth, signed);
244
245    // Step 2: Apply forward MCT if RGB with 3+ components
246    let use_mct = num_components >= 3;
247    if use_mct {
248        if options.reversible {
249            forward_mct::forward_rct(&mut components);
250        } else {
251            forward_mct::forward_ict(&mut components);
252        }
253    }
254
255    // Step 3: Apply forward DWT to each component
256    let num_levels = options.num_decomposition_levels.min(
257        // Don't decompose more than the image supports
258        max_decomposition_levels(width, height),
259    );
260
261    let decompositions: Vec<DwtDecomposition> = components
262        .iter()
263        .map(|comp| fdwt::forward_dwt(comp, width, height, num_levels, options.reversible))
264        .collect();
265
266    // Step 4: Compute quantization step sizes
267    let guard_bits = if options.reversible {
268        options.guard_bits
269    } else {
270        options.guard_bits.max(2)
271    };
272
273    let step_sizes =
274        quantize::compute_step_sizes(bit_depth, num_levels, options.reversible, guard_bits);
275
276    // Step 5: Quantize and encode code-blocks for each component
277    let cb_width = 1u32 << (options.code_block_width_exp + 2);
278    let cb_height = 1u32 << (options.code_block_height_exp + 2);
279
280    let mut resolution_packets: Vec<ResolutionPacket> = Vec::new();
281
282    for decomp in decompositions.iter().take(num_components as usize) {
283        // LL subband (resolution 0)
284        let ll_subband = encode_subband(
285            &decomp.ll,
286            decomp.ll_width,
287            decomp.ll_height,
288            &step_sizes[0],
289            guard_bits,
290            options.reversible,
291            block_coding_mode,
292            cb_width,
293            cb_height,
294            SubBandType::LowLow,
295        )?;
296        resolution_packets.push(ResolutionPacket {
297            subbands: vec![ll_subband],
298        });
299
300        // Higher resolution levels
301        for (level_idx, level) in decomp.levels.iter().enumerate() {
302            let step_base = 1 + level_idx * 3;
303
304            // HL subband
305            let hl_subband = encode_subband(
306                &level.hl,
307                level.high_width,
308                level.low_height,
309                &step_sizes[step_base],
310                guard_bits,
311                options.reversible,
312                block_coding_mode,
313                cb_width,
314                cb_height,
315                SubBandType::HighLow,
316            )?;
317
318            // LH subband
319            let lh_subband = encode_subband(
320                &level.lh,
321                level.low_width,
322                level.high_height,
323                &step_sizes[step_base + 1],
324                guard_bits,
325                options.reversible,
326                block_coding_mode,
327                cb_width,
328                cb_height,
329                SubBandType::LowHigh,
330            )?;
331
332            // HH subband
333            let hh_subband = encode_subband(
334                &level.hh,
335                level.high_width,
336                level.high_height,
337                &step_sizes[step_base + 2],
338                guard_bits,
339                options.reversible,
340                block_coding_mode,
341                cb_width,
342                cb_height,
343                SubBandType::HighHigh,
344            )?;
345
346            resolution_packets.push(ResolutionPacket {
347                subbands: vec![hl_subband, lh_subband, hh_subband],
348            });
349        }
350    }
351
352    // Step 6: Form tile bitstream (T2)
353    let tile_data = packet_encode::form_tile_bitstream(&mut resolution_packets, 1, num_components);
354
355    // Step 7: Write codestream
356    let quant_params: Vec<(u16, u16)> = step_sizes
357        .iter()
358        .map(|s| (s.exponent, s.mantissa))
359        .collect();
360
361    let params = EncodeParams {
362        width,
363        height,
364        num_components,
365        bit_depth,
366        signed,
367        num_decomposition_levels: num_levels,
368        reversible: options.reversible,
369        code_block_width_exp: options.code_block_width_exp,
370        code_block_height_exp: options.code_block_height_exp,
371        num_layers: 1,
372        use_mct,
373        guard_bits,
374        block_coding_mode,
375    };
376
377    Ok(codestream_write::write_codestream(
378        &params,
379        &tile_data,
380        &quant_params,
381    ))
382}
383
384/// Encode a single subband into a SubbandPrecinct.
385fn encode_subband(
386    coefficients: &[f32],
387    width: u32,
388    height: u32,
389    step_size: &QuantStepSize,
390    guard_bits: u8,
391    reversible: bool,
392    block_coding_mode: BlockCodingMode,
393    cb_width: u32,
394    cb_height: u32,
395    sub_band_type: SubBandType,
396) -> Result<SubbandPrecinct, &'static str> {
397    if width == 0 || height == 0 {
398        return Ok(SubbandPrecinct {
399            code_blocks: Vec::new(),
400            num_cbs_x: 0,
401            num_cbs_y: 0,
402        });
403    }
404
405    // Quantize
406    let quantized = quantize::quantize_subband(coefficients, step_size, guard_bits, reversible);
407    debug_assert!(step_size.exponent <= u16::from(u8::MAX));
408    let total_bitplanes = guard_bits
409        .saturating_add(step_size.exponent as u8)
410        .saturating_sub(1);
411
412    // Split into code-blocks
413    let num_cbs_x = width.div_ceil(cb_width);
414    let num_cbs_y = height.div_ceil(cb_height);
415    let mut code_blocks = Vec::with_capacity((num_cbs_x * num_cbs_y) as usize);
416
417    for cby in 0..num_cbs_y {
418        for cbx in 0..num_cbs_x {
419            let x0 = cbx * cb_width;
420            let y0 = cby * cb_height;
421            let x1 = (x0 + cb_width).min(width);
422            let y1 = (y0 + cb_height).min(height);
423            let cbw = x1 - x0;
424            let cbh = y1 - y0;
425
426            // Extract code-block coefficients
427            let mut cb_coeffs = vec![0i32; (cbw * cbh) as usize];
428            for y in 0..cbh {
429                for x in 0..cbw {
430                    cb_coeffs[(y * cbw + x) as usize] =
431                        quantized[((y0 + y) * width + (x0 + x)) as usize];
432                }
433            }
434
435            let encoded = if block_coding_mode == BlockCodingMode::HighThroughput {
436                ht_block_encode::encode_code_block(&cb_coeffs, cbw, cbh, total_bitplanes)?
437            } else {
438                bitplane_encode::encode_code_block(
439                    &cb_coeffs,
440                    cbw,
441                    cbh,
442                    sub_band_type,
443                    total_bitplanes,
444                )
445            };
446
447            code_blocks.push(CodeBlockPacketData {
448                data: encoded.data,
449                num_coding_passes: encoded.num_coding_passes,
450                num_zero_bitplanes: encoded.num_zero_bitplanes,
451                previously_included: false,
452                l_block: 3,
453                block_coding_mode,
454            });
455        }
456    }
457
458    Ok(SubbandPrecinct {
459        code_blocks,
460        num_cbs_x,
461        num_cbs_y,
462    })
463}
464
465/// Convert interleaved pixel bytes to per-component f32 arrays.
466fn deinterleave_to_f32(
467    pixels: &[u8],
468    num_pixels: usize,
469    num_components: u8,
470    bit_depth: u8,
471    signed: bool,
472) -> Vec<Vec<f32>> {
473    let nc = num_components as usize;
474    let mut components = vec![vec![0.0f32; num_pixels]; nc];
475    let unsigned_offset = if signed {
476        0.0
477    } else {
478        (1u32 << (bit_depth as u32 - 1)) as f32
479    };
480
481    if bit_depth <= 8 {
482        for (i, pixel) in pixels.chunks_exact(nc).take(num_pixels).enumerate() {
483            for (c, component) in components.iter_mut().enumerate().take(nc) {
484                let val = pixel[c];
485                component[i] = if signed {
486                    (val as i8) as f32
487                } else {
488                    val as f32 - unsigned_offset
489                };
490            }
491        }
492    } else {
493        // 16-bit samples (little-endian)
494        for (i, pixel) in pixels.chunks_exact(nc * 2).take(num_pixels).enumerate() {
495            for (c, component) in components.iter_mut().enumerate().take(nc) {
496                let offset = c * 2;
497                let val = u16::from_le_bytes([pixel[offset], pixel[offset + 1]]);
498                component[i] = if signed {
499                    (val as i16) as f32
500                } else {
501                    val as f32 - unsigned_offset
502                };
503            }
504        }
505    }
506
507    components
508}
509
510/// Calculate the maximum number of decomposition levels for given dimensions.
511fn max_decomposition_levels(width: u32, height: u32) -> u8 {
512    let min_dim = width.min(height);
513    if min_dim <= 1 {
514        return 0;
515    }
516    (min_dim as f32).log2().floor() as u8
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    #[test]
524    fn test_encode_8bit_gray() {
525        let width = 8u32;
526        let height = 8u32;
527        let pixels: Vec<u8> = (0..64).collect();
528
529        let result = encode(
530            &pixels,
531            width,
532            height,
533            1,
534            8,
535            false,
536            &EncodeOptions {
537                num_decomposition_levels: 2,
538                ..Default::default()
539            },
540        );
541
542        assert!(result.is_ok());
543        let codestream = result.unwrap();
544        // Verify SOC marker
545        assert_eq!(codestream[0], 0xFF);
546        assert_eq!(codestream[1], 0x4F);
547        // Verify EOC marker
548        let len = codestream.len();
549        assert_eq!(codestream[len - 2], 0xFF);
550        assert_eq!(codestream[len - 1], 0xD9);
551    }
552
553    #[test]
554    fn test_encode_16bit_gray() {
555        let width = 8u32;
556        let height = 8u32;
557        let mut pixels = Vec::with_capacity(128);
558        for i in 0..64u16 {
559            let val = i * 100;
560            pixels.extend_from_slice(&val.to_le_bytes());
561        }
562
563        let result = encode(
564            &pixels,
565            width,
566            height,
567            1,
568            16,
569            false,
570            &EncodeOptions {
571                num_decomposition_levels: 2,
572                ..Default::default()
573            },
574        );
575
576        assert!(result.is_ok());
577    }
578
579    #[test]
580    fn test_encode_rgb() {
581        let width = 16u32;
582        let height = 16u32;
583        let pixels: Vec<u8> = (0..width * height * 3).map(|i| (i & 0xFF) as u8).collect();
584
585        let result = encode(
586            &pixels,
587            width,
588            height,
589            3,
590            8,
591            false,
592            &EncodeOptions {
593                num_decomposition_levels: 3,
594                ..Default::default()
595            },
596        );
597
598        assert!(result.is_ok(), "RGB encode failed: {:?}", result.err());
599    }
600
601    #[test]
602    fn test_encode_lossy() {
603        let pixels: Vec<u8> = (0..64).collect();
604
605        let result = encode(
606            &pixels,
607            8,
608            8,
609            1,
610            8,
611            false,
612            &EncodeOptions {
613                num_decomposition_levels: 2,
614                reversible: false,
615                guard_bits: 2,
616                ..Default::default()
617            },
618        );
619
620        assert!(result.is_ok());
621    }
622
623    fn assert_htj2k_lossless_roundtrip(
624        pixels: &[u8],
625        width: u32,
626        height: u32,
627        bit_depth: u8,
628        num_decomposition_levels: u8,
629    ) {
630        let codestream = encode_htj2k(
631            pixels,
632            width,
633            height,
634            1,
635            bit_depth,
636            false,
637            &EncodeOptions {
638                num_decomposition_levels,
639                ..Default::default()
640            },
641        )
642        .expect("HTJ2K encode");
643
644        assert!(codestream.windows(2).any(|window| window == [0xFF, 0x50]));
645        let cod_offset = codestream
646            .windows(2)
647            .position(|window| window == [0xFF, 0x52])
648            .expect("COD marker");
649        assert_eq!(codestream[cod_offset + 12], 0x40);
650
651        let image = Image::new(
652            &codestream,
653            &DecodeSettings {
654                resolve_palette_indices: true,
655                strict: true,
656                target_resolution: None,
657            },
658        )
659        .expect("parse HT codestream");
660        let decoded = image.decode_native().expect("decode HT codestream");
661
662        assert_eq!(decoded.width, width);
663        assert_eq!(decoded.height, height);
664        assert_eq!(decoded.bit_depth, bit_depth);
665        assert_eq!(decoded.data, pixels);
666    }
667
668    fn gradient_u8(width: u32, height: u32) -> Vec<u8> {
669        let mut pixels = Vec::with_capacity((width * height) as usize);
670        for y in 0..height {
671            for x in 0..width {
672                pixels.push(((x * 17 + y * 31) % 256) as u8);
673            }
674        }
675        pixels
676    }
677
678    #[test]
679    fn test_encode_high_throughput_zero_image_roundtrip() {
680        let width = 4u32;
681        let height = 4u32;
682        let sample = 2048u16.to_le_bytes();
683        let mut pixels = Vec::with_capacity((width * height * 2) as usize);
684        for _ in 0..(width * height) {
685            pixels.extend_from_slice(&sample);
686        }
687
688        let codestream = encode(
689            &pixels,
690            width,
691            height,
692            1,
693            12,
694            false,
695            &EncodeOptions {
696                num_decomposition_levels: 2,
697                use_ht_block_coding: true,
698                ..Default::default()
699            },
700        )
701        .expect("HT all-zero encode");
702
703        assert!(codestream.windows(2).any(|window| window == [0xFF, 0x50]));
704        let cod_offset = codestream
705            .windows(2)
706            .position(|window| window == [0xFF, 0x52])
707            .expect("COD marker");
708        assert_eq!(codestream[cod_offset + 12], 0x40);
709
710        let image =
711            Image::new(&codestream, &DecodeSettings::default()).expect("parse HT codestream");
712        let decoded = image.decode_native().expect("decode HT codestream");
713
714        assert_eq!(decoded.width, width);
715        assert_eq!(decoded.height, height);
716        assert_eq!(decoded.bit_depth, 12);
717        assert_eq!(decoded.data, pixels);
718    }
719
720    #[test]
721    fn test_encode_high_throughput_nonzero_roundtrip() {
722        let width = 1u32;
723        let height = 1u32;
724        let pixels = 2049u16.to_le_bytes().to_vec();
725
726        let codestream = encode_htj2k(
727            &pixels,
728            width,
729            height,
730            1,
731            12,
732            false,
733            &EncodeOptions {
734                num_decomposition_levels: 0,
735                ..Default::default()
736            },
737        )
738        .expect("HT non-zero encode");
739
740        assert!(codestream.windows(2).any(|window| window == [0xFF, 0x50]));
741        let image =
742            Image::new(&codestream, &DecodeSettings::default()).expect("parse HT codestream");
743        let decoded = image.decode_native().expect("decode HT codestream");
744
745        assert_eq!(decoded.width, width);
746        assert_eq!(decoded.height, height);
747        assert_eq!(decoded.bit_depth, 12);
748        assert_eq!(decoded.data, pixels);
749    }
750
751    #[test]
752    fn test_encode_high_throughput_varied_12bit_roundtrip() {
753        let mut pixels = Vec::with_capacity(32);
754        for i in 0u16..16 {
755            pixels.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
756        }
757
758        let codestream = encode_htj2k(
759            &pixels,
760            4,
761            4,
762            1,
763            12,
764            false,
765            &EncodeOptions {
766                num_decomposition_levels: 1,
767                ..Default::default()
768            },
769        )
770        .expect("HT varied encode");
771
772        let image =
773            Image::new(&codestream, &DecodeSettings::default()).expect("parse HT codestream");
774        let decoded = image.decode_native().expect("decode HT codestream");
775
776        assert_eq!(decoded.width, 4);
777        assert_eq!(decoded.height, 4);
778        assert_eq!(decoded.bit_depth, 12);
779        assert_eq!(decoded.data, pixels);
780    }
781
782    #[test]
783    fn test_encode_high_throughput_gradient_8bit_roundtrip() {
784        let pixels: Vec<u8> = (0..64).collect();
785
786        let codestream = encode_htj2k(
787            &pixels,
788            8,
789            8,
790            1,
791            8,
792            false,
793            &EncodeOptions {
794                num_decomposition_levels: 3,
795                ..Default::default()
796            },
797        )
798        .expect("HT gradient encode");
799
800        let image =
801            Image::new(&codestream, &DecodeSettings::default()).expect("parse HT codestream");
802        let decoded = image.decode_native().expect("decode HT codestream");
803
804        assert_eq!(decoded.width, 8);
805        assert_eq!(decoded.height, 8);
806        assert_eq!(decoded.bit_depth, 8);
807        assert_eq!(decoded.data, pixels);
808    }
809
810    #[test]
811    fn test_encode_high_throughput_varied_12bit_large_roundtrip() {
812        let width = 16u32;
813        let height = 8u32;
814        let mut pixels = Vec::with_capacity((width * height * 2) as usize);
815        for y in 0u16..height as u16 {
816            for x in 0u16..width as u16 {
817                let value = (x * 257 + y * 17) & 0x0FFF;
818                pixels.extend_from_slice(&value.to_le_bytes());
819            }
820        }
821
822        assert_htj2k_lossless_roundtrip(&pixels, width, height, 12, 4);
823    }
824
825    #[test]
826    fn test_encode_high_throughput_ramp_16bit_roundtrip() {
827        let width = 48u32;
828        let height = 24u32;
829        let mut pixels = Vec::with_capacity((width * height * 2) as usize);
830        for y in 0u16..height as u16 {
831            for x in 0u16..width as u16 {
832                let value = x * 521 + y * 997;
833                pixels.extend_from_slice(&value.to_le_bytes());
834            }
835        }
836
837        assert_htj2k_lossless_roundtrip(&pixels, width, height, 16, 4);
838    }
839
840    #[test]
841    fn test_encode_high_throughput_lossy_large_gradient_is_parseable() {
842        let pixels = gradient_u8(128, 128);
843
844        let codestream = encode_htj2k(
845            &pixels,
846            128,
847            128,
848            1,
849            8,
850            false,
851            &EncodeOptions {
852                num_decomposition_levels: 5,
853                reversible: false,
854                guard_bits: 2,
855                ..Default::default()
856            },
857        )
858        .expect("lossy HT encode");
859
860        assert!(codestream.windows(2).any(|window| window == [0xFF, 0x50]));
861        assert!(!codestream.is_empty());
862
863        let image = Image::new(
864            &codestream,
865            &DecodeSettings {
866                resolve_palette_indices: true,
867                strict: true,
868                target_resolution: None,
869            },
870        )
871        .expect("parse lossy HT codestream");
872        let decoded = image.decode_native().expect("decode lossy HT codestream");
873
874        assert_eq!(decoded.width, 128);
875        assert_eq!(decoded.height, 128);
876        assert_eq!(decoded.bit_depth, 8);
877    }
878
879    #[test]
880    fn test_encode_invalid_dimensions() {
881        let result = encode(&[], 0, 0, 1, 8, false, &EncodeOptions::default());
882        assert!(result.is_err());
883    }
884
885    #[test]
886    fn test_encode_too_short() {
887        let pixels = vec![0u8; 10]; // Way too short for 8x8
888        let result = encode(&pixels, 8, 8, 1, 8, false, &EncodeOptions::default());
889        assert!(result.is_err());
890    }
891
892    #[test]
893    fn test_deinterleave_rgb() {
894        let pixels = vec![
895            10u8, 20, 30, // pixel 0: R=10, G=20, B=30
896            40, 50, 60, // pixel 1: R=40, G=50, B=60
897        ];
898        let comps = deinterleave_to_f32(&pixels, 2, 3, 8, false);
899        assert_eq!(comps[0], vec![-118.0, -88.0]); // R
900        assert_eq!(comps[1], vec![-108.0, -78.0]); // G
901        assert_eq!(comps[2], vec![-98.0, -68.0]); // B
902    }
903
904    #[test]
905    fn test_encode_decode_roundtrip_gray_8bit() {
906        use crate::{DecodeSettings, Image};
907
908        // Constant image: all pixels = 42 — simplest possible test
909        let original: Vec<u8> = vec![42u8; 64]; // 8x8, all same value
910        let encoded = encode(
911            &original,
912            8,
913            8,
914            1,
915            8,
916            false,
917            &EncodeOptions {
918                num_decomposition_levels: 0,
919                reversible: true,
920                ..Default::default()
921            },
922        )
923        .expect("encode failed");
924
925        let settings = DecodeSettings {
926            resolve_palette_indices: false,
927            strict: false,
928            target_resolution: None,
929        };
930        let image = Image::new(&encoded, &settings).expect("parse failed");
931        let decoded = image.decode_native().expect("decode failed");
932
933        assert_eq!(decoded.width, 8);
934        assert_eq!(decoded.height, 8);
935        assert_eq!(decoded.data, original, "round-trip mismatch");
936    }
937}