1use 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#[derive(Debug, Snafu)]
25pub struct Error(InnerError);
26
27#[derive(Debug, Snafu)]
28pub(crate) enum InnerError {
29 UnknownSrcTransferSyntax { ts: String },
31
32 UnsupportedTransferSyntax,
34
35 UnsupportedTranscoding,
37
38 DecodePixelData { source: crate::Error },
40
41 EncodePixelData {
43 source: dicom_encoding::adapters::EncodeError,
44 },
45
46 UnknownSamplesPerPixel,
48
49 UnsupportedBitsAllocated { bits_allocated: u16 },
51}
52
53pub type Result<T, E = Error> = std::result::Result<T, E>;
55
56pub trait Transcode {
80 fn transcode_with_options(&mut self, ts: &TransferSyntax, options: EncodeOptions)
93 -> Result<()>;
94
95 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 if current_ts_uid == ts.uid() {
123 return Ok(());
124 }
125
126 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 self.meta_mut().set_transfer_syntax(ts);
141 Ok(())
142 }
143 (true, false) => {
144 decode_inline(self, ts)?;
146 Ok(())
147 }
148 (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 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 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 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 self.put(DataElement::new(
199 tags::ENCAPSULATED_PIXEL_DATA_VALUE_TOTAL_LENGTH,
200 VR::UV,
201 PrimitiveValue::from(total_pixeldata_len),
202 ));
203
204 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 self.update_meta(|meta| meta.set_transfer_syntax(ts));
216
217 Ok(())
218 }
219 Err(dicom_encoding::adapters::EncodeError::NotNative) => {
220 return decode_and_encode(self, ts, options);
222 }
223 Err(e) => Err(e),
224 }
225 .context(EncodePixelDataSnafu)?;
226 Ok(())
227 }
228 (_, true) => {
229 decode_and_encode(self, ts, options)
231 }
232 }
233 }
234}
235
236fn 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 let decoded_pixeldata = obj.decode_pixel_data().context(DecodePixelDataSnafu)?;
247 let bits_allocated = decoded_pixeldata.bits_allocated();
248
249 match bits_allocated {
251 8 => {
252 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 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 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 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 obj.put(DataElement::new(
300 tags::PHOTOMETRIC_INTERPRETATION,
301 VR::CS,
302 "RGB",
303 ));
304 }
305
306 obj.update_meta(|meta| meta.set_transfer_syntax(ts));
308
309 Ok(())
310}
311
312fn 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 unreachable!("Unexpected codec from transfer syntax")
330 }
331 };
332
333 decode_inline(obj, &EXPLICIT_VR_LITTLE_ENDIAN)?;
335
336 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 obj.put(DataElement::new(
362 tags::ENCAPSULATED_PIXEL_DATA_VALUE_TOTAL_LENGTH,
363 VR::UV,
364 PrimitiveValue::from(total_pixeldata_len),
365 ));
366
367 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 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 assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_LOSSLESS_SV1);
402
403 obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
405 .expect("Should have transcoded successfully");
406
407 assert_eq!(
409 obj.meta().transfer_syntax(),
410 EXPLICIT_VR_LITTLE_ENDIAN.uid()
411 );
412
413 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 #[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 assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_BASELINE8_BIT);
450
451 assert_eq!(
453 obj.get(tags::PHOTOMETRIC_INTERPRETATION)
454 .unwrap()
455 .to_str()
456 .unwrap(),
457 "YBR_FULL",
458 );
459
460 obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
462 .expect("Should have transcoded successfully");
463
464 assert_eq!(
466 obj.meta().transfer_syntax(),
467 EXPLICIT_VR_LITTLE_ENDIAN.uid()
468 );
469
470 assert_eq!(
473 obj.get(tags::PHOTOMETRIC_INTERPRETATION)
474 .unwrap()
475 .to_str()
476 .unwrap(),
477 "RGB",
478 );
479
480 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 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 assert_eq!(
517 obj.meta().transfer_syntax(),
518 uids::EXPLICIT_VR_LITTLE_ENDIAN
519 );
520
521 obj.transcode(&JPEG_BASELINE.erased())
523 .expect("Should have transcoded successfully");
524
525 assert_eq!(obj.meta().transfer_syntax(), JPEG_BASELINE.uid());
527
528 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 assert_eq!(fragments.len(), 1);
537
538 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 let mut obj = open_file(test_file).unwrap();
548
549 assert_eq!(
551 obj.meta().transfer_syntax(),
552 uids::EXPLICIT_VR_LITTLE_ENDIAN
553 );
554
555 let mut options = EncodeOptions::new();
557 options.quality = Some(50);
559 obj.transcode_with_options(&JPEG_BASELINE.erased(), options)
560 .expect("Should have transcoded successfully");
561
562 assert_eq!(obj.meta().transfer_syntax(), JPEG_BASELINE.uid());
564
565 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 assert_eq!(fragments.len(), 1);
574
575 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 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 #[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 assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_EXTENDED12_BIT);
602
603 obj.transcode(&EXPLICIT_VR_LITTLE_ENDIAN.erased())
605 .expect("Should have transcoded successfully");
606
607 assert_eq!(
609 obj.meta().transfer_syntax(),
610 EXPLICIT_VR_LITTLE_ENDIAN.uid()
611 );
612
613 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 #[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 assert_eq!(
637 obj.meta().transfer_syntax(),
638 uids::EXPLICIT_VR_LITTLE_ENDIAN
639 );
640
641 obj.transcode(&JPEG_BASELINE.erased())
643 .expect("Should have transcoded successfully");
644
645 assert_eq!(obj.meta().transfer_syntax(), JPEG_BASELINE.uid());
647
648 let num_frames = obj.get(tags::NUMBER_OF_FRAMES).unwrap();
650 assert_eq!(num_frames.to_int::<u32>().unwrap(), 2);
651
652 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 assert_eq!(fragments.len(), 2);
661
662 assert!(fragments[0].len() > 4);
664 assert!(fragments[1].len() > 4);
665 }
666
667 #[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 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 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 obj.transcode(&JPEG_EXTENDED.erased())
694 .expect("Should have transcoded successfully");
695
696 assert_eq!(obj.meta().transfer_syntax(), uids::JPEG_EXTENDED12_BIT);
697 let fragments = obj.get(tags::PIXEL_DATA).unwrap().fragments().unwrap();
699 assert_eq!(fragments.len(), 1);
700 }
701 }
702
703 #[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 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 let pixel_data = obj.get(tags::PIXEL_DATA).unwrap();
720 let fragments = pixel_data.fragments().unwrap();
721 assert_eq!(fragments.len(), 2);
722 assert_eq!(fragments[0].len(), 100 * 100 * 3);
724 assert_eq!(fragments[1].len(), 100 * 100 * 3);
725 }
726}