Skip to main content

dicom_toolkit_codec/jpeg_ls/
marker.rs

1//! JPEG-LS marker parsing and writing (SOI, SOF-55, SOS, LSE, EOI, APP8).
2//!
3//! Port of CharLS header.cc marker handling.
4
5use dicom_toolkit_core::error::{DcmError, DcmResult};
6
7use super::params::{ColorTransform, InterleaveMode, JlsCustomParameters, JlsParameters};
8
9// Marker bytes (without the leading 0xFF).
10const JPEG_SOI: u8 = 0xD8;
11const JPEG_EOI: u8 = 0xD9;
12const JPEG_SOS: u8 = 0xDA;
13const JPEG_SOF55: u8 = 0xF7;
14const JPEG_LSE: u8 = 0xF8;
15const JPEG_APP8: u8 = 0xE8;
16const JPEG_COM: u8 = 0xFE;
17const JPEG_DRI: u8 = 0xDD;
18
19/// APP8 color-transform tag.
20const COLOR_XFORM_TAG: &[u8; 4] = b"mrfx";
21
22/// Result of parsing a JPEG-LS bitstream's markers.
23#[derive(Debug, Clone)]
24pub struct FrameInfo {
25    pub params: JlsParameters,
26    /// Byte offset where each scan's compressed data begins.
27    pub scan_offsets: Vec<usize>,
28}
29
30/// Parse all JPEG-LS markers from `data`, returning frame parameters and scan data offsets.
31pub fn parse_markers(data: &[u8]) -> DcmResult<FrameInfo> {
32    let len = data.len();
33    if len < 4 {
34        return Err(DcmError::DecompressionError {
35            reason: "JPEG-LS: data too short".into(),
36        });
37    }
38
39    // Must start with SOI.
40    if data[0] != 0xFF || data[1] != JPEG_SOI {
41        return Err(DcmError::DecompressionError {
42            reason: "JPEG-LS: missing SOI marker".into(),
43        });
44    }
45
46    let mut pos = 2;
47    let mut params = JlsParameters::default();
48    let mut scan_offsets = Vec::new();
49    let mut got_sof = false;
50
51    while pos + 1 < len {
52        if data[pos] != 0xFF {
53            return Err(DcmError::DecompressionError {
54                reason: format!("JPEG-LS: expected marker at offset {pos}"),
55            });
56        }
57        let marker = data[pos + 1];
58        pos += 2;
59
60        match marker {
61            JPEG_EOI => break,
62
63            JPEG_SOF55 => {
64                // SOF-55 (JPEG-LS frame header).
65                if pos + 2 > len {
66                    return Err(short_data_err());
67                }
68                let seg_len = read_u16_be(data, pos) as usize;
69                if pos + seg_len > len || seg_len < 8 {
70                    return Err(short_data_err());
71                }
72                params.bits_per_sample = data[pos + 2];
73                params.height = read_u16_be(data, pos + 3) as u32;
74                params.width = read_u16_be(data, pos + 5) as u32;
75                params.components = data[pos + 7];
76
77                // Validate basic constraints.
78                if params.bits_per_sample < 2 || params.bits_per_sample > 16 {
79                    return Err(DcmError::DecompressionError {
80                        reason: format!(
81                            "JPEG-LS: unsupported bit depth {}",
82                            params.bits_per_sample
83                        ),
84                    });
85                }
86                if params.components == 0 || params.components > 4 {
87                    return Err(DcmError::DecompressionError {
88                        reason: format!(
89                            "JPEG-LS: unsupported component count {}",
90                            params.components
91                        ),
92                    });
93                }
94
95                got_sof = true;
96                pos += seg_len;
97            }
98
99            JPEG_SOS => {
100                // Start of Scan.
101                if pos + 2 > len {
102                    return Err(short_data_err());
103                }
104                let seg_len = read_u16_be(data, pos) as usize;
105                if pos + seg_len > len || seg_len < 6 {
106                    return Err(short_data_err());
107                }
108
109                let ns = data[pos + 2]; // Number of components in this scan.
110                                        // Component spec entries: Ns * 2 bytes at pos+3.
111                let near_offset = pos + 3 + (ns as usize * 2);
112                if near_offset + 2 > pos + seg_len {
113                    return Err(short_data_err());
114                }
115                params.near = data[near_offset] as i32;
116                let ilv = data[near_offset + 1];
117                params.interleave = InterleaveMode::from_u8(ilv).unwrap_or(InterleaveMode::None);
118
119                // The scan data starts right after the SOS segment.
120                let scan_start = pos + seg_len;
121                scan_offsets.push(scan_start);
122
123                // Skip over entropy-coded data to find the next marker.
124                // In JPEG-LS, after 0xFF the next byte's MSB is 0 for entropy
125                // data (bit-stuffed) and >= 0x80 for a real marker.
126                pos = scan_start;
127                while pos < len {
128                    if data[pos] == 0xFF && pos + 1 < len && data[pos + 1] >= 0x80 {
129                        break;
130                    }
131                    pos += 1;
132                }
133            }
134
135            JPEG_LSE => {
136                // JPEG-LS Extension (custom parameters).
137                if pos + 2 > len {
138                    return Err(short_data_err());
139                }
140                let seg_len = read_u16_be(data, pos) as usize;
141                if pos + seg_len > len {
142                    return Err(short_data_err());
143                }
144                if seg_len >= 13 {
145                    let id = data[pos + 2];
146                    if id == 1 {
147                        // Preset coding parameters (type 1).
148                        params.custom = JlsCustomParameters {
149                            max_val: read_u16_be(data, pos + 3) as i32,
150                            t1: read_u16_be(data, pos + 5) as i32,
151                            t2: read_u16_be(data, pos + 7) as i32,
152                            t3: read_u16_be(data, pos + 9) as i32,
153                            reset: read_u16_be(data, pos + 11) as i32,
154                        };
155                    }
156                }
157                pos += seg_len;
158            }
159
160            JPEG_APP8 => {
161                // HP color transform.
162                if pos + 2 > len {
163                    return Err(short_data_err());
164                }
165                let seg_len = read_u16_be(data, pos) as usize;
166                if pos + seg_len > len {
167                    return Err(short_data_err());
168                }
169                if seg_len >= 7 && &data[pos + 2..pos + 6] == COLOR_XFORM_TAG {
170                    let xform = data[pos + 6];
171                    params.color_transform =
172                        ColorTransform::from_u8(xform).unwrap_or(ColorTransform::None);
173                }
174                pos += seg_len;
175            }
176
177            JPEG_COM | JPEG_DRI => {
178                // Skip comment and restart-interval markers.
179                if pos + 2 > len {
180                    return Err(short_data_err());
181                }
182                let seg_len = read_u16_be(data, pos) as usize;
183                pos += seg_len;
184            }
185
186            m if (0xE0..=0xEF).contains(&m) => {
187                // Other APPn markers — skip.
188                if pos + 2 > len {
189                    return Err(short_data_err());
190                }
191                let seg_len = read_u16_be(data, pos) as usize;
192                pos += seg_len;
193            }
194
195            _ => {
196                // Unknown marker — try skipping if it has a length.
197                if pos + 2 <= len {
198                    let seg_len = read_u16_be(data, pos) as usize;
199                    pos += seg_len;
200                }
201            }
202        }
203    }
204
205    if !got_sof {
206        return Err(DcmError::DecompressionError {
207            reason: "JPEG-LS: no SOF-55 marker found".into(),
208        });
209    }
210
211    Ok(FrameInfo {
212        params,
213        scan_offsets,
214    })
215}
216
217/// Write a minimal JPEG-LS bitstream header (SOI + SOF55 + APP8 + SOS).
218pub fn write_header(params: &JlsParameters) -> Vec<u8> {
219    let mut buf = Vec::with_capacity(64);
220
221    // SOI
222    buf.push(0xFF);
223    buf.push(JPEG_SOI);
224
225    // APP8 color transform (if applicable)
226    if params.color_transform != ColorTransform::None {
227        buf.push(0xFF);
228        buf.push(JPEG_APP8);
229        let seg_len: u16 = 7; // length includes 2 length bytes + 4 tag + 1 xform
230        buf.extend_from_slice(&seg_len.to_be_bytes());
231        buf.extend_from_slice(COLOR_XFORM_TAG);
232        buf.push(params.color_transform as u8);
233    }
234
235    // SOF-55 (JPEG-LS frame header)
236    buf.push(0xFF);
237    buf.push(JPEG_SOF55);
238    let sof_len: u16 = 8 + 3 * params.components as u16;
239    buf.extend_from_slice(&sof_len.to_be_bytes());
240    buf.push(params.bits_per_sample);
241    buf.extend_from_slice(&(params.height as u16).to_be_bytes());
242    buf.extend_from_slice(&(params.width as u16).to_be_bytes());
243    buf.push(params.components);
244    for c in 0..params.components {
245        buf.push(c + 1); // Component ID (1-based).
246        buf.push(0x11); // Sampling factors (1×1).
247        buf.push(0); // Quantization table (unused).
248    }
249
250    // LSE — custom parameters (if set)
251    if params.custom.max_val > 0 {
252        write_lse_params(&mut buf, &params.custom);
253    }
254
255    // SOS (Start of Scan)
256    let ns = if params.interleave == InterleaveMode::None {
257        1
258    } else {
259        params.components
260    };
261    buf.push(0xFF);
262    buf.push(JPEG_SOS);
263    let sos_len: u16 = 6 + 2 * ns as u16;
264    buf.extend_from_slice(&sos_len.to_be_bytes());
265    buf.push(ns);
266    for c in 0..ns {
267        buf.push(c + 1); // Component ID.
268        buf.push(0); // Mapping table index.
269    }
270    buf.push(params.near as u8);
271    buf.push(params.interleave as u8);
272    buf.push(0); // Successive approximation (unused).
273
274    buf
275}
276
277/// Write a JPEG-LS LSE (parameter extension) segment for custom thresholds.
278fn write_lse_params(buf: &mut Vec<u8>, custom: &JlsCustomParameters) {
279    buf.push(0xFF);
280    buf.push(JPEG_LSE);
281    let seg_len: u16 = 13;
282    buf.extend_from_slice(&seg_len.to_be_bytes());
283    buf.push(1); // LSE type: preset coding parameters.
284    buf.extend_from_slice(&(custom.max_val as u16).to_be_bytes());
285    buf.extend_from_slice(&(custom.t1 as u16).to_be_bytes());
286    buf.extend_from_slice(&(custom.t2 as u16).to_be_bytes());
287    buf.extend_from_slice(&(custom.t3 as u16).to_be_bytes());
288    buf.extend_from_slice(&(custom.reset as u16).to_be_bytes());
289}
290
291/// Write the EOI (end of image) marker.
292pub fn write_eoi(buf: &mut Vec<u8>) {
293    buf.push(0xFF);
294    buf.push(JPEG_EOI);
295}
296
297fn read_u16_be(data: &[u8], offset: usize) -> u16 {
298    ((data[offset] as u16) << 8) | data[offset + 1] as u16
299}
300
301fn short_data_err() -> DcmError {
302    DcmError::DecompressionError {
303        reason: "JPEG-LS: truncated marker segment".into(),
304    }
305}
306
307// ── Tests ─────────────────────────────────────────────────────────────────────
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn parse_minimal_header() {
315        let params = JlsParameters {
316            width: 64,
317            height: 48,
318            bits_per_sample: 8,
319            components: 1,
320            near: 0,
321            interleave: InterleaveMode::None,
322            ..Default::default()
323        };
324        let mut header = write_header(&params);
325        // Add fake scan data byte + EOI.
326        header.push(0x00);
327        write_eoi(&mut header);
328
329        let info = parse_markers(&header).unwrap();
330        assert_eq!(info.params.width, 64);
331        assert_eq!(info.params.height, 48);
332        assert_eq!(info.params.bits_per_sample, 8);
333        assert_eq!(info.params.components, 1);
334        assert_eq!(info.params.near, 0);
335        assert_eq!(info.params.interleave, InterleaveMode::None);
336        assert_eq!(info.scan_offsets.len(), 1);
337    }
338
339    #[test]
340    fn parse_with_color_transform() {
341        let params = JlsParameters {
342            width: 256,
343            height: 256,
344            bits_per_sample: 8,
345            components: 3,
346            near: 0,
347            interleave: InterleaveMode::Line,
348            color_transform: ColorTransform::Hp1,
349            ..Default::default()
350        };
351        let mut header = write_header(&params);
352        header.push(0x00);
353        write_eoi(&mut header);
354
355        let info = parse_markers(&header).unwrap();
356        assert_eq!(info.params.color_transform, ColorTransform::Hp1);
357        assert_eq!(info.params.interleave, InterleaveMode::Line);
358        assert_eq!(info.params.components, 3);
359    }
360
361    #[test]
362    fn parse_with_custom_params() {
363        let params = JlsParameters {
364            width: 32,
365            height: 32,
366            bits_per_sample: 12,
367            components: 1,
368            near: 0,
369            interleave: InterleaveMode::None,
370            custom: JlsCustomParameters {
371                max_val: 4095,
372                t1: 10,
373                t2: 20,
374                t3: 30,
375                reset: 64,
376            },
377            ..Default::default()
378        };
379        let mut header = write_header(&params);
380        header.push(0x00);
381        write_eoi(&mut header);
382
383        let info = parse_markers(&header).unwrap();
384        assert_eq!(info.params.custom.max_val, 4095);
385        assert_eq!(info.params.custom.t1, 10);
386        assert_eq!(info.params.custom.t2, 20);
387        assert_eq!(info.params.custom.t3, 30);
388        assert_eq!(info.params.custom.reset, 64);
389    }
390
391    #[test]
392    fn reject_missing_soi() {
393        let data = [0x00, 0x00, 0xFF, 0xD9];
394        assert!(parse_markers(&data).is_err());
395    }
396
397    #[test]
398    fn reject_too_short() {
399        let data = [0xFF, 0xD8];
400        assert!(parse_markers(&data).is_err());
401    }
402}