Skip to main content

dicom_toolkit_codec/
registry.rs

1//! Codec registry — maps DICOM transfer syntax UIDs to codec implementations.
2//!
3//! Ports DCMTK's `DcmCodecList` singleton. Codecs can be registered either
4//! statically (via compile-time dependencies) or dynamically.
5
6use std::collections::HashMap;
7use std::sync::{Arc, LazyLock, RwLock};
8
9use dicom_toolkit_core::error::{DcmError, DcmResult};
10use dicom_toolkit_data::value::PixelData;
11use dicom_toolkit_dict::ts::transfer_syntaxes;
12
13// ── ImageCodec trait ──────────────────────────────────────────────────────────
14
15/// Trait for DICOM image codecs.
16///
17/// Each codec handles one or more transfer syntaxes.  The codec is responsible
18/// for decompressing/compressing pixel data items.
19pub trait ImageCodec: Send + Sync {
20    /// UID(s) of the transfer syntax(es) this codec handles.
21    fn transfer_syntax_uids(&self) -> &[&str];
22
23    /// Decode all frames in `encapsulated` pixel data.
24    ///
25    /// Returns the raw, uncompressed pixel bytes for all frames concatenated.
26    fn decode(
27        &self,
28        encapsulated: &PixelData,
29        rows: u16,
30        columns: u16,
31        samples_per_pixel: u8,
32        bits_allocated: u8,
33    ) -> DcmResult<Vec<u8>>;
34
35    /// Encode raw pixel bytes into an encapsulated `PixelData`.
36    ///
37    /// `bits_allocated` describes the native storage width of `pixels`.
38    /// `bits_stored` is the actual sample precision to encode into the
39    /// compressed stream and must be `<= bits_allocated`.
40    fn encode(
41        &self,
42        pixels: &[u8],
43        rows: u16,
44        columns: u16,
45        samples_per_pixel: u8,
46        bits_allocated: u8,
47        bits_stored: u8,
48    ) -> DcmResult<PixelData>;
49}
50
51fn validate_stored_bits(codec_name: &str, bits_allocated: u8, bits_stored: u8) -> DcmResult<()> {
52    if bits_stored == 0 {
53        return Err(DcmError::CompressionError {
54            reason: format!("{codec_name}: BitsStored must be at least 1"),
55        });
56    }
57    if bits_stored > bits_allocated {
58        return Err(DcmError::CompressionError {
59            reason: format!(
60                "{codec_name}: BitsStored ({bits_stored}) exceeds BitsAllocated ({bits_allocated})"
61            ),
62        });
63    }
64    Ok(())
65}
66
67// ── Built-in RLE codec ────────────────────────────────────────────────────────
68
69struct RleCodec;
70
71impl ImageCodec for RleCodec {
72    fn transfer_syntax_uids(&self) -> &[&str] {
73        &[transfer_syntaxes::RLE_LOSSLESS.uid]
74    }
75
76    fn decode(
77        &self,
78        pixel_data: &PixelData,
79        rows: u16,
80        columns: u16,
81        samples_per_pixel: u8,
82        bits_allocated: u8,
83    ) -> DcmResult<Vec<u8>> {
84        let fragments = match pixel_data {
85            PixelData::Encapsulated { fragments, .. } => fragments,
86            PixelData::Native { bytes } => return Ok(bytes.clone()),
87        };
88
89        let mut all_frames = Vec::new();
90        for fragment in fragments {
91            let frame = crate::rle::rle_decode_frame(
92                fragment,
93                rows,
94                columns,
95                samples_per_pixel,
96                bits_allocated,
97            )?;
98            all_frames.extend_from_slice(&frame);
99        }
100        Ok(all_frames)
101    }
102
103    fn encode(
104        &self,
105        pixels: &[u8],
106        rows: u16,
107        columns: u16,
108        samples_per_pixel: u8,
109        bits_allocated: u8,
110        _bits_stored: u8,
111    ) -> DcmResult<PixelData> {
112        let encoded =
113            crate::rle::rle_encode_frame(pixels, rows, columns, samples_per_pixel, bits_allocated)?;
114        Ok(PixelData::Encapsulated {
115            offset_table: vec![0],
116            fragments: vec![encoded],
117        })
118    }
119}
120
121// ── Built-in JPEG codec ───────────────────────────────────────────────────────
122
123struct JpegCodec {
124    uids: Vec<&'static str>,
125}
126
127impl JpegCodec {
128    fn baseline() -> Self {
129        Self {
130            uids: vec![
131                transfer_syntaxes::JPEG_BASELINE.uid,
132                transfer_syntaxes::JPEG_EXTENDED.uid,
133            ],
134        }
135    }
136}
137
138impl ImageCodec for JpegCodec {
139    fn transfer_syntax_uids(&self) -> &[&str] {
140        &self.uids
141    }
142
143    fn decode(
144        &self,
145        pixel_data: &PixelData,
146        _rows: u16,
147        _columns: u16,
148        _samples_per_pixel: u8,
149        _bits_allocated: u8,
150    ) -> DcmResult<Vec<u8>> {
151        let fragments = match pixel_data {
152            PixelData::Encapsulated { fragments, .. } => fragments,
153            PixelData::Native { bytes } => return Ok(bytes.clone()),
154        };
155
156        let mut all_frames = Vec::new();
157        for fragment in fragments {
158            let frame = crate::jpeg::decoder::decode_jpeg(fragment)?;
159            all_frames.extend_from_slice(&frame.data);
160        }
161        Ok(all_frames)
162    }
163
164    fn encode(
165        &self,
166        pixels: &[u8],
167        rows: u16,
168        columns: u16,
169        samples_per_pixel: u8,
170        _bits_allocated: u8,
171        _bits_stored: u8,
172    ) -> DcmResult<PixelData> {
173        use crate::jpeg::params::JpegParams;
174        let encoded = crate::jpeg::encoder::encode_jpeg(
175            pixels,
176            columns,
177            rows,
178            samples_per_pixel,
179            &JpegParams::default(),
180        )?;
181        Ok(PixelData::Encapsulated {
182            offset_table: vec![0],
183            fragments: vec![encoded],
184        })
185    }
186}
187
188// ── JPEG-LS codec ─────────────────────────────────────────────────────────────
189
190struct JpegLsCodec;
191
192impl ImageCodec for JpegLsCodec {
193    fn transfer_syntax_uids(&self) -> &[&str] {
194        &[
195            transfer_syntaxes::JPEG_LS_LOSSLESS.uid,
196            transfer_syntaxes::JPEG_LS_LOSSY.uid,
197        ]
198    }
199
200    fn decode(
201        &self,
202        pixel_data: &PixelData,
203        _rows: u16,
204        _columns: u16,
205        _samples_per_pixel: u8,
206        _bits_allocated: u8,
207    ) -> DcmResult<Vec<u8>> {
208        let fragments = match pixel_data {
209            PixelData::Encapsulated { fragments, .. } => fragments,
210            PixelData::Native { bytes } => return Ok(bytes.clone()),
211        };
212        let empty = vec![];
213        let data = fragments.first().unwrap_or(&empty);
214        let decoded = crate::jpeg_ls::decoder::decode_jpeg_ls(data)?;
215        Ok(decoded.pixels)
216    }
217
218    fn encode(
219        &self,
220        pixels: &[u8],
221        rows: u16,
222        columns: u16,
223        samples_per_pixel: u8,
224        bits_allocated: u8,
225        bits_stored: u8,
226    ) -> DcmResult<PixelData> {
227        validate_stored_bits("JPEG-LS", bits_allocated, bits_stored)?;
228        let near = 0; // Lossless by default.
229        let encoded = crate::jpeg_ls::encoder::encode_jpeg_ls(
230            pixels,
231            columns as u32,
232            rows as u32,
233            bits_stored,
234            samples_per_pixel,
235            near,
236        )?;
237        Ok(PixelData::Encapsulated {
238            offset_table: vec![],
239            fragments: vec![encoded],
240        })
241    }
242}
243
244// ── JPEG 2000 codec ───────────────────────────────────────────────────────────
245
246struct Jp2kRegistryCodec;
247
248impl ImageCodec for Jp2kRegistryCodec {
249    fn transfer_syntax_uids(&self) -> &[&str] {
250        &[
251            transfer_syntaxes::JPEG_2000_LOSSLESS.uid,
252            transfer_syntaxes::JPEG_2000.uid,
253        ]
254    }
255
256    fn decode(
257        &self,
258        pixel_data: &PixelData,
259        _rows: u16,
260        _columns: u16,
261        _samples_per_pixel: u8,
262        _bits_allocated: u8,
263    ) -> DcmResult<Vec<u8>> {
264        let fragments = match pixel_data {
265            PixelData::Encapsulated { fragments, .. } => fragments,
266            PixelData::Native { bytes } => return Ok(bytes.clone()),
267        };
268        let mut all_pixels = Vec::new();
269        for fragment in fragments {
270            if fragment.is_empty() {
271                continue;
272            }
273            let decoded = crate::jp2k::decoder::decode_jp2k(fragment)?;
274            all_pixels.extend_from_slice(&decoded.pixels);
275        }
276        Ok(all_pixels)
277    }
278
279    fn encode(
280        &self,
281        pixels: &[u8],
282        rows: u16,
283        columns: u16,
284        samples_per_pixel: u8,
285        bits_allocated: u8,
286        bits_stored: u8,
287    ) -> DcmResult<PixelData> {
288        validate_stored_bits("JPEG 2000", bits_allocated, bits_stored)?;
289        // TS .90 is lossless-only; .91 supports both (default to lossless).
290        let lossless = true;
291        let encoded = crate::jp2k::encoder::encode_jp2k(
292            pixels,
293            columns as u32,
294            rows as u32,
295            bits_stored,
296            samples_per_pixel,
297            lossless,
298        )?;
299        Ok(PixelData::Encapsulated {
300            offset_table: vec![],
301            fragments: vec![encoded],
302        })
303    }
304}
305
306// ── CodecRegistry ─────────────────────────────────────────────────────────────
307
308/// Registry of all available image codecs, keyed by transfer syntax UID.
309pub struct CodecRegistry {
310    codecs: RwLock<HashMap<String, Arc<dyn ImageCodec>>>,
311}
312
313impl CodecRegistry {
314    /// Create an empty registry.
315    pub fn new() -> Self {
316        Self {
317            codecs: RwLock::new(HashMap::new()),
318        }
319    }
320
321    /// Register a codec (replaces any existing codec for the same UID).
322    pub fn register(&self, codec: Arc<dyn ImageCodec>) {
323        let mut map = self.codecs.write().unwrap();
324        for uid in codec.transfer_syntax_uids() {
325            map.insert(uid.to_string(), Arc::clone(&codec));
326        }
327    }
328
329    /// Look up a codec by transfer syntax UID.
330    pub fn find(&self, transfer_syntax_uid: &str) -> Option<Arc<dyn ImageCodec>> {
331        self.codecs
332            .read()
333            .unwrap()
334            .get(transfer_syntax_uid)
335            .cloned()
336    }
337
338    /// Look up a codec or return a [`DcmError::NoCodec`] error.
339    pub fn find_required(&self, transfer_syntax_uid: &str) -> DcmResult<Arc<dyn ImageCodec>> {
340        self.find(transfer_syntax_uid)
341            .ok_or_else(|| DcmError::NoCodec {
342                uid: transfer_syntax_uid.to_string(),
343            })
344    }
345}
346
347impl Default for CodecRegistry {
348    fn default() -> Self {
349        Self::new()
350    }
351}
352
353// ── Global registry ───────────────────────────────────────────────────────────
354
355/// Global codec registry, pre-populated with built-in codecs.
356pub static GLOBAL_REGISTRY: LazyLock<CodecRegistry> = LazyLock::new(|| {
357    let reg = CodecRegistry::new();
358    reg.register(Arc::new(RleCodec));
359    reg.register(Arc::new(JpegCodec::baseline()));
360    reg.register(Arc::new(JpegLsCodec));
361    reg.register(Arc::new(Jp2kRegistryCodec));
362    reg
363});
364
365// ── Flat functional API ───────────────────────────────────────────────────────
366
367/// Transfer syntax UIDs that this crate can decode.
368const SUPPORTED_TS: &[&str] = &[
369    transfer_syntaxes::RLE_LOSSLESS.uid,
370    transfer_syntaxes::JPEG_BASELINE.uid,
371    transfer_syntaxes::JPEG_EXTENDED.uid,
372    transfer_syntaxes::JPEG_LOSSLESS.uid,
373    transfer_syntaxes::JPEG_LOSSLESS_SV1.uid,
374    transfer_syntaxes::JPEG_LS_LOSSLESS.uid,
375    transfer_syntaxes::JPEG_LS_LOSSY.uid,
376    transfer_syntaxes::JPEG_2000_LOSSLESS.uid,
377    transfer_syntaxes::JPEG_2000.uid,
378];
379
380/// Registered codec information for a transfer syntax.
381#[derive(Debug, Clone, Copy)]
382pub struct CodecInfo {
383    /// Transfer Syntax UID this codec handles.
384    pub transfer_syntax_uid: &'static str,
385    /// Human-readable name.
386    pub name: &'static str,
387}
388
389/// Returns `true` if a decoder is available for the given transfer syntax UID.
390pub fn can_decode(ts_uid: &str) -> bool {
391    SUPPORTED_TS.contains(&ts_uid)
392}
393
394/// Returns all transfer syntax UIDs that this crate can decode.
395pub fn supported_transfer_syntaxes() -> &'static [&'static str] {
396    SUPPORTED_TS
397}
398
399/// Decode a single pixel-data fragment for the given transfer syntax.
400///
401/// `data` must be the raw fragment bytes (RLE header + data, or JPEG bitstream).
402/// Returns the decoded pixel bytes in native little-endian order.
403pub fn decode_pixel_data(
404    ts_uid: &str,
405    data: &[u8],
406    rows: u16,
407    cols: u16,
408    bits_allocated: u16,
409    samples: u16,
410) -> DcmResult<Vec<u8>> {
411    match ts_uid {
412        uid if uid == transfer_syntaxes::RLE_LOSSLESS.uid => {
413            crate::rle::RleCodec::decode(data, rows, cols, bits_allocated, samples)
414        }
415        uid if uid == transfer_syntaxes::JPEG_BASELINE.uid
416            || uid == transfer_syntaxes::JPEG_EXTENDED.uid
417            || uid == transfer_syntaxes::JPEG_LOSSLESS.uid
418            || uid == transfer_syntaxes::JPEG_LOSSLESS_SV1.uid =>
419        {
420            crate::jpeg::JpegDecoder::decode_frame(data).map(|f| f.pixels)
421        }
422        uid if uid == transfer_syntaxes::JPEG_LS_LOSSLESS.uid
423            || uid == transfer_syntaxes::JPEG_LS_LOSSY.uid =>
424        {
425            crate::jpeg_ls::JpegLsCodec::decode_frame(data).map(|f| f.pixels)
426        }
427        uid if uid == transfer_syntaxes::JPEG_2000_LOSSLESS.uid
428            || uid == transfer_syntaxes::JPEG_2000.uid =>
429        {
430            crate::jp2k::Jp2kCodec::decode_frame(data).map(|f| f.pixels)
431        }
432        uid => Err(DcmError::NoCodec {
433            uid: uid.to_string(),
434        }),
435    }
436}
437
438// ── Tests ─────────────────────────────────────────────────────────────────────
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use dicom_toolkit_dict::ts::transfer_syntaxes;
444
445    #[test]
446    fn global_registry_has_rle() {
447        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::RLE_LOSSLESS.uid);
448        assert!(codec.is_some());
449    }
450
451    #[test]
452    fn global_registry_has_jpeg_baseline() {
453        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_BASELINE.uid);
454        assert!(codec.is_some());
455    }
456
457    #[test]
458    fn global_registry_has_jpeg_extended() {
459        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_EXTENDED.uid);
460        assert!(codec.is_some());
461    }
462
463    #[test]
464    fn global_registry_has_jpeg_ls() {
465        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_LS_LOSSLESS.uid);
466        assert!(codec.is_some());
467    }
468
469    #[test]
470    fn unknown_uid_returns_none() {
471        let codec = GLOBAL_REGISTRY.find("1.2.3.4.5.999");
472        assert!(codec.is_none());
473    }
474
475    #[test]
476    fn find_required_returns_error_for_unknown() {
477        let result = GLOBAL_REGISTRY.find_required("1.9.9.9.9");
478        assert!(matches!(result, Err(DcmError::NoCodec { .. })));
479    }
480
481    // ── Flat API tests ────────────────────────────────────────────────────────
482
483    #[test]
484    fn codec_registry_can_decode_rle() {
485        assert!(can_decode(transfer_syntaxes::RLE_LOSSLESS.uid));
486    }
487
488    #[test]
489    fn codec_registry_can_decode_jpeg_baseline() {
490        assert!(can_decode(transfer_syntaxes::JPEG_BASELINE.uid));
491    }
492
493    #[test]
494    fn codec_registry_cannot_decode_unknown() {
495        assert!(!can_decode("1.2.3.4.5.999"));
496    }
497
498    #[test]
499    fn supported_transfer_syntaxes_is_non_empty() {
500        let list = supported_transfer_syntaxes();
501        assert!(!list.is_empty());
502        assert!(list.contains(&transfer_syntaxes::RLE_LOSSLESS.uid));
503        assert!(list.contains(&transfer_syntaxes::JPEG_BASELINE.uid));
504    }
505
506    #[test]
507    fn rle_codec_roundtrip_via_registry() {
508        use crate::rle::rle_encode_frame;
509
510        let rows = 4u16;
511        let cols = 4u16;
512        let samples = 1u8;
513        let bits = 8u8;
514        let pixels: Vec<u8> = (0u8..16).collect();
515
516        let encoded_frame = rle_encode_frame(&pixels, rows, cols, samples, bits).unwrap();
517        let pixel_data = PixelData::Encapsulated {
518            offset_table: vec![0],
519            fragments: vec![encoded_frame],
520        };
521
522        let codec = GLOBAL_REGISTRY
523            .find(transfer_syntaxes::RLE_LOSSLESS.uid)
524            .unwrap();
525        let decoded = codec
526            .decode(&pixel_data, rows, cols, samples, bits)
527            .unwrap();
528        assert_eq!(&decoded[..16], &pixels[..]);
529    }
530
531    #[test]
532    fn jp2k_codec_multiframe_decode_via_registry() {
533        let rows = 4u16;
534        let cols = 4u16;
535        let samples = 1u8;
536        let bits = 8u8;
537        let frame_a: Vec<u8> = (0u8..16).collect();
538        let frame_b: Vec<u8> = (16u8..32).collect();
539
540        let encoded_a = crate::jp2k::encoder::encode_jp2k(
541            &frame_a,
542            cols as u32,
543            rows as u32,
544            bits,
545            samples,
546            true,
547        )
548        .unwrap();
549        let encoded_b = crate::jp2k::encoder::encode_jp2k(
550            &frame_b,
551            cols as u32,
552            rows as u32,
553            bits,
554            samples,
555            true,
556        )
557        .unwrap();
558
559        let pixel_data = PixelData::Encapsulated {
560            offset_table: vec![],
561            fragments: vec![encoded_a, encoded_b],
562        };
563
564        let codec = GLOBAL_REGISTRY
565            .find(transfer_syntaxes::JPEG_2000_LOSSLESS.uid)
566            .unwrap();
567        let decoded = codec
568            .decode(&pixel_data, rows, cols, samples, bits)
569            .unwrap();
570
571        let mut expected = frame_a;
572        expected.extend_from_slice(&frame_b);
573        assert_eq!(decoded, expected);
574    }
575
576    #[test]
577    fn jpeg_ls_codec_encode_uses_bits_stored_precision() {
578        let rows = 4u16;
579        let cols = 4u16;
580        let samples = 1u8;
581        let bits_allocated = 16u8;
582        let bits_stored = 12u8;
583        let mut pixels = Vec::with_capacity(32);
584        for i in 0u16..16 {
585            pixels.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
586        }
587
588        let codec = GLOBAL_REGISTRY
589            .find(transfer_syntaxes::JPEG_LS_LOSSLESS.uid)
590            .unwrap();
591        let encoded = codec
592            .encode(&pixels, rows, cols, samples, bits_allocated, bits_stored)
593            .unwrap();
594        let fragment = match encoded {
595            PixelData::Encapsulated { fragments, .. } => fragments.into_iter().next().unwrap(),
596            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
597        };
598
599        let decoded = crate::jpeg_ls::decoder::decode_jpeg_ls(&fragment).unwrap();
600        assert_eq!(decoded.bits_per_sample, bits_stored);
601        assert_eq!(decoded.pixels, pixels);
602    }
603
604    #[test]
605    fn jp2k_codec_encode_uses_bits_stored_precision() {
606        let rows = 4u16;
607        let cols = 4u16;
608        let samples = 1u8;
609        let bits_allocated = 16u8;
610        let bits_stored = 12u8;
611        let mut pixels = Vec::with_capacity(32);
612        for i in 0u16..16 {
613            pixels.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
614        }
615
616        let codec = GLOBAL_REGISTRY
617            .find(transfer_syntaxes::JPEG_2000_LOSSLESS.uid)
618            .unwrap();
619        let encoded = codec
620            .encode(&pixels, rows, cols, samples, bits_allocated, bits_stored)
621            .unwrap();
622        let fragment = match encoded {
623            PixelData::Encapsulated { fragments, .. } => fragments.into_iter().next().unwrap(),
624            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
625        };
626
627        let decoded = crate::jp2k::decoder::decode_jp2k(&fragment).unwrap();
628        assert_eq!(decoded.bits_per_sample, bits_stored);
629        assert_eq!(decoded.pixels, pixels);
630    }
631}