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::{encapsulated_pixel_data_from_frames, 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    /// UID(s) of the transfer syntax(es) this codec can decode.
24    fn decode_transfer_syntax_uids(&self) -> &[&str] {
25        self.transfer_syntax_uids()
26    }
27
28    /// UID(s) of the transfer syntax(es) this codec can encode.
29    fn encode_transfer_syntax_uids(&self) -> &[&str] {
30        self.transfer_syntax_uids()
31    }
32
33    /// Decode all frames in `encapsulated` pixel data.
34    ///
35    /// Returns the raw, uncompressed pixel bytes for all frames concatenated.
36    fn decode(
37        &self,
38        encapsulated: &PixelData,
39        rows: u16,
40        columns: u16,
41        samples_per_pixel: u8,
42        bits_allocated: u8,
43    ) -> DcmResult<Vec<u8>>;
44
45    /// Encode raw pixel bytes into an encapsulated `PixelData`.
46    ///
47    /// `bits_allocated` describes the native storage width of `pixels`.
48    /// `bits_stored` is the actual sample precision to encode into the
49    /// compressed stream and must be `<= bits_allocated`.
50    fn encode(
51        &self,
52        pixels: &[u8],
53        rows: u16,
54        columns: u16,
55        samples_per_pixel: u8,
56        bits_allocated: u8,
57        bits_stored: u8,
58    ) -> DcmResult<PixelData>;
59}
60
61fn validate_stored_bits(codec_name: &str, bits_allocated: u8, bits_stored: u8) -> DcmResult<()> {
62    if bits_stored == 0 {
63        return Err(DcmError::CompressionError {
64            reason: format!("{codec_name}: BitsStored must be at least 1"),
65        });
66    }
67    if bits_stored > bits_allocated {
68        return Err(DcmError::CompressionError {
69            reason: format!(
70                "{codec_name}: BitsStored ({bits_stored}) exceeds BitsAllocated ({bits_allocated})"
71            ),
72        });
73    }
74    Ok(())
75}
76
77// ── Built-in RLE codec ────────────────────────────────────────────────────────
78
79struct RleCodec;
80
81impl ImageCodec for RleCodec {
82    fn transfer_syntax_uids(&self) -> &[&str] {
83        &[transfer_syntaxes::RLE_LOSSLESS.uid]
84    }
85
86    fn decode(
87        &self,
88        pixel_data: &PixelData,
89        rows: u16,
90        columns: u16,
91        samples_per_pixel: u8,
92        bits_allocated: u8,
93    ) -> DcmResult<Vec<u8>> {
94        let fragments = match pixel_data {
95            PixelData::Encapsulated { fragments, .. } => fragments,
96            PixelData::Native { bytes } => return Ok(bytes.clone()),
97        };
98
99        let mut all_frames = Vec::new();
100        for fragment in fragments {
101            let frame = crate::rle::rle_decode_frame(
102                fragment,
103                rows,
104                columns,
105                samples_per_pixel,
106                bits_allocated,
107            )?;
108            all_frames.extend_from_slice(&frame);
109        }
110        Ok(all_frames)
111    }
112
113    fn encode(
114        &self,
115        pixels: &[u8],
116        rows: u16,
117        columns: u16,
118        samples_per_pixel: u8,
119        bits_allocated: u8,
120        _bits_stored: u8,
121    ) -> DcmResult<PixelData> {
122        let encoded =
123            crate::rle::rle_encode_frame(pixels, rows, columns, samples_per_pixel, bits_allocated)?;
124        encapsulated_pixel_data_from_frames(&[encoded])
125    }
126}
127
128// ── Built-in JPEG codec ───────────────────────────────────────────────────────
129
130struct JpegCodec {
131    uids: Vec<&'static str>,
132}
133
134impl JpegCodec {
135    fn baseline() -> Self {
136        Self {
137            uids: vec![
138                transfer_syntaxes::JPEG_BASELINE.uid,
139                transfer_syntaxes::JPEG_EXTENDED.uid,
140            ],
141        }
142    }
143}
144
145struct JpegLosslessCodec;
146
147impl ImageCodec for JpegCodec {
148    fn transfer_syntax_uids(&self) -> &[&str] {
149        &self.uids
150    }
151
152    fn decode(
153        &self,
154        pixel_data: &PixelData,
155        _rows: u16,
156        _columns: u16,
157        _samples_per_pixel: u8,
158        _bits_allocated: u8,
159    ) -> DcmResult<Vec<u8>> {
160        let fragments = match pixel_data {
161            PixelData::Encapsulated { fragments, .. } => fragments,
162            PixelData::Native { bytes } => return Ok(bytes.clone()),
163        };
164
165        let mut all_frames = Vec::new();
166        for fragment in fragments {
167            let frame = crate::jpeg::decoder::decode_jpeg(fragment)?;
168            all_frames.extend_from_slice(&frame.data);
169        }
170        Ok(all_frames)
171    }
172
173    fn encode(
174        &self,
175        pixels: &[u8],
176        rows: u16,
177        columns: u16,
178        samples_per_pixel: u8,
179        _bits_allocated: u8,
180        _bits_stored: u8,
181    ) -> DcmResult<PixelData> {
182        use crate::jpeg::params::JpegParams;
183        let encoded = crate::jpeg::encoder::encode_jpeg(
184            pixels,
185            columns,
186            rows,
187            samples_per_pixel,
188            &JpegParams::default(),
189        )?;
190        encapsulated_pixel_data_from_frames(&[encoded])
191    }
192}
193
194impl ImageCodec for JpegLosslessCodec {
195    fn transfer_syntax_uids(&self) -> &[&str] {
196        &[
197            transfer_syntaxes::JPEG_LOSSLESS.uid,
198            transfer_syntaxes::JPEG_LOSSLESS_SV1.uid,
199        ]
200    }
201
202    fn decode(
203        &self,
204        pixel_data: &PixelData,
205        _rows: u16,
206        _columns: u16,
207        _samples_per_pixel: u8,
208        _bits_allocated: u8,
209    ) -> DcmResult<Vec<u8>> {
210        let fragments = match pixel_data {
211            PixelData::Encapsulated { fragments, .. } => fragments,
212            PixelData::Native { bytes } => return Ok(bytes.clone()),
213        };
214
215        let mut all_frames = Vec::new();
216        for fragment in fragments {
217            let frame = crate::jpeg::decoder::decode_jpeg(fragment)?;
218            all_frames.extend_from_slice(&frame.data);
219        }
220        Ok(all_frames)
221    }
222
223    fn encode(
224        &self,
225        pixels: &[u8],
226        rows: u16,
227        columns: u16,
228        samples_per_pixel: u8,
229        bits_allocated: u8,
230        bits_stored: u8,
231    ) -> DcmResult<PixelData> {
232        let encoded = crate::jpeg::lossless_encoder::encode_jpeg_lossless(
233            pixels,
234            columns,
235            rows,
236            samples_per_pixel,
237            bits_allocated,
238            bits_stored,
239            1,
240        )?;
241        encapsulated_pixel_data_from_frames(&[encoded])
242    }
243}
244
245// ── JPEG-LS codec ─────────────────────────────────────────────────────────────
246
247struct JpegLsCodec;
248
249impl ImageCodec for JpegLsCodec {
250    fn transfer_syntax_uids(&self) -> &[&str] {
251        &[
252            transfer_syntaxes::JPEG_LS_LOSSLESS.uid,
253            transfer_syntaxes::JPEG_LS_LOSSY.uid,
254        ]
255    }
256
257    fn decode(
258        &self,
259        pixel_data: &PixelData,
260        _rows: u16,
261        _columns: u16,
262        _samples_per_pixel: u8,
263        _bits_allocated: u8,
264    ) -> DcmResult<Vec<u8>> {
265        let fragments = match pixel_data {
266            PixelData::Encapsulated { fragments, .. } => fragments,
267            PixelData::Native { bytes } => return Ok(bytes.clone()),
268        };
269        let empty = vec![];
270        let data = fragments.first().unwrap_or(&empty);
271        let decoded = crate::jpeg_ls::decoder::decode_jpeg_ls(data)?;
272        Ok(decoded.pixels)
273    }
274
275    fn encode(
276        &self,
277        pixels: &[u8],
278        rows: u16,
279        columns: u16,
280        samples_per_pixel: u8,
281        bits_allocated: u8,
282        bits_stored: u8,
283    ) -> DcmResult<PixelData> {
284        validate_stored_bits("JPEG-LS", bits_allocated, bits_stored)?;
285        let near = 0; // Lossless by default.
286        let encoded = crate::jpeg_ls::encoder::encode_jpeg_ls(
287            pixels,
288            columns as u32,
289            rows as u32,
290            bits_stored,
291            samples_per_pixel,
292            near,
293        )?;
294        encapsulated_pixel_data_from_frames(&[encoded])
295    }
296}
297
298// ── JPEG 2000 codec ───────────────────────────────────────────────────────────
299
300struct Jp2kDecodeCodec;
301
302struct Jp2kEncodeCodec {
303    transfer_syntax_uid: &'static str,
304    high_throughput: bool,
305    lossless: bool,
306}
307
308const JP2K_DECODE_TRANSFER_SYNTAXES: &[&str] = &[
309    transfer_syntaxes::JPEG_2000_LOSSLESS.uid,
310    transfer_syntaxes::JPEG_2000.uid,
311    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid,
312    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY.uid,
313    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid,
314];
315
316const NO_TRANSFER_SYNTAXES: &[&str] = &[];
317
318impl Jp2kEncodeCodec {
319    const fn new(transfer_syntax_uid: &'static str, high_throughput: bool, lossless: bool) -> Self {
320        Self {
321            transfer_syntax_uid,
322            high_throughput,
323            lossless,
324        }
325    }
326
327    fn codec_name(&self) -> &'static str {
328        if self.high_throughput {
329            "HTJ2K"
330        } else {
331            "JPEG 2000"
332        }
333    }
334}
335
336fn decode_jp2k_pixel_data(pixel_data: &PixelData) -> DcmResult<Vec<u8>> {
337    let fragments = match pixel_data {
338        PixelData::Encapsulated { fragments, .. } => fragments,
339        PixelData::Native { bytes } => return Ok(bytes.clone()),
340    };
341
342    let mut all_pixels = Vec::new();
343    for fragment in fragments {
344        if fragment.is_empty() {
345            continue;
346        }
347        let decoded = crate::jp2k::decoder::decode_jp2k(fragment)?;
348        all_pixels.extend_from_slice(&decoded.pixels);
349    }
350
351    Ok(all_pixels)
352}
353
354fn encode_jp2k_pixel_data(
355    codec: &Jp2kEncodeCodec,
356    pixels: &[u8],
357    rows: u16,
358    columns: u16,
359    samples_per_pixel: u8,
360    bits_allocated: u8,
361    bits_stored: u8,
362) -> DcmResult<PixelData> {
363    validate_stored_bits(codec.codec_name(), bits_allocated, bits_stored)?;
364    let encoded = if codec.high_throughput {
365        crate::jp2k::encoder::encode_htj2k(
366            pixels,
367            columns as u32,
368            rows as u32,
369            bits_stored,
370            samples_per_pixel,
371            codec.lossless,
372        )?
373    } else {
374        crate::jp2k::encoder::encode_jp2k(
375            pixels,
376            columns as u32,
377            rows as u32,
378            bits_stored,
379            samples_per_pixel,
380            codec.lossless,
381        )?
382    };
383
384    encapsulated_pixel_data_from_frames(&[encoded])
385}
386
387impl ImageCodec for Jp2kDecodeCodec {
388    fn transfer_syntax_uids(&self) -> &[&str] {
389        JP2K_DECODE_TRANSFER_SYNTAXES
390    }
391
392    fn encode_transfer_syntax_uids(&self) -> &[&str] {
393        NO_TRANSFER_SYNTAXES
394    }
395
396    fn decode(
397        &self,
398        pixel_data: &PixelData,
399        _rows: u16,
400        _columns: u16,
401        _samples_per_pixel: u8,
402        _bits_allocated: u8,
403    ) -> DcmResult<Vec<u8>> {
404        decode_jp2k_pixel_data(pixel_data)
405    }
406
407    fn encode(
408        &self,
409        _pixels: &[u8],
410        _rows: u16,
411        _columns: u16,
412        _samples_per_pixel: u8,
413        _bits_allocated: u8,
414        _bits_stored: u8,
415    ) -> DcmResult<PixelData> {
416        Err(DcmError::CompressionError {
417            reason: "JPEG 2000 encoding is not available for this transfer syntax".to_string(),
418        })
419    }
420}
421
422impl ImageCodec for Jp2kEncodeCodec {
423    fn transfer_syntax_uids(&self) -> &[&str] {
424        std::slice::from_ref(&self.transfer_syntax_uid)
425    }
426
427    fn decode(
428        &self,
429        pixel_data: &PixelData,
430        _rows: u16,
431        _columns: u16,
432        _samples_per_pixel: u8,
433        _bits_allocated: u8,
434    ) -> DcmResult<Vec<u8>> {
435        decode_jp2k_pixel_data(pixel_data)
436    }
437
438    fn encode(
439        &self,
440        pixels: &[u8],
441        rows: u16,
442        columns: u16,
443        samples_per_pixel: u8,
444        bits_allocated: u8,
445        bits_stored: u8,
446    ) -> DcmResult<PixelData> {
447        encode_jp2k_pixel_data(
448            self,
449            pixels,
450            rows,
451            columns,
452            samples_per_pixel,
453            bits_allocated,
454            bits_stored,
455        )
456    }
457}
458
459// ── CodecRegistry ─────────────────────────────────────────────────────────────
460
461/// Registry of all available image codecs, keyed by transfer syntax UID.
462pub struct CodecRegistry {
463    codecs: RwLock<HashMap<String, Arc<dyn ImageCodec>>>,
464    decoder_codecs: RwLock<HashMap<String, Arc<dyn ImageCodec>>>,
465    encoder_codecs: RwLock<HashMap<String, Arc<dyn ImageCodec>>>,
466}
467
468impl CodecRegistry {
469    /// Create an empty registry.
470    pub fn new() -> Self {
471        Self {
472            codecs: RwLock::new(HashMap::new()),
473            decoder_codecs: RwLock::new(HashMap::new()),
474            encoder_codecs: RwLock::new(HashMap::new()),
475        }
476    }
477
478    /// Register a codec (replaces any existing codec for the same UID).
479    pub fn register(&self, codec: Arc<dyn ImageCodec>) {
480        let decode_uids = codec.decode_transfer_syntax_uids();
481        let encode_uids = codec.encode_transfer_syntax_uids();
482
483        let mut codecs = self.codecs.write().unwrap();
484        let mut decoder_codecs = self.decoder_codecs.write().unwrap();
485        let mut encoder_codecs = self.encoder_codecs.write().unwrap();
486
487        for uid in decode_uids {
488            decoder_codecs.insert(uid.to_string(), Arc::clone(&codec));
489            if encode_uids.contains(uid) {
490                codecs.insert(uid.to_string(), Arc::clone(&codec));
491            }
492        }
493
494        for uid in encode_uids {
495            encoder_codecs.insert(uid.to_string(), Arc::clone(&codec));
496        }
497    }
498
499    /// Look up a codec that supports both decoding and encoding for a transfer
500    /// syntax UID.
501    pub fn find(&self, transfer_syntax_uid: &str) -> Option<Arc<dyn ImageCodec>> {
502        self.codecs
503            .read()
504            .unwrap()
505            .get(transfer_syntax_uid)
506            .cloned()
507    }
508
509    /// Look up a decoder by transfer syntax UID.
510    pub fn find_decoder(&self, transfer_syntax_uid: &str) -> Option<Arc<dyn ImageCodec>> {
511        self.decoder_codecs
512            .read()
513            .unwrap()
514            .get(transfer_syntax_uid)
515            .cloned()
516    }
517
518    /// Look up an encoder by transfer syntax UID.
519    pub fn find_encoder(&self, transfer_syntax_uid: &str) -> Option<Arc<dyn ImageCodec>> {
520        self.encoder_codecs
521            .read()
522            .unwrap()
523            .get(transfer_syntax_uid)
524            .cloned()
525    }
526
527    /// Look up a codec that supports both decoding and encoding or return a
528    /// [`DcmError::NoCodec`] error.
529    pub fn find_required(&self, transfer_syntax_uid: &str) -> DcmResult<Arc<dyn ImageCodec>> {
530        self.find(transfer_syntax_uid)
531            .ok_or_else(|| DcmError::NoCodec {
532                uid: transfer_syntax_uid.to_string(),
533            })
534    }
535
536    /// Look up a decoder or return a [`DcmError::NoCodec`] error.
537    pub fn find_decoder_required(
538        &self,
539        transfer_syntax_uid: &str,
540    ) -> DcmResult<Arc<dyn ImageCodec>> {
541        self.find_decoder(transfer_syntax_uid)
542            .ok_or_else(|| DcmError::NoCodec {
543                uid: transfer_syntax_uid.to_string(),
544            })
545    }
546
547    /// Look up an encoder or return a [`DcmError::NoCodec`] error.
548    pub fn find_encoder_required(
549        &self,
550        transfer_syntax_uid: &str,
551    ) -> DcmResult<Arc<dyn ImageCodec>> {
552        self.find_encoder(transfer_syntax_uid)
553            .ok_or_else(|| DcmError::NoCodec {
554                uid: transfer_syntax_uid.to_string(),
555            })
556    }
557}
558
559impl Default for CodecRegistry {
560    fn default() -> Self {
561        Self::new()
562    }
563}
564
565// ── Global registry ───────────────────────────────────────────────────────────
566
567/// Global codec registry, pre-populated with built-in codecs.
568pub static GLOBAL_REGISTRY: LazyLock<CodecRegistry> = LazyLock::new(|| {
569    let reg = CodecRegistry::new();
570    reg.register(Arc::new(RleCodec));
571    reg.register(Arc::new(JpegCodec::baseline()));
572    reg.register(Arc::new(JpegLosslessCodec));
573    reg.register(Arc::new(JpegLsCodec));
574    reg.register(Arc::new(Jp2kDecodeCodec));
575    reg.register(Arc::new(Jp2kEncodeCodec::new(
576        transfer_syntaxes::JPEG_2000_LOSSLESS.uid,
577        false,
578        true,
579    )));
580    reg.register(Arc::new(Jp2kEncodeCodec::new(
581        transfer_syntaxes::JPEG_2000.uid,
582        false,
583        true,
584    )));
585    reg.register(Arc::new(Jp2kEncodeCodec::new(
586        transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid,
587        true,
588        true,
589    )));
590    // `.203` supports both lossless and lossy streams, but the registry encode
591    // API has no quality selection, so default to a lossless HTJ2K stream here.
592    reg.register(Arc::new(Jp2kEncodeCodec::new(
593        transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid,
594        true,
595        true,
596    )));
597    reg
598});
599
600// ── Flat functional API ───────────────────────────────────────────────────────
601
602/// Transfer syntax UIDs that this crate can decode.
603const SUPPORTED_DECODE_TS: &[&str] = &[
604    transfer_syntaxes::RLE_LOSSLESS.uid,
605    transfer_syntaxes::JPEG_BASELINE.uid,
606    transfer_syntaxes::JPEG_EXTENDED.uid,
607    transfer_syntaxes::JPEG_LOSSLESS.uid,
608    transfer_syntaxes::JPEG_LOSSLESS_SV1.uid,
609    transfer_syntaxes::JPEG_LS_LOSSLESS.uid,
610    transfer_syntaxes::JPEG_LS_LOSSY.uid,
611    transfer_syntaxes::JPEG_2000_LOSSLESS.uid,
612    transfer_syntaxes::JPEG_2000.uid,
613    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid,
614    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY.uid,
615    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid,
616];
617
618/// Transfer syntax UIDs that this crate can encode.
619const SUPPORTED_ENCODE_TS: &[&str] = &[
620    transfer_syntaxes::RLE_LOSSLESS.uid,
621    transfer_syntaxes::JPEG_BASELINE.uid,
622    transfer_syntaxes::JPEG_EXTENDED.uid,
623    transfer_syntaxes::JPEG_LOSSLESS.uid,
624    transfer_syntaxes::JPEG_LOSSLESS_SV1.uid,
625    transfer_syntaxes::JPEG_LS_LOSSLESS.uid,
626    transfer_syntaxes::JPEG_LS_LOSSY.uid,
627    transfer_syntaxes::JPEG_2000_LOSSLESS.uid,
628    transfer_syntaxes::JPEG_2000.uid,
629    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid,
630    transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid,
631];
632
633/// Registered codec information for a transfer syntax.
634#[derive(Debug, Clone, Copy)]
635pub struct CodecInfo {
636    /// Transfer Syntax UID this codec handles.
637    pub transfer_syntax_uid: &'static str,
638    /// Human-readable name.
639    pub name: &'static str,
640}
641
642/// Returns `true` if a decoder is available for the given transfer syntax UID.
643pub fn can_decode(ts_uid: &str) -> bool {
644    SUPPORTED_DECODE_TS.contains(&ts_uid)
645}
646
647/// Returns all transfer syntax UIDs that this crate can decode.
648pub fn supported_decode_transfer_syntaxes() -> &'static [&'static str] {
649    SUPPORTED_DECODE_TS
650}
651
652/// Returns `true` if an encoder is available for the given transfer syntax UID.
653pub fn can_encode(ts_uid: &str) -> bool {
654    SUPPORTED_ENCODE_TS.contains(&ts_uid)
655}
656
657/// Returns all transfer syntax UIDs that this crate can encode.
658pub fn supported_encode_transfer_syntaxes() -> &'static [&'static str] {
659    SUPPORTED_ENCODE_TS
660}
661
662/// Returns all transfer syntax UIDs that this crate can decode.
663pub fn supported_transfer_syntaxes() -> &'static [&'static str] {
664    supported_decode_transfer_syntaxes()
665}
666
667/// Decode a single pixel-data fragment for the given transfer syntax.
668///
669/// `data` must be the raw fragment bytes (RLE header + data, or JPEG bitstream).
670/// Returns the decoded pixel bytes in native little-endian order.
671pub fn decode_pixel_data(
672    ts_uid: &str,
673    data: &[u8],
674    rows: u16,
675    cols: u16,
676    bits_allocated: u16,
677    samples: u16,
678) -> DcmResult<Vec<u8>> {
679    match ts_uid {
680        uid if uid == transfer_syntaxes::RLE_LOSSLESS.uid => {
681            crate::rle::RleCodec::decode(data, rows, cols, bits_allocated, samples)
682        }
683        uid if uid == transfer_syntaxes::JPEG_BASELINE.uid
684            || uid == transfer_syntaxes::JPEG_EXTENDED.uid
685            || uid == transfer_syntaxes::JPEG_LOSSLESS.uid
686            || uid == transfer_syntaxes::JPEG_LOSSLESS_SV1.uid =>
687        {
688            crate::jpeg::JpegDecoder::decode_frame(data).map(|f| f.pixels)
689        }
690        uid if uid == transfer_syntaxes::JPEG_LS_LOSSLESS.uid
691            || uid == transfer_syntaxes::JPEG_LS_LOSSY.uid =>
692        {
693            crate::jpeg_ls::JpegLsCodec::decode_frame(data).map(|f| f.pixels)
694        }
695        uid if uid == transfer_syntaxes::JPEG_2000_LOSSLESS.uid
696            || uid == transfer_syntaxes::JPEG_2000.uid
697            || uid == transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid
698            || uid == transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY.uid
699            || uid == transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid =>
700        {
701            crate::jp2k::Jp2kCodec::decode_frame(data).map(|f| f.pixels)
702        }
703        uid => Err(DcmError::NoCodec {
704            uid: uid.to_string(),
705        }),
706    }
707}
708
709// ── Tests ─────────────────────────────────────────────────────────────────────
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714    use dicom_toolkit_dict::ts::transfer_syntaxes;
715
716    #[test]
717    fn global_registry_has_rle() {
718        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::RLE_LOSSLESS.uid);
719        assert!(codec.is_some());
720    }
721
722    #[test]
723    fn global_registry_has_jpeg_baseline() {
724        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_BASELINE.uid);
725        assert!(codec.is_some());
726    }
727
728    #[test]
729    fn global_registry_has_jpeg_extended() {
730        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_EXTENDED.uid);
731        assert!(codec.is_some());
732    }
733
734    #[test]
735    fn global_registry_has_jpeg_ls() {
736        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_LS_LOSSLESS.uid);
737        assert!(codec.is_some());
738    }
739
740    #[test]
741    fn global_registry_has_jpeg_lossless_encoder() {
742        let codec = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_LOSSLESS.uid);
743        assert!(codec.is_some());
744        let sv1 = GLOBAL_REGISTRY.find(transfer_syntaxes::JPEG_LOSSLESS_SV1.uid);
745        assert!(sv1.is_some());
746    }
747
748    #[test]
749    fn global_registry_has_htj2k_decoder() {
750        let codec = GLOBAL_REGISTRY.find_decoder(transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid);
751        assert!(codec.is_some());
752    }
753
754    #[test]
755    fn global_registry_exposes_htj2k_encoder_variants() {
756        let lossless =
757            GLOBAL_REGISTRY.find(transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid);
758        let generic = GLOBAL_REGISTRY.find(transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid);
759        assert!(lossless.is_some());
760        assert!(generic.is_some());
761    }
762
763    #[test]
764    fn global_registry_keeps_htj2k_rpcl_decode_only() {
765        let codec = GLOBAL_REGISTRY
766            .find(transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY.uid);
767        assert!(codec.is_none());
768    }
769
770    #[test]
771    fn unknown_uid_returns_none() {
772        let codec = GLOBAL_REGISTRY.find("1.2.3.4.5.999");
773        assert!(codec.is_none());
774    }
775
776    #[test]
777    fn find_required_returns_error_for_unknown() {
778        let result = GLOBAL_REGISTRY.find_required("1.9.9.9.9");
779        assert!(matches!(result, Err(DcmError::NoCodec { .. })));
780    }
781
782    // ── Flat API tests ────────────────────────────────────────────────────────
783
784    #[test]
785    fn codec_registry_can_decode_rle() {
786        assert!(can_decode(transfer_syntaxes::RLE_LOSSLESS.uid));
787    }
788
789    #[test]
790    fn codec_registry_can_decode_jpeg_baseline() {
791        assert!(can_decode(transfer_syntaxes::JPEG_BASELINE.uid));
792    }
793
794    #[test]
795    fn codec_registry_can_decode_htj2k() {
796        assert!(can_decode(transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid));
797    }
798
799    #[test]
800    fn codec_registry_cannot_decode_unknown() {
801        assert!(!can_decode("1.2.3.4.5.999"));
802    }
803
804    #[test]
805    fn supported_transfer_syntaxes_is_non_empty() {
806        let list = supported_decode_transfer_syntaxes();
807        assert!(!list.is_empty());
808        assert!(list.contains(&transfer_syntaxes::RLE_LOSSLESS.uid));
809        assert!(list.contains(&transfer_syntaxes::JPEG_BASELINE.uid));
810        assert!(list.contains(&transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid));
811    }
812
813    #[test]
814    fn supported_encode_transfer_syntaxes_excludes_decode_only_codecs() {
815        let list = supported_encode_transfer_syntaxes();
816        assert!(list.contains(&transfer_syntaxes::RLE_LOSSLESS.uid));
817        assert!(list.contains(&transfer_syntaxes::JPEG_LOSSLESS.uid));
818        assert!(list.contains(&transfer_syntaxes::JPEG_LOSSLESS_SV1.uid));
819        assert!(list.contains(&transfer_syntaxes::JPEG_2000_LOSSLESS.uid));
820        assert!(list.contains(&transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000.uid));
821        assert!(
822            !list.contains(&transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY.uid)
823        );
824    }
825
826    #[test]
827    fn can_encode_distinguishes_decode_only_transfer_syntaxes() {
828        assert!(can_encode(transfer_syntaxes::JPEG_BASELINE.uid));
829        assert!(can_encode(transfer_syntaxes::JPEG_LS_LOSSLESS.uid));
830        assert!(can_encode(transfer_syntaxes::JPEG_LOSSLESS.uid));
831        assert!(can_encode(transfer_syntaxes::JPEG_LOSSLESS_SV1.uid));
832        assert!(!can_encode(
833            transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY.uid
834        ));
835    }
836
837    #[test]
838    fn rle_codec_roundtrip_via_registry() {
839        use crate::rle::rle_encode_frame;
840
841        let rows = 4u16;
842        let cols = 4u16;
843        let samples = 1u8;
844        let bits = 8u8;
845        let pixels: Vec<u8> = (0u8..16).collect();
846
847        let encoded_frame = rle_encode_frame(&pixels, rows, cols, samples, bits).unwrap();
848        let pixel_data = PixelData::Encapsulated {
849            offset_table: vec![0],
850            fragments: vec![encoded_frame],
851        };
852
853        let codec = GLOBAL_REGISTRY
854            .find(transfer_syntaxes::RLE_LOSSLESS.uid)
855            .unwrap();
856        let decoded = codec
857            .decode(&pixel_data, rows, cols, samples, bits)
858            .unwrap();
859        assert_eq!(&decoded[..16], &pixels[..]);
860    }
861
862    #[test]
863    fn jp2k_codec_multiframe_decode_via_registry() {
864        let rows = 4u16;
865        let cols = 4u16;
866        let samples = 1u8;
867        let bits = 8u8;
868        let frame_a: Vec<u8> = (0u8..16).collect();
869        let frame_b: Vec<u8> = (16u8..32).collect();
870
871        let encoded_a = crate::jp2k::encoder::encode_jp2k(
872            &frame_a,
873            cols as u32,
874            rows as u32,
875            bits,
876            samples,
877            true,
878        )
879        .unwrap();
880        let encoded_b = crate::jp2k::encoder::encode_jp2k(
881            &frame_b,
882            cols as u32,
883            rows as u32,
884            bits,
885            samples,
886            true,
887        )
888        .unwrap();
889
890        let pixel_data = PixelData::Encapsulated {
891            offset_table: vec![],
892            fragments: vec![encoded_a, encoded_b],
893        };
894
895        let codec = GLOBAL_REGISTRY
896            .find(transfer_syntaxes::JPEG_2000_LOSSLESS.uid)
897            .unwrap();
898        let decoded = codec
899            .decode(&pixel_data, rows, cols, samples, bits)
900            .unwrap();
901
902        let mut expected = frame_a;
903        expected.extend_from_slice(&frame_b);
904        assert_eq!(decoded, expected);
905    }
906
907    #[test]
908    fn jpeg_ls_codec_encode_uses_bits_stored_precision() {
909        let rows = 4u16;
910        let cols = 4u16;
911        let samples = 1u8;
912        let bits_allocated = 16u8;
913        let bits_stored = 12u8;
914        let mut pixels = Vec::with_capacity(32);
915        for i in 0u16..16 {
916            pixels.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
917        }
918
919        let codec = GLOBAL_REGISTRY
920            .find(transfer_syntaxes::JPEG_LS_LOSSLESS.uid)
921            .unwrap();
922        let encoded = codec
923            .encode(&pixels, rows, cols, samples, bits_allocated, bits_stored)
924            .unwrap();
925        let fragment = match encoded {
926            PixelData::Encapsulated { fragments, .. } => fragments.into_iter().next().unwrap(),
927            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
928        };
929
930        let decoded = crate::jpeg_ls::decoder::decode_jpeg_ls(&fragment).unwrap();
931        assert_eq!(decoded.bits_per_sample, bits_stored);
932        assert_eq!(decoded.pixels, pixels);
933    }
934
935    #[test]
936    fn jp2k_codec_encode_uses_bits_stored_precision() {
937        let rows = 4u16;
938        let cols = 4u16;
939        let samples = 1u8;
940        let bits_allocated = 16u8;
941        let bits_stored = 12u8;
942        let mut pixels = Vec::with_capacity(32);
943        for i in 0u16..16 {
944            pixels.extend_from_slice(&((i * 257) & 0x0FFF).to_le_bytes());
945        }
946
947        let codec = GLOBAL_REGISTRY
948            .find(transfer_syntaxes::JPEG_2000_LOSSLESS.uid)
949            .unwrap();
950        let encoded = codec
951            .encode(&pixels, rows, cols, samples, bits_allocated, bits_stored)
952            .unwrap();
953        let fragment = match encoded {
954            PixelData::Encapsulated { fragments, .. } => fragments.into_iter().next().unwrap(),
955            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
956        };
957
958        let decoded = crate::jp2k::decoder::decode_jp2k(&fragment).unwrap();
959        assert_eq!(decoded.bits_per_sample, bits_stored);
960        assert_eq!(decoded.pixels, pixels);
961    }
962
963    #[test]
964    fn htj2k_codec_encode_uses_bits_stored_precision() {
965        let rows = 4u16;
966        let cols = 4u16;
967        let samples = 1u8;
968        let bits_allocated = 16u8;
969        let bits_stored = 12u8;
970        let mut pixels = Vec::with_capacity(32);
971        for _ in 0..16 {
972            pixels.extend_from_slice(&2048u16.to_le_bytes());
973        }
974
975        let codec = GLOBAL_REGISTRY
976            .find(transfer_syntaxes::HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY.uid)
977            .unwrap();
978        let encoded = codec
979            .encode(&pixels, rows, cols, samples, bits_allocated, bits_stored)
980            .unwrap();
981        let fragment = match encoded {
982            PixelData::Encapsulated { fragments, .. } => fragments.into_iter().next().unwrap(),
983            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
984        };
985
986        assert!(fragment.windows(2).any(|window| window == [0xFF, 0x50]));
987        let decoded = crate::jp2k::decoder::decode_jp2k(&fragment).unwrap();
988        assert_eq!(decoded.bits_per_sample, bits_stored);
989        assert_eq!(decoded.pixels, pixels);
990    }
991}