Skip to main content

dicom_toolkit_codec/jp2k/
encoder.rs

1//! JPEG 2000 encoder — wraps the forked `dicom-toolkit-jpeg2000` encoder for DICOM use.
2//!
3//! Encodes raw pixel data into JPEG 2000 codestreams suitable for embedding
4//! in DICOM encapsulated pixel data.
5
6use dicom_toolkit_core::error::{DcmError, DcmResult};
7use dicom_toolkit_jpeg2000::{encode as j2k_encode, encode_htj2k as htj2k_encode, EncodeOptions};
8
9/// Encode raw pixel data into a JPEG 2000 codestream.
10///
11/// # Arguments
12/// * `pixels` — Raw pixel bytes. For ≤8-bit: one byte per sample.
13///   For >8-bit: two bytes per sample in little-endian u16 layout.
14/// * `width` — Image width in pixels.
15/// * `height` — Image height in pixels.
16/// * `bits_per_sample` — Actual sample precision written into the codestream
17///   (for example 8, 12, or 16).
18/// * `samples_per_pixel` — Number of components (1=grayscale, 3=RGB).
19/// * `lossless` — If true, use reversible DWT 5-3 (lossless); if false, use DWT 9-7 (lossy).
20///
21/// # Returns
22/// The encoded JPEG 2000 codestream bytes (`.j2c` format).
23pub fn encode_jp2k(
24    pixels: &[u8],
25    width: u32,
26    height: u32,
27    bits_per_sample: u8,
28    samples_per_pixel: u8,
29    lossless: bool,
30) -> DcmResult<Vec<u8>> {
31    encode_with_mode(
32        pixels,
33        width,
34        height,
35        bits_per_sample,
36        samples_per_pixel,
37        lossless,
38        false,
39    )
40}
41
42/// Encode raw pixel data into an HTJ2K codestream.
43pub fn encode_htj2k(
44    pixels: &[u8],
45    width: u32,
46    height: u32,
47    bits_per_sample: u8,
48    samples_per_pixel: u8,
49    lossless: bool,
50) -> DcmResult<Vec<u8>> {
51    encode_with_mode(
52        pixels,
53        width,
54        height,
55        bits_per_sample,
56        samples_per_pixel,
57        lossless,
58        true,
59    )
60}
61
62fn encode_with_mode(
63    pixels: &[u8],
64    width: u32,
65    height: u32,
66    bits_per_sample: u8,
67    samples_per_pixel: u8,
68    lossless: bool,
69    use_ht_block_coding: bool,
70) -> DcmResult<Vec<u8>> {
71    let codec_name = if use_ht_block_coding {
72        "HTJ2K"
73    } else {
74        "JPEG 2000"
75    };
76
77    if bits_per_sample == 0 || bits_per_sample > 16 {
78        return Err(DcmError::CompressionError {
79            reason: format!("{codec_name}: unsupported bit depth {bits_per_sample}"),
80        });
81    }
82
83    let bit_depth = bits_per_sample;
84
85    // Choose appropriate decomposition levels based on image size.
86    // Cannot have more levels than log2(min(width, height)).
87    let max_levels = (width.min(height) as f64).log2().floor() as u8;
88    let num_levels = max_levels.min(5);
89
90    let options = EncodeOptions {
91        num_decomposition_levels: num_levels,
92        reversible: lossless,
93        guard_bits: if lossless { 1 } else { 2 },
94        use_ht_block_coding,
95        ..Default::default()
96    };
97
98    let encode_impl = if use_ht_block_coding {
99        htj2k_encode
100    } else {
101        j2k_encode
102    };
103
104    encode_impl(
105        pixels,
106        width,
107        height,
108        samples_per_pixel,
109        bit_depth,
110        false, // DICOM pixel data is unsigned by convention
111        &options,
112    )
113    .map_err(|e| DcmError::CompressionError {
114        reason: format!("{codec_name} encode error: {e}"),
115    })
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn assert_lossless_htj2k_roundtrip(
123        pixels: &[u8],
124        width: u32,
125        height: u32,
126        bits_per_sample: u8,
127        samples_per_pixel: u8,
128    ) {
129        let encoded = encode_htj2k(
130            pixels,
131            width,
132            height,
133            bits_per_sample,
134            samples_per_pixel,
135            true,
136        )
137        .expect("HTJ2K encode");
138        assert!(encoded.windows(2).any(|window| window == [0xFF, 0x50]));
139
140        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).expect("HTJ2K decode");
141        assert_eq!(decoded.width, width);
142        assert_eq!(decoded.height, height);
143        assert_eq!(decoded.bits_per_sample, bits_per_sample);
144        assert_eq!(decoded.components, samples_per_pixel);
145        assert_eq!(decoded.pixels, pixels);
146    }
147
148    fn gradient_u8(width: u32, height: u32) -> Vec<u8> {
149        let mut pixels = Vec::with_capacity((width * height) as usize);
150        for y in 0..height {
151            for x in 0..width {
152                pixels.push(((x * 17 + y * 31) % 256) as u8);
153            }
154        }
155        pixels
156    }
157
158    #[test]
159    fn encode_grayscale_8bit() {
160        let pixels: Vec<u8> = (0..64).collect();
161        let result = encode_jp2k(&pixels, 8, 8, 8, 1, true);
162        assert!(result.is_ok(), "encode failed: {:?}", result.err());
163        let encoded = result.unwrap();
164        // J2K codestream starts with SOC marker (FF 4F)
165        assert!(encoded.len() > 4);
166        assert_eq!(encoded[0], 0xFF);
167        assert_eq!(encoded[1], 0x4F);
168    }
169
170    #[test]
171    fn encode_grayscale_16bit() {
172        // 4×4 image, 16-bit: 32 bytes (16 samples × 2 bytes each)
173        let mut pixels = Vec::with_capacity(32);
174        for i in 0u16..16 {
175            pixels.extend_from_slice(&(i * 256).to_le_bytes());
176        }
177        let result = encode_jp2k(&pixels, 4, 4, 16, 1, true);
178        assert!(result.is_ok(), "encode failed: {:?}", result.err());
179    }
180
181    #[test]
182    fn encode_rgb_8bit() {
183        let pixels: Vec<u8> = (0..192).map(|i| (i & 0xFF) as u8).collect();
184        let result = encode_jp2k(&pixels, 8, 8, 8, 3, true);
185        assert!(result.is_ok(), "encode failed: {:?}", result.err());
186    }
187
188    #[test]
189    fn encode_lossy() {
190        let pixels: Vec<u8> = (0..64).collect();
191        let result = encode_jp2k(&pixels, 8, 8, 8, 1, false);
192        assert!(result.is_ok(), "encode failed: {:?}", result.err());
193    }
194
195    #[test]
196    fn encode_roundtrip_8bit() {
197        let original: Vec<u8> = (0..64).collect();
198        let encoded = encode_jp2k(&original, 8, 8, 8, 1, true).unwrap();
199
200        // Decode using the decoder
201        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).unwrap();
202        assert_eq!(decoded.width, 8);
203        assert_eq!(decoded.height, 8);
204        assert_eq!(decoded.bits_per_sample, 8);
205        assert_eq!(decoded.components, 1);
206        assert_eq!(decoded.pixels, original);
207    }
208
209    #[test]
210    fn encode_roundtrip_16bit() {
211        let mut original = Vec::with_capacity(32);
212        for i in 0u16..16 {
213            original.extend_from_slice(&(i * 257).to_le_bytes());
214        }
215
216        let encoded = encode_jp2k(&original, 4, 4, 16, 1, true).unwrap();
217        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).unwrap();
218
219        assert_eq!(decoded.width, 4);
220        assert_eq!(decoded.height, 4);
221        assert_eq!(decoded.bits_per_sample, 16);
222        assert_eq!(decoded.components, 1);
223        assert_eq!(decoded.pixels, original);
224    }
225
226    #[test]
227    fn encode_roundtrip_12bit_in_u16_container() {
228        let mut original = Vec::with_capacity(32);
229        for i in 0u16..16 {
230            original.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
231        }
232
233        let encoded = encode_jp2k(&original, 4, 4, 12, 1, true).unwrap();
234        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).unwrap();
235
236        assert_eq!(decoded.width, 4);
237        assert_eq!(decoded.height, 4);
238        assert_eq!(decoded.bits_per_sample, 12);
239        assert_eq!(decoded.components, 1);
240        assert_eq!(decoded.pixels, original);
241    }
242
243    #[test]
244    fn encode_htj2k_roundtrip_12bit() {
245        let mut original = Vec::with_capacity(32);
246        for _ in 0..16 {
247            original.extend_from_slice(&2048u16.to_le_bytes());
248        }
249
250        let encoded = encode_htj2k(&original, 4, 4, 12, 1, true).unwrap();
251        assert!(encoded.windows(2).any(|window| window == [0xFF, 0x50]));
252
253        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).unwrap();
254        assert_eq!(decoded.width, 4);
255        assert_eq!(decoded.height, 4);
256        assert_eq!(decoded.bits_per_sample, 12);
257        assert_eq!(decoded.components, 1);
258        assert_eq!(decoded.pixels, original);
259    }
260
261    #[test]
262    fn encode_htj2k_lossless_roundtrip_varied_12bit() {
263        let mut original = Vec::with_capacity(32);
264        for i in 0u16..16 {
265            original.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
266        }
267
268        let encoded = encode_htj2k(&original, 4, 4, 12, 1, true).unwrap();
269        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).unwrap();
270        assert_eq!(decoded.width, 4);
271        assert_eq!(decoded.height, 4);
272        assert_eq!(decoded.bits_per_sample, 12);
273        assert_eq!(decoded.components, 1);
274        assert_eq!(decoded.pixels, original);
275    }
276
277    #[test]
278    fn encode_htj2k_lossy_is_parseable() {
279        let pixels: Vec<u8> = (0..64).collect();
280        let encoded = encode_htj2k(&pixels, 8, 8, 8, 1, false).unwrap();
281        assert!(encoded.windows(2).any(|window| window == [0xFF, 0x50]));
282
283        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).unwrap();
284        assert_eq!(decoded.width, 8);
285        assert_eq!(decoded.height, 8);
286        assert_eq!(decoded.bits_per_sample, 8);
287        assert_eq!(decoded.components, 1);
288    }
289
290    #[test]
291    fn encode_htj2k_lossless_roundtrip_gradient_8bit() {
292        let gradient: Vec<u8> = (0..64).collect();
293        assert_lossless_htj2k_roundtrip(&gradient, 8, 8, 8, 1);
294    }
295
296    #[test]
297    fn encode_htj2k_lossless_roundtrip_large_16bit() {
298        let mut ramp = Vec::with_capacity(48 * 24 * 2);
299        for y in 0u16..24 {
300            for x in 0u16..48 {
301                let value = x * 521 + y * 997;
302                ramp.extend_from_slice(&value.to_le_bytes());
303            }
304        }
305
306        assert_lossless_htj2k_roundtrip(&ramp, 48, 24, 16, 1);
307    }
308
309    #[test]
310    fn encode_htj2k_lossy_large_gradient_has_stable_decode() {
311        let pixels = gradient_u8(128, 128);
312        let encoded = encode_htj2k(&pixels, 128, 128, 8, 1, false).expect("lossy HTJ2K encode");
313        assert!(encoded.windows(2).any(|window| window == [0xFF, 0x50]));
314        assert!(!encoded.is_empty());
315
316        let decoded = crate::jp2k::decoder::decode_jp2k(&encoded).expect("lossy HTJ2K decode");
317        assert_eq!(decoded.width, 128);
318        assert_eq!(decoded.height, 128);
319        assert_eq!(decoded.bits_per_sample, 8);
320        assert_eq!(decoded.components, 1);
321    }
322}