dicom_pixeldata/
transcode.rs

1//! DICOM Transcoder API
2//!
3//! This module collects the pixel data decoding and encoding capabilities
4//! of `dicom_encoding` and `dicom_pixeldata`
5//! to offer a convenient API for converting DICOM objects
6//! to different transfer syntaxes.
7//!
8//! See the [`Transcode`] trait for more information.
9use std::borrow::Cow;
10
11use dicom_core::{
12    ops::ApplyOp, value::PixelFragmentSequence, DataDictionary, DataElement, Length,
13    PrimitiveValue, VR,
14};
15use dicom_dictionary_std::{tags, uids};
16use dicom_encoding::{adapters::EncodeOptions, Codec, TransferSyntax, TransferSyntaxIndex};
17use dicom_object::{FileDicomObject, InMemDicomObject};
18use dicom_transfer_syntax_registry::{entries::EXPLICIT_VR_LITTLE_ENDIAN, TransferSyntaxRegistry};
19use snafu::{OptionExt, ResultExt, Snafu};
20
21use crate::PixelDecoder;
22
23/// An error occurred during the object transcoding process.
24#[derive(Debug, Snafu)]
25pub struct Error(InnerError);
26
27#[derive(Debug, Snafu)]
28pub(crate) enum InnerError {
29    /// Unrecognized transfer syntax of receiving object ({ts})
30    UnknownSrcTransferSyntax { ts: String },
31
32    /// Unsupported target transfer syntax
33    UnsupportedTransferSyntax,
34
35    /// Unsupported transcoding capability
36    UnsupportedTranscoding,
37
38    /// Could not decode pixel data of receiving object  
39    DecodePixelData { source: crate::Error },
40
41    /// Could not encode pixel data to target encoding
42    EncodePixelData {
43        source: dicom_encoding::adapters::EncodeError,
44    },
45
46    /// Number of samples per pixel unknown
47    UnknownSamplesPerPixel,
48
49    /// Unsupported bits per sample ({bits_allocated})
50    UnsupportedBitsAllocated { bits_allocated: u16 },
51}
52
53/// Alias for the result of transcoding a DICOM object.
54pub type Result<T, E = Error> = std::result::Result<T, E>;
55
56/// Interface for transcoding a DICOM object's pixel data
57/// to comply with a different transfer syntax.
58/// Can be implemented by in-memory DICOM object representations
59/// as well as partial or lazy DICOM object readers,
60/// so that transcoding can be performed without loading the entire object.
61///
62/// # Example
63///
64/// A typical [file DICOM object in memory](FileDicomObject)
65/// can be transcoded inline using [`transcode`](Transcode::transcode).
66///
67/// ```no_run
68/// # use dicom_object::open_file;
69/// use dicom_pixeldata::Transcode as _;
70///
71/// let mut obj = dicom_object::open_file("image.dcm").unwrap();
72/// // convert to JPEG
73/// obj.transcode(&dicom_transfer_syntax_registry::entries::JPEG_BASELINE.erased())?;
74///
75/// // save transcoded version to file
76/// obj.write_to_file("image_jpg.dcm")?;
77/// # Ok::<(), Box<dyn std::error::Error>>(())
78/// ```
79pub trait Transcode {
80    /// Convert the receiving object's transfer syntax
81    /// to the one specified in `ts` according to the given encoding options.
82    ///
83    /// This method may replace one or more attributes accordingly,
84    /// including the meta group specifying the transfer syntax.
85    /// The encoding options only apply if the pixel data needs to be re-encoded.
86    ///
87    /// If the receiving object's pixel data is encapsulated,
88    /// the object might be first decoded into native pixel data.
89    /// In case of an encoding error,
90    /// the object may be left in an intermediate state,
91    /// which should not be assumed to be consistent.
92    fn transcode_with_options(&mut self, ts: &TransferSyntax, options: EncodeOptions)
93        -> Result<()>;
94
95    /// Convert the receiving object's transfer syntax
96    /// to the one specified in `ts`.
97    ///
98    /// This method may replace one or more attributes accordingly,
99    /// including the meta group specifying the transfer syntax.
100    ///
101    /// If the receiving object's pixel data is encapsulated,
102    /// the object might be first decoded into native pixel data.
103    /// In case of an encoding error,
104    /// the object may be left in an intermediate state,
105    /// which should not be assumed to be consistent.
106    fn transcode(&mut self, ts: &TransferSyntax) -> Result<()> {
107        self.transcode_with_options(ts, EncodeOptions::default())
108    }
109}
110
111impl<D> Transcode for FileDicomObject<InMemDicomObject<D>>
112where
113    D: Clone + DataDictionary,
114{
115    fn transcode_with_options(
116        &mut self,
117        ts: &TransferSyntax,
118        options: EncodeOptions,
119    ) -> Result<()> {
120        let current_ts_uid = self.meta().transfer_syntax();
121        // do nothing if the transfer syntax already matches
122        if current_ts_uid == ts.uid() {
123            return Ok(());
124        }
125
126        // inspect current object TS
127        let current_ts = TransferSyntaxRegistry
128            .get(current_ts_uid)
129            .with_context(|| UnknownSrcTransferSyntaxSnafu {
130                ts: current_ts_uid.to_string(),
131            })?;
132
133        match (
134            current_ts.is_encapsulated_pixel_data(),
135            ts.is_encapsulated_pixel_data(),
136        ) {
137            (false, false) => {
138                // no pixel data conversion is necessary:
139                // change transfer syntax and return
140                self.meta_mut().set_transfer_syntax(ts);
141                Ok(())
142            }
143            (true, false) => {
144                // decode pixel data
145                decode_inline(self, ts)?;
146                Ok(())
147            }
148            // make some exceptions for transfer syntaxes
149            // which are best transcoded from encapsulated pixel data:
150            // - JPEG baseline -> JPEG XL * (can recompress JPEG)
151            // - JPEG XL recompression -> JPEG baseline (can do lossless conversion)
152            (true, true)
153                if (current_ts.uid() == uids::JPEG_BASELINE8_BIT
154                    && (ts.uid() == uids::JPEGXLJPEG_RECOMPRESSION
155                        || ts.uid() == uids::JPEGXL
156                        || ts.uid() == uids::JPEGXL_LOSSLESS))
157                    || (current_ts.uid() == uids::JPEGXLJPEG_RECOMPRESSION
158                        && ts.uid() == uids::JPEG_BASELINE8_BIT) =>
159            {
160                // start by assuming that the codec can work with it as is
161                let writer = match ts.codec() {
162                    Codec::EncapsulatedPixelData(_, Some(writer)) => writer,
163                    Codec::EncapsulatedPixelData(..) => {
164                        return UnsupportedTransferSyntaxSnafu.fail()?
165                    }
166                    Codec::Dataset(None) => return UnsupportedTransferSyntaxSnafu.fail()?,
167                    Codec::Dataset(Some(_)) => return UnsupportedTranscodingSnafu.fail()?,
168                    Codec::None => {
169                        // already tested in `is_encapsulated_pixel_data`
170                        unreachable!("Unexpected codec from transfer syntax")
171                    }
172                };
173
174                let mut offset_table = Vec::new();
175                let mut fragments = Vec::new();
176
177                match writer.encode(&*self, options.clone(), &mut fragments, &mut offset_table) {
178                    Ok(ops) => {
179                        // success!
180                        let num_frames = offset_table.len();
181                        let total_pixeldata_len: u64 =
182                            fragments.iter().map(|f| f.len() as u64).sum();
183
184                        self.put(DataElement::new_with_len(
185                            tags::PIXEL_DATA,
186                            VR::OB,
187                            Length::UNDEFINED,
188                            PixelFragmentSequence::new(offset_table, fragments),
189                        ));
190
191                        self.put(DataElement::new(
192                            tags::NUMBER_OF_FRAMES,
193                            VR::IS,
194                            num_frames.to_string(),
195                        ));
196
197                        // provide Encapsulated Pixel Data Value Total Length
198                        self.put(DataElement::new(
199                            tags::ENCAPSULATED_PIXEL_DATA_VALUE_TOTAL_LENGTH,
200                            VR::UV,
201                            PrimitiveValue::from(total_pixeldata_len),
202                        ));
203
204                        // try to apply operations
205                        for (n, op) in ops.into_iter().enumerate() {
206                            match self.apply(op) {
207                                Ok(_) => (),
208                                Err(e) => {
209                                    tracing::warn!("Could not apply transcoding step #{}: {}", n, e)
210                                }
211                            }
212                        }
213
214                        // change transfer syntax
215                        self.update_meta(|meta| meta.set_transfer_syntax(ts));
216
217                        Ok(())
218                    }
219                    Err(dicom_encoding::adapters::EncodeError::NotNative) => {
220                        // not supported after all, fall back
221                        return decode_and_encode(self, ts, options);
222                    }
223                    Err(e) => Err(e),
224                }
225                .context(EncodePixelDataSnafu)?;
226                Ok(())
227            }
228            (_, true) => {
229                // must decode then encode
230                decode_and_encode(self, ts, options)
231            }
232        }
233    }
234}
235
236/// decode and override pixel data to native form
237/// (`ts` must be a native pixel data transfer syntax)
238fn decode_inline<D, T, U, V>(
239    obj: &mut FileDicomObject<InMemDicomObject<D>>,
240    ts: &TransferSyntax<T, U, V>,
241) -> Result<()>
242where
243    D: Clone + DataDictionary,
244{
245    // decode pixel data
246    let decoded_pixeldata = obj.decode_pixel_data().context(DecodePixelDataSnafu)?;
247    let bits_allocated = decoded_pixeldata.bits_allocated();
248
249    // apply change to pixel data attribute
250    match bits_allocated {
251        8 => {
252            // 8-bit samples
253            let pixels = decoded_pixeldata.data().to_vec();
254            obj.put(DataElement::new_with_len(
255                tags::PIXEL_DATA,
256                VR::OW,
257                Length::defined(pixels.len() as u32),
258                PrimitiveValue::from(pixels),
259            ));
260        }
261        16 => {
262            // 16-bit samples
263            let pixels = decoded_pixeldata.data_ow();
264            obj.put(DataElement::new_with_len(
265                tags::PIXEL_DATA,
266                VR::OW,
267                Length::defined(pixels.len() as u32 * 2),
268                PrimitiveValue::U16(pixels.into()),
269            ));
270        }
271        _ => return UnsupportedBitsAllocatedSnafu { bits_allocated }.fail()?,
272    };
273
274    // correct photometric interpretation if necessary
275    let samples_per_pixel: u16 = obj
276        .get(tags::SAMPLES_PER_PIXEL)
277        .context(UnknownSamplesPerPixelSnafu)?
278        .to_int()
279        .ok()
280        .context(UnknownSamplesPerPixelSnafu)?;
281
282    if samples_per_pixel == 1 {
283        let pmi = obj
284            .get(tags::PHOTOMETRIC_INTERPRETATION)
285            .and_then(|e| e.to_str().ok());
286
287        // set Photometric Interpretation to Monochrome2
288        // if it was neither of the expected monochromes
289        if pmi != Some(Cow::from("MONOCHROME1")) && pmi != Some(Cow::from("MONOCHROME2")) {
290            obj.put(DataElement::new(
291                tags::PHOTOMETRIC_INTERPRETATION,
292                VR::CS,
293                "MONOCHROME2",
294            ));
295        }
296    } else if samples_per_pixel == 3 {
297        // force Photometric Interpretation to RGB
298        // as mandated by the pixel data reading interface
299        obj.put(DataElement::new(
300            tags::PHOTOMETRIC_INTERPRETATION,
301            VR::CS,
302            "RGB",
303        ));
304    }
305
306    // change transfer syntax to Explicit VR little endian
307    obj.update_meta(|meta| meta.set_transfer_syntax(ts));
308
309    Ok(())
310}
311
312/// the impl of transcoding which decodes encapsulated pixel data to native
313/// and then encodes it to the target transfer syntax
314fn decode_and_encode<D>(
315    obj: &mut FileDicomObject<InMemDicomObject<D>>,
316    ts: &TransferSyntax,
317    options: EncodeOptions,
318) -> Result<()>
319where
320    D: Clone + DataDictionary,
321{
322    let writer = match ts.codec() {
323        Codec::EncapsulatedPixelData(_, Some(writer)) => writer,
324        Codec::EncapsulatedPixelData(..) => return UnsupportedTransferSyntaxSnafu.fail()?,
325        Codec::Dataset(None) => return UnsupportedTransferSyntaxSnafu.fail()?,
326        Codec::Dataset(Some(_)) => return UnsupportedTranscodingSnafu.fail()?,
327        Codec::None => {
328            // already tested in `is_codec_free`
329            unreachable!("Unexpected codec from transfer syntax")
330        }
331    };
332
333    // decode pixel data
334    decode_inline(obj, &EXPLICIT_VR_LITTLE_ENDIAN)?;
335
336    // use pixel data writer API
337    let mut offset_table = Vec::new();
338    let mut fragments = Vec::new();
339
340    let ops = writer
341        .encode(&*obj, options, &mut fragments, &mut offset_table)
342        .context(EncodePixelDataSnafu)?;
343
344    let num_frames = offset_table.len();
345    let total_pixeldata_len: u64 = fragments.iter().map(|f| f.len() as u64).sum();
346
347    obj.put(DataElement::new_with_len(
348        tags::PIXEL_DATA,
349        VR::OB,
350        Length::UNDEFINED,
351        PixelFragmentSequence::new(offset_table, fragments),
352    ));
353
354    obj.put(DataElement::new(
355        tags::NUMBER_OF_FRAMES,
356        VR::IS,
357        num_frames.to_string(),
358    ));
359
360    // provide Encapsulated Pixel Data Value Total Length
361    obj.put(DataElement::new(
362        tags::ENCAPSULATED_PIXEL_DATA_VALUE_TOTAL_LENGTH,
363        VR::UV,
364        PrimitiveValue::from(total_pixeldata_len),
365    ));
366
367    // try to apply operations
368    for (n, op) in ops.into_iter().enumerate() {
369        match obj.apply(op) {
370            Ok(_) => (),
371            Err(e) => {
372                tracing::warn!("Could not apply transcoding step #{}: {}", n, e)
373            }
374        }
375    }
376
377    // change transfer syntax
378    obj.update_meta(|meta| meta.set_transfer_syntax(ts));
379
380    Ok(())
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use dicom_dictionary_std::uids;
387    use dicom_object::open_file;
388    #[cfg(feature = "native")]
389    use dicom_transfer_syntax_registry::entries::JPEG_BASELINE;
390    use dicom_transfer_syntax_registry::entries::{
391        ENCAPSULATED_UNCOMPRESSED_EXPLICIT_VR_LITTLE_ENDIAN, JPEG_EXTENDED,
392    };
393
394    #[cfg(feature = "native")]
395    #[test]
396    fn test_transcode_from_jpeg_lossless_to_native_rgb() {
397        let test_file = dicom_test_files::path("pydicom/SC_rgb_jpeg_gdcm.dcm").unwrap();
398        let mut obj = open_file(test_file).unwrap();
399
400        // pre-condition check: pixel data conversion is needed here
401        assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_LOSSLESS_SV1);
402
403        // transcode to explicit VR little endian
404        obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
405            .expect("Should have transcoded successfully");
406
407        // check transfer syntax
408        assert_eq!(
409            obj.meta().transfer_syntax(),
410            EXPLICIT_VR_LITTLE_ENDIAN.uid()
411        );
412
413        // check that the pixel data is in its native form
414        // and has the expected size
415        let pixel_data = obj.element(tags::PIXEL_DATA).unwrap();
416        let pixels = pixel_data
417            .to_bytes()
418            .expect("Pixel Data should be in bytes");
419
420        let rows = 100;
421        let cols = 100;
422        let spp = 3;
423
424        assert_eq!(pixels.len(), rows * cols * spp);
425    }
426
427    #[cfg(any(feature = "jpeg", feature = "gdcm"))]
428    fn assert_sample_eq_approx(description: &str, found: u8, expected: u8) {
429        const ERROR_MARGIN: u8 = 8;
430
431        assert!(
432            found.abs_diff(expected) < ERROR_MARGIN,
433            "mismatch in sample {}: {} vs {}",
434            description,
435            found,
436            expected
437        );
438    }
439
440    /// !!! #674 Decoding via GDCM is buggy
441    #[cfg(any(feature = "jpeg", feature = "gdcm"))]
442    #[cfg_attr(feature = "gdcm", ignore)]
443    #[test]
444    fn transcode_from_jpeg_baseline_to_native_rgb() {
445        let test_file = dicom_test_files::path("pydicom/SC_rgb_jpeg_dcmtk.dcm").unwrap();
446        let mut obj = open_file(test_file).unwrap();
447
448        // pre-condition check: pixel data conversion is needed here
449        assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_BASELINE8_BIT);
450
451        // pre-condition check: photometric interpretation was YBR_FULL
452        assert_eq!(
453            obj.get(tags::PHOTOMETRIC_INTERPRETATION)
454                .unwrap()
455                .to_str()
456                .unwrap(),
457            "YBR_FULL",
458        );
459
460        // transcode to explicit VR little endian
461        obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
462            .expect("Should have transcoded successfully");
463
464        // check transfer syntax
465        assert_eq!(
466            obj.meta().transfer_syntax(),
467            EXPLICIT_VR_LITTLE_ENDIAN.uid()
468        );
469
470        // check that the photometric interpretation
471        // was updated (no longer YBR_FULL)
472        assert_eq!(
473            obj.get(tags::PHOTOMETRIC_INTERPRETATION)
474                .unwrap()
475                .to_str()
476                .unwrap(),
477            "RGB",
478        );
479
480        // check that the pixel data is in its native form
481        // and has the expected size
482        let pixel_data = obj.element(tags::PIXEL_DATA).unwrap();
483        let pixels = pixel_data
484            .to_bytes()
485            .expect("Pixel Data should be in bytes");
486
487        let rows = 100;
488        let cols = 100;
489        let spp = 3;
490
491        assert_eq!(pixels.len(), rows * cols * spp);
492
493        // poke a few pixels
494
495        assert_sample_eq_approx("R0", pixels[0], 255);
496        assert_sample_eq_approx("G0", pixels[1], 0);
497        assert_sample_eq_approx("B0", pixels[2], 0);
498
499        let y = 54;
500        assert_sample_eq_approx("R5400", pixels[cols * spp * y + 0], 128);
501        assert_sample_eq_approx("G5400", pixels[cols * spp * y + 1], 128);
502        assert_sample_eq_approx("G5400", pixels[cols * spp * y + 2], 255);
503
504        assert_sample_eq_approx("R5450", pixels[cols * spp * y + 150], 128);
505        assert_sample_eq_approx("R5450", pixels[cols * spp * y + 151], 128);
506        assert_sample_eq_approx("R5450", pixels[cols * spp * y + 152], 255);
507    }
508
509    #[cfg(feature = "native")]
510    #[test]
511    fn test_transcode_from_native_to_jpeg_rgb() {
512        let test_file = dicom_test_files::path("pydicom/SC_rgb.dcm").unwrap();
513        let mut obj = open_file(&test_file).unwrap();
514
515        // pre-condition check: pixel data is native
516        assert_eq!(
517            obj.meta().transfer_syntax(),
518            uids::EXPLICIT_VR_LITTLE_ENDIAN
519        );
520
521        // transcode to JPEG baseline
522        obj.transcode(&JPEG_BASELINE.erased())
523            .expect("Should have transcoded successfully");
524
525        // check transfer syntax
526        assert_eq!(obj.meta().transfer_syntax(), JPEG_BASELINE.uid());
527
528        // check that the pixel data is encapsulated
529        // and has the expected number of fragments
530        let pixel_data = obj.get(tags::PIXEL_DATA).unwrap();
531        let fragments = pixel_data
532            .fragments()
533            .expect("Pixel Data should be in encapsulated fragments");
534
535        // one frame, one fragment (as required by JPEG baseline)
536        assert_eq!(fragments.len(), 1);
537
538        // check that the fragment data is in valid JPEG (magic code)
539        let fragment = &fragments[0];
540        assert!(fragment.len() > 4);
541        assert_eq!(&fragment[0..2], &[0xFF, 0xD8]);
542
543        let size_1 = fragment.len();
544
545        // re-encode with different options
546
547        let mut obj = open_file(test_file).unwrap();
548
549        // pre-condition check: pixel data is native
550        assert_eq!(
551            obj.meta().transfer_syntax(),
552            uids::EXPLICIT_VR_LITTLE_ENDIAN
553        );
554
555        // transcode to JPEG baseline
556        let mut options = EncodeOptions::new();
557        // low quality
558        options.quality = Some(50);
559        obj.transcode_with_options(&JPEG_BASELINE.erased(), options)
560            .expect("Should have transcoded successfully");
561
562        // check transfer syntax
563        assert_eq!(obj.meta().transfer_syntax(), JPEG_BASELINE.uid());
564
565        // check that the pixel data is encapsulated
566        // and has the expected number of fragments
567        let pixel_data = obj.get(tags::PIXEL_DATA).unwrap();
568        let fragments = pixel_data
569            .fragments()
570            .expect("Pixel Data should be in encapsulated fragments");
571
572        // one frame, one fragment (as required by JPEG baseline)
573        assert_eq!(fragments.len(), 1);
574
575        // check that the fragment data is in valid JPEG (magic code)
576        let fragment = &fragments[0];
577        assert!(fragment.len() > 4);
578        assert_eq!(&fragment[0..2], &[0xFF, 0xD8]);
579
580        let size_2 = fragment.len();
581
582        // the size of the second fragment should be smaller
583        // due to lower quality
584        assert!(
585            size_2 < size_1,
586            "expected smaller size for lower quality, but {} => {}",
587            size_2,
588            size_1
589        );
590    }
591
592    #[cfg(feature = "native")]
593    #[test]
594    // Note: Test ignored until 12-bit JPEG decoding is supported
595    #[ignore]
596    fn test_transcode_from_jpeg_to_native_16bit() {
597        let test_file = dicom_test_files::path("pydicom/JPEG-lossy.dcm").unwrap();
598        let mut obj = open_file(test_file).unwrap();
599
600        // pre-condition check: pixel data conversion is needed here
601        assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_EXTENDED12_BIT);
602
603        // transcode to explicit VR little endian
604        obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
605            .expect("Should have transcoded successfully");
606
607        // check transfer syntax
608        assert_eq!(
609            obj.meta().transfer_syntax(),
610            EXPLICIT_VR_LITTLE_ENDIAN.uid()
611        );
612
613        // check that the pixel data is in its native form
614        // and has the expected size
615        let pixel_data = obj.element(tags::PIXEL_DATA).unwrap();
616        let pixels = pixel_data
617            .to_bytes()
618            .expect("Pixel Data should be in bytes");
619
620        let rows = 1024;
621        let cols = 256;
622        let spp = 3;
623        let bps = 2;
624
625        assert_eq!(pixels.len(), rows * cols * spp * bps);
626    }
627
628    /// can transcode native multi-frame pixel data
629    #[cfg(feature = "native")]
630    #[test]
631    fn test_transcode_2frames_to_jpeg() {
632        let test_file = dicom_test_files::path("pydicom/SC_rgb_2frame.dcm").unwrap();
633        let mut obj = open_file(test_file).unwrap();
634
635        // pre-condition check: pixel data conversion is needed here
636        assert_eq!(
637            obj.meta().transfer_syntax(),
638            uids::EXPLICIT_VR_LITTLE_ENDIAN
639        );
640
641        // transcode to JPEG baseline
642        obj.transcode(&JPEG_BASELINE.erased())
643            .expect("Should have transcoded successfully");
644
645        // check transfer syntax
646        assert_eq!(obj.meta().transfer_syntax(), JPEG_BASELINE.uid());
647
648        // check that the number of frames stayed the same
649        let num_frames = obj.get(tags::NUMBER_OF_FRAMES).unwrap();
650        assert_eq!(num_frames.to_int::<u32>().unwrap(), 2);
651
652        // check that the pixel data is encapsulated
653        let pixel_data = obj.element(tags::PIXEL_DATA).unwrap();
654
655        let fragments = pixel_data
656            .fragments()
657            .expect("Pixel Data should be in encapsulated fragments");
658
659        // two frames, two fragments (as required by JPEG baseline)
660        assert_eq!(fragments.len(), 2);
661
662        // each frame has some data
663        assert!(fragments[0].len() > 4);
664        assert!(fragments[1].len() > 4);
665    }
666
667    /// if the transfer syntax is the same, no transcoding should be performed
668    #[test]
669    fn test_no_transcoding_needed() {
670        {
671            let test_file = dicom_test_files::path("pydicom/SC_rgb.dcm").unwrap();
672            let mut obj = open_file(test_file).unwrap();
673
674            // transcode to the same TS
675            obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
676                .expect("Should have transcoded successfully");
677
678            assert_eq!(
679                obj.meta().transfer_syntax(),
680                EXPLICIT_VR_LITTLE_ENDIAN.uid()
681            );
682            // pixel data is still native
683            let pixel_data = obj.get(tags::PIXEL_DATA).unwrap().to_bytes().unwrap();
684            assert_eq!(pixel_data.len(), 100 * 100 * 3);
685        }
686        {
687            let test_file = dicom_test_files::path("pydicom/JPEG-lossy.dcm").unwrap();
688            let mut obj = open_file(test_file).unwrap();
689
690            assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_EXTENDED12_BIT);
691
692            // transcode to the same TS
693            obj.transcode(&JPEG_EXTENDED.erased())
694                .expect("Should have transcoded successfully");
695
696            assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_EXTENDED12_BIT);
697            // pixel data is still encapsulated
698            let fragments = obj.get(tags::PIXEL_DATA).unwrap().fragments().unwrap();
699            assert_eq!(fragments.len(), 1);
700        }
701    }
702
703    /// converting to Encapsulated Uncompressed Explicit VR Little Endian
704    /// should split each frame into separate fragments in native form
705    #[test]
706    fn test_transcode_encapsulated_uncompressed() {
707        let test_file = dicom_test_files::path("pydicom/SC_rgb_2frame.dcm").unwrap();
708        let mut obj = open_file(test_file).unwrap();
709
710        // transcode to the same TS
711        obj.transcode(&ENCAPSULATED_UNCOMPRESSED_EXPLICIT_VR_LITTLE_ENDIAN.erased())
712            .expect("Should have transcoded successfully");
713
714        assert_eq!(
715            obj.meta().transfer_syntax(),
716            ENCAPSULATED_UNCOMPRESSED_EXPLICIT_VR_LITTLE_ENDIAN.uid()
717        );
718        // pixel data is encapsulated, but in native form
719        let pixel_data = obj.get(tags::PIXEL_DATA).unwrap();
720        let fragments = pixel_data.fragments().unwrap();
721        assert_eq!(fragments.len(), 2);
722        // each frame should have native pixel data (100x100 RGB)
723        assert_eq!(fragments[0].len(), 100 * 100 * 3);
724        assert_eq!(fragments[1].len(), 100 * 100 * 3);
725    }
726}