diff --git a/Cargo.lock b/Cargo.lock index aa30e13f..18c5d7d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -655,7 +655,9 @@ dependencies = [ "charls", "dicom-core", "dicom-encoding", + "dicom-object", "dicom-test-files", + "flate2", "jpeg-decoder", "jpeg-encoder", "jpeg2k", diff --git a/encoding/src/transfer_syntax/mod.rs b/encoding/src/transfer_syntax/mod.rs index 570c6cfd..e7ed5343 100644 --- a/encoding/src/transfer_syntax/mod.rs +++ b/encoding/src/transfer_syntax/mod.rs @@ -319,57 +319,29 @@ pub enum Codec { pub type AdapterFreeTransferSyntax = TransferSyntax; -/// An adapter of byte read and write streams. -pub trait DataRWAdapter { - /// The type of the adapted reader. - type Reader: Read; - /// The type of the adapted writer. - type Writer: Write; - +/// A fully dynamic adapter of byte read and write streams. +pub trait DataRWAdapter { /// Adapt a byte reader. - fn adapt_reader(&self, reader: R) -> Self::Reader - where - R: Read; + fn adapt_reader<'r>(&self, reader: Box) -> Box; /// Adapt a byte writer. - fn adapt_writer(&self, writer: W) -> Self::Writer - where - W: Write; + fn adapt_writer<'w>(&self, writer: Box) -> Box; } /// Alias type for a dynamically dispatched data adapter. -pub type DynDataRWAdapter = Box< - dyn DataRWAdapter< - Box, - Box, - Reader = Box, - Writer = Box, - > + Send - + Sync, ->; - -impl DataRWAdapter for &'_ T +pub type DynDataRWAdapter = Box; + +impl DataRWAdapter for &'_ T where - T: DataRWAdapter, - R: Read, - W: Write, + T: DataRWAdapter, { - type Reader = >::Reader; - type Writer = >::Writer; - /// Adapt a byte reader. - fn adapt_reader(&self, reader: R) -> Self::Reader - where - R: Read, - { + fn adapt_reader<'r>(&self, reader: Box) -> Box { (**self).adapt_reader(reader) } /// Adapt a byte writer. - fn adapt_writer(&self, writer: W) -> Self::Writer - where - W: Write, - { + fn adapt_writer<'w>(&self, writer: Box) -> Box { (**self).adapt_writer(writer) } } @@ -384,21 +356,13 @@ where #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum NeverAdapter {} -impl DataRWAdapter for NeverAdapter { - type Reader = Box; - type Writer = Box; +impl DataRWAdapter for NeverAdapter { - fn adapt_reader(&self, _reader: R) -> Self::Reader - where - R: Read, - { + fn adapt_reader<'r>(&self, _reader: Box) -> Box { unreachable!() } - fn adapt_writer(&self, _writer: W) -> Self::Writer - where - W: Write, - { + fn adapt_writer<'w>(&self, _writer: Box) -> Box { unreachable!() } } @@ -531,6 +495,13 @@ impl TransferSyntax { matches!(self.codec, Codec::Dataset(None)) } + /// Check whether this transfer syntax expects pixel data to be encapsulated. + /// + /// This does not imply that the pixel data can be decoded. + pub fn is_encapsulated_pixel_data(&self) -> bool { + matches!(self.codec, Codec::EncapsulatedPixelData(..)) + } + /// Check whether reading and writing the pixel data is unsupported. /// If this is `true`, encoding and decoding of the data set may still /// be possible, but the pixel data will only be available in its @@ -628,12 +599,7 @@ impl TransferSyntax { pub fn erased(self) -> TransferSyntax where D: Send + Sync + 'static, - D: DataRWAdapter< - Box, - Box, - Reader = Box, - Writer = Box, - >, + D: DataRWAdapter, R: Send + Sync + 'static, R: PixelDataReader, W: Send + Sync + 'static, diff --git a/object/src/file.rs b/object/src/file.rs index 9748d653..e8e986fd 100644 --- a/object/src/file.rs +++ b/object/src/file.rs @@ -18,7 +18,7 @@ pub type Result = std::result::Result; /// preamble: file meta group, followed by the rest of the data set. pub fn from_reader(file: F) -> Result where - F: Read, + F: Read + 'static, { OpenFileOptions::new().from_reader(file) } diff --git a/object/src/lib.rs b/object/src/lib.rs index abbd8422..eb3b6ec2 100644 --- a/object/src/lib.rs +++ b/object/src/lib.rs @@ -151,6 +151,7 @@ pub use dicom_dictionary_std::StandardDataDictionary; pub type DefaultDicomObject = FileDicomObject>; use dicom_core::header::{GroupNumber, Header}; +use dicom_encoding::Codec; use dicom_encoding::adapters::{PixelDataObject, RawPixelData}; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_parser::dataset::{DataSetWriter, IntoTokens}; @@ -475,21 +476,34 @@ where .with_context(|| WriteUnsupportedTransferSyntaxSnafu { uid: self.meta.transfer_syntax.clone(), })?; - let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?; + if let Codec::Dataset(Some(adapter))= ts.codec() { + let adapter = adapter.adapt_writer(Box::new(to)); + let mut dset_writer = DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?; - // We use the default options, because only the inner object knows if something needs to change - dset_writer - .write_sequence((&self.obj).into_tokens()) - .context(PrintDataSetSnafu)?; + // write object + dset_writer + .write_sequence((&self.obj).into_tokens()) + .context(PrintDataSetSnafu)?; - Ok(()) + Ok(()) + + } else { + let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence((&self.obj).into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) + } } /// Write the entire object as a DICOM file /// into the given writer. /// Preamble, magic code, and file meta group will be included /// before the inner object. - pub fn write_all(&self, to: W) -> Result<(), WriteError> { + pub fn write_all(&self, to: impl Write) -> Result<(), WriteError> { let mut to = BufWriter::new(to); // write preamble @@ -507,14 +521,27 @@ where .with_context(|| WriteUnsupportedTransferSyntaxSnafu { uid: self.meta.transfer_syntax.clone(), })?; - let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?; + if let Codec::Dataset(Some(adapter))= ts.codec() { + let adapter = adapter.adapt_writer(Box::new(to)); + let mut dset_writer = DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence((&self.obj).into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) - // We use the default options, because only the inner object knows if something needs to change - dset_writer - .write_sequence((&self.obj).into_tokens()) - .context(PrintDataSetSnafu)?; + } else { + let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?; - Ok(()) + // write object + dset_writer + .write_sequence((&self.obj).into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) + } } /// Write the file meta group set into the given writer. @@ -528,7 +555,7 @@ where /// without preamble, magic code, nor file meta group. /// /// The transfer syntax is selected from the file meta table. - pub fn write_dataset(&self, to: W) -> Result<(), WriteError> { + pub fn write_dataset(&self, to: W) -> Result<(), WriteError> { let to = BufWriter::new(to); // prepare encoder @@ -537,14 +564,27 @@ where .with_context(|| WriteUnsupportedTransferSyntaxSnafu { uid: self.meta.transfer_syntax.clone(), })?; - let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?; + if let Codec::Dataset(Some(adapter))= ts.codec() { + let adapter = adapter.adapt_writer(Box::new(to)); + let mut dset_writer = DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence((&self.obj).into_tokens()) + .context(PrintDataSetSnafu)?; - // write object - dset_writer - .write_sequence((&self.obj).into_tokens()) - .context(PrintDataSetSnafu)?; + Ok(()) - Ok(()) + } else { + let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence((&self.obj).into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) + } } } diff --git a/object/src/mem.rs b/object/src/mem.rs index 9cdd8ff6..4e682111 100644 --- a/object/src/mem.rs +++ b/object/src/mem.rs @@ -39,6 +39,7 @@ use dicom_core::ops::{ ApplyOp, AttributeAction, AttributeOp, AttributeSelector, AttributeSelectorStep, }; +use dicom_encoding::Codec; use dicom_parser::dataset::read::{DataSetReaderOptions, OddLengthStrategy}; use itertools::Itertools; use smallvec::SmallVec; @@ -161,7 +162,7 @@ impl FileDicomObject> { /// followed by the rest of the data set. pub fn from_reader(src: S) -> Result where - S: Read, + S: Read + 'static, { Self::from_reader_with_dict(src, StandardDataDictionary) } @@ -237,7 +238,7 @@ impl InMemDicomObject { cs: SpecificCharacterSet, ) -> Result where - S: Read, + S: Read + 'static, { Self::read_dataset_with_dict_ts_cs(from, StandardDataDictionary, ts, cs) } @@ -380,20 +381,31 @@ where if let Some(ts) = ts_index.get(&meta.transfer_syntax) { let mut options = DataSetReaderOptions::default(); options.odd_length = odd_length; - let mut dataset = DataSetReader::new_with_ts_cs_options( - file, - ts, - SpecificCharacterSet::default(), - options, - ) - .context(CreateParserSnafu)?; - let obj = InMemDicomObject::build_object( - &mut dataset, - dict, - false, - Length::UNDEFINED, - read_until, - )?; + + let obj = if let Codec::Dataset(Some(adapter)) = ts.codec() { + let adapter = adapter.adapt_reader(Box::new(file)); + let mut dataset = + DataSetReader::new_with_ts(adapter, ts).context(CreateParserSnafu)?; + + InMemDicomObject::build_object( + &mut dataset, + dict, + false, + Length::UNDEFINED, + read_until, + )? + } else { + let mut dataset = + DataSetReader::new_with_ts(file, ts).context(CreateParserSnafu)?; + + InMemDicomObject::build_object( + &mut dataset, + dict, + false, + Length::UNDEFINED, + read_until, + )? + }; // if Media Storage SOP Class UID is empty attempt to infer from SOP Class UID if meta.media_storage_sop_class_uid().is_empty() { @@ -453,9 +465,9 @@ where /// is insufficient. Otherwise, please use [`from_reader_with_dict`] instead. /// /// [`from_reader_with_dict`]: #method.from_reader_with_dict - pub fn from_reader_with<'s, S, R>(src: S, dict: D, ts_index: R) -> Result + pub fn from_reader_with(src: S, dict: D, ts_index: R) -> Result where - S: Read + 's, + S: Read, R: TransferSyntaxIndex, { Self::from_reader_with_all_options( @@ -468,7 +480,7 @@ where ) } - pub(crate) fn from_reader_with_all_options<'s, S, R>( + pub(crate) fn from_reader_with_all_options( src: S, dict: D, ts_index: R, @@ -477,7 +489,7 @@ where odd_length: OddLengthStrategy, ) -> Result where - S: Read + 's, + S: Read, R: TransferSyntaxIndex, { let mut file = BufReader::new(src); @@ -500,20 +512,31 @@ where if let Some(ts) = ts_index.get(&meta.transfer_syntax) { let mut options = DataSetReaderOptions::default(); options.odd_length = odd_length; - let mut dataset = DataSetReader::new_with_ts_options( - file, - ts, - options, - ) - .context(CreateParserSnafu)?; - let obj = InMemDicomObject::build_object( - &mut dataset, - dict, - false, - Length::UNDEFINED, - read_until, - )?; - Ok(FileDicomObject { meta, obj }) + + if let Codec::Dataset(Some(adapter)) = ts.codec() { + let adapter = adapter.adapt_reader(Box::new(file)); + let mut dataset = + DataSetReader::new_with_ts_options(adapter, ts, options).context(CreateParserSnafu)?; + let obj = InMemDicomObject::build_object( + &mut dataset, + dict, + false, + Length::UNDEFINED, + read_until, + )?; + Ok(FileDicomObject { meta, obj }) + } else { + let mut dataset = + DataSetReader::new_with_ts_options(file, ts, options).context(CreateParserSnafu)?; + let obj = InMemDicomObject::build_object( + &mut dataset, + dict, + false, + Length::UNDEFINED, + read_until, + )?; + Ok(FileDicomObject { meta, obj }) + } } else { ReadUnsupportedTransferSyntaxSnafu { uid: meta.transfer_syntax, @@ -663,8 +686,15 @@ where D: DataDictionary, { let from = BufReader::new(from); - let mut dataset = DataSetReader::new_with_ts_cs(from, ts, cs).context(CreateParserSnafu)?; - InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) + if let Codec::Dataset(Some(adapter)) = ts.codec() { + let adapter = adapter.adapt_reader(Box::new(from)); + let mut dataset = + DataSetReader::new_with_ts_cs(adapter, ts, cs).context(CreateParserSnafu)?; + InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) + } else { + let mut dataset = DataSetReader::new_with_ts_cs(from, ts, cs).context(CreateParserSnafu)?; + InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) + } } // Standard methods follow. They are not placed as a trait implementation @@ -1745,7 +1775,9 @@ where /// in which then that character set will be used. /// /// Note: [`write_dataset_with_ts`] and [`write_dataset_with_ts_cs`] - /// may be easier to use. + /// may be easier to use and _will_ apply a dataset adapter (such as + /// DeflatedExplicitVRLittleEndian (1.2.840.10008.1.2.99)) whereas this + /// method will _not_ /// /// [`write_dataset_with_ts`]: #method.write_dataset_with_ts /// [`write_dataset_with_ts_cs`]: #method.write_dataset_with_ts_cs @@ -1781,16 +1813,28 @@ where where W: Write, { - // prepare data set writer - let mut dset_writer = DataSetWriter::with_ts_cs(to, ts, cs).context(CreatePrinterSnafu)?; - let required_options = IntoTokensOptions::new(self.charset_changed); + if let Codec::Dataset(Some(adapter)) = ts.codec() { + let adapter = adapter.adapt_writer(Box::new(to)); + // prepare data set writer + let mut dset_writer = DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?; - // write object - dset_writer - .write_sequence(self.into_tokens_with_options(required_options)) - .context(PrintDataSetSnafu)?; + // write object + dset_writer + .write_sequence(self.into_tokens()) + .context(PrintDataSetSnafu)?; - Ok(()) + Ok(()) + } else { + // prepare data set writer + let mut dset_writer = DataSetWriter::with_ts_cs(to, ts, cs).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence(self.into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) + } } /// Write this object's data set into the given writer, diff --git a/pixeldata/src/lib.rs b/pixeldata/src/lib.rs index 4c29b2ce..188a0469 100644 --- a/pixeldata/src/lib.rs +++ b/pixeldata/src/lib.rs @@ -2545,6 +2545,23 @@ mod tests { } } + #[test] + #[ignore = "test is unsound"] + fn test_can_read_deflated(){ + + let path = dicom_test_files::path("pydicom/image_dfl.dcm").expect("test DICOM file should exist"); + + // should read preamble even though it's from a reader + let obj = open_file(path.clone()).expect("Should read file"); + + let res = obj.decode_pixel_data().expect("Should decode pixel data."); + assert_eq!(res.to_vec::().unwrap().len(), (res.rows() as usize * res.columns() as usize)); + let mut buf = Vec::::new(); + obj.write_all(&mut buf).expect("Should write deflated"); + + assert_eq!(std::fs::metadata(path).unwrap().len() as usize, buf.len()) + } + #[cfg(not(feature = "gdcm"))] mod not_gdcm { #[cfg(feature = "ndarray")] diff --git a/pixeldata/src/transcode.rs b/pixeldata/src/transcode.rs index ca26239f..f82008f9 100644 --- a/pixeldata/src/transcode.rs +++ b/pixeldata/src/transcode.rs @@ -125,14 +125,14 @@ where ts: current_ts_uid.to_string(), })?; - match (current_ts.is_codec_free(), ts.is_codec_free()) { - (true, true) => { + match (current_ts.is_encapsulated_pixel_data(), ts.is_encapsulated_pixel_data()) { + (false, false) => { // no pixel data conversion is necessary: // change transfer syntax and return self.meta_mut().set_transfer_syntax(ts); Ok(()) } - (false, true) => { + (true, false) => { // decode pixel data let decoded_pixeldata = self.decode_pixel_data().context(DecodePixelDataSnafu)?; @@ -171,7 +171,7 @@ where Ok(()) } - (_, false) => { + (_, true) => { // must decode then encode let writer = match ts.codec() { Codec::EncapsulatedPixelData(_, Some(writer)) => writer, @@ -181,7 +181,7 @@ where Codec::Dataset(None) => return UnsupportedTransferSyntaxSnafu.fail()?, Codec::Dataset(Some(_)) => return UnsupportedTranscodingSnafu.fail()?, Codec::None => { - // already tested in `is_codec_free` + // already tested in `is_encapsulated_pixel_data` unreachable!("Unexpected codec from transfer syntax") } }; diff --git a/storescu/src/main.rs b/storescu/src/main.rs index 4b153772..0647bc90 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -141,6 +141,7 @@ enum Error { /// No TransferSyntax NoNegotiatedTransferSyntax, /// Transcoding error + #[cfg(feature = "transcode")] Transcode { source: dicom_pixeldata::TranscodeError, }, diff --git a/transfer-syntax-registry/Cargo.toml b/transfer-syntax-registry/Cargo.toml index cf91f741..37dc30a0 100644 --- a/transfer-syntax-registry/Cargo.toml +++ b/transfer-syntax-registry/Cargo.toml @@ -11,11 +11,13 @@ keywords = ["dicom"] readme = "README.md" [features] -default = ["rayon", "simd"] +default = ["rayon", "simd", "deflate"] # inventory for compile time plugin-based transfer syntax registration inventory-registry = ['dicom-encoding/inventory-registry'] +# Flate2 for Deflate read/write +deflate = ["dep:flate2"] # natively implemented image encodings native = ["jpeg", "rle"] # native implementations that work on Windows @@ -56,6 +58,10 @@ lazy_static = "1.2.0" byteordered = "0.6" tracing = "0.1.34" +[dependencies.flate2] +version = "1.0.28" +optional = true + [dependencies.jpeg2k] version = "0.9.1" optional = true @@ -94,3 +100,4 @@ features = ["native"] [dev-dependencies] dicom-test-files = "0.3" +dicom-object = { path = "../object" } diff --git a/transfer-syntax-registry/src/adapters/encapsulated.rs b/transfer-syntax-registry/src/adapters/encapsulated.rs new file mode 100644 index 00000000..9c43f19a --- /dev/null +++ b/transfer-syntax-registry/src/adapters/encapsulated.rs @@ -0,0 +1,56 @@ +#[derive(debug)] +pub struct EncapsulatedAdapter; + +pub const ENCAPSULATED_UNCOMPRESSED_EXPLICIT_VR_LITTLE_ENDIAN: TransferSyntax = TransferSyntax::new( + "1.2.840.10008.1.2.1.98", + "Encapsulated Uncompressed Explicit VR Little Endian", + Endianness::Little, + true, + Codec::Encapsulated(Some(EncapsulatedAdapter), Some(EncapsulatedAdapter), +); + + +impl PixelDataReader for EncapsulatedAdapter { + fn decode_frame( + &self, + src: &dyn PixelDataObject, + frame: u32, + dst: &mut Vec, + ) -> DecodeResult<()>{ + let cols = src + .cols() + .context(decode_error::MissingAttributeSnafu { name: "Columns" })?; + let rows = src + .rows() + .context(decode_error::MissingAttributeSnafu { name: "Rows" })?; + let samples_per_pixel = + src.samples_per_pixel() + .context(decode_error::MissingAttributeSnafu { + name: "SamplesPerPixel", + })?; + let bits_allocated = src + .bits_allocated() + .context(decode_error::MissingAttributeSnafu { + name: "BitsAllocated", + })?; + + if bits_allocated != 8 && bits_allocated != 16 { + whatever!("BitsAllocated other than 8 or 16 is not supported"); + } + // Encapsulated Uncompressed has each frame encoded in one fragment + // So we can assume 1 frag = 1 frame + // ref. PS3.5 A.4.11 + let nr_frames = + src.number_of_fragments() + .whatever_context("Invalid pixel data, no fragments found")? as usize; + ensure!( + nr_frames > frame as usize, + decode_error::FrameRangeOutOfBoundsSnafu + ); + + let bytes_per_sample = (bits_allocated / 8) as usize; + let samples_per_pixel = samples_per_pixel as usize; + let decoded_pixel_data = match &src.fragment(frame as usize).value(); + dst + } +} diff --git a/transfer-syntax-registry/src/deflate.rs b/transfer-syntax-registry/src/deflate.rs index 617fc475..4fc709fa 100644 --- a/transfer-syntax-registry/src/deflate.rs +++ b/transfer-syntax-registry/src/deflate.rs @@ -1,43 +1,19 @@ //! Implementation of Deflated Explicit VR Little Endian. use std::io::{Read, Write}; -use byteordered::Endianness; -use dicom_encoding::{Codec, TransferSyntax, transfer_syntax::DataRWAdapter}; -use flate2; +use dicom_encoding::transfer_syntax::DataRWAdapter; use flate2::Compression; -/// Immaterial type representing an adapter for deflated data. +/// An adapter for deflated data. #[derive(Debug)] pub struct FlateAdapter; -/// **Fully implemented**: Deflated Explicit VR Little Endian -pub const DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN: TransferSyntax = TransferSyntax::new( - "1.2.840.10008.1.2.1.99", - "Deflated Explicit VR Little Endian", - Endianness::Little, - true, - Codec::Dataset(FlateAdapter), -); - -impl DataRWAdapter for FlateAdapter -where - R: Read, - W: Write, -{ - type Reader = flate2::read::DeflateDecoder; - type Writer = flate2::write::DeflateEncoder; - - fn adapt_reader(&self, reader: R) -> Self::Reader - where - R: Read, - { - flate2::read::DeflateDecoder::new(reader) +impl DataRWAdapter for FlateAdapter { + fn adapt_reader<'r>(&self, reader: Box) -> Box { + Box::new(flate2::read::DeflateDecoder::new(reader)) } - fn adapt_writer(&self, writer: W) -> Self::Writer - where - W: Write, - { - flate2::write::DeflateEncoder::new(writer, Compression::fast()) + fn adapt_writer<'w>(&self, writer: Box) -> Box { + Box::new(flate2::write::DeflateEncoder::new(writer, Compression::fast())) } } diff --git a/transfer-syntax-registry/src/entries.rs b/transfer-syntax-registry/src/entries.rs index 07736d0c..0b311676 100644 --- a/transfer-syntax-registry/src/entries.rs +++ b/transfer-syntax-registry/src/entries.rs @@ -40,6 +40,8 @@ use crate::adapters::jpegls::{JpegLsAdapter, JpegLsLosslessWriter}; use crate::adapters::jpegxl::{JpegXlAdapter, JpegXlLosslessEncoder}; #[cfg(feature = "rle")] use crate::adapters::rle_lossless::RleLosslessAdapter; +#[cfg(feature = "deflate")] +use crate::deflate::FlateAdapter; // -- the three base transfer syntaxes, fully supported -- @@ -181,8 +183,17 @@ pub const JPEG_LOSSLESS_NON_HIERARCHICAL_FIRST_ORDER_PREDICTION: Ts = create_ts_ "JPEG Lossless, Non-Hierarchical, First-Order Prediction", ); -// --- stub transfer syntaxes, known but not supported --- +#[cfg(feature = "deflate")] +/// **Fully implemented**: Deflated Explicit VR Little Endian +pub const DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN: TransferSyntax = TransferSyntax::new( + "1.2.840.10008.1.2.1.99", + "Deflated Explicit VR Little Endian", + Endianness::Little, + true, + Codec::Dataset(Some(FlateAdapter)), +); +#[cfg(not(feature = "deflate"))] /// **Stub descriptor:** Deflated Explicit VR Little Endian pub const DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN: Ts = Ts::new_ele( "1.2.840.10008.1.2.1.99", @@ -190,14 +201,38 @@ pub const DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN: Ts = Ts::new_ele( Codec::Dataset(None), ); +// --- stub transfer syntaxes, known but not supported --- + +/// **Implemented**: JPIP Referenced Deflate +#[cfg(feature = "deflate")] +pub const JPIP_REFERENCED_DEFLATE: TransferSyntax = TransferSyntax::new( + "1.2.840.10008.1.2.4.95", + "JPIP Referenced Deflate", + Endianness::Little, + true, + Codec::Dataset(Some(FlateAdapter)), +); + /// **Stub descriptor:** JPIP Referenced Deflate +#[cfg(not(feature = "deflate"))] pub const JPIP_REFERENCED_DEFLATE: Ts = Ts::new_ele( "1.2.840.10008.1.2.4.95", "JPIP Referenced Deflate", Codec::Dataset(None), ); -/// **Stub descriptor:** JPIP Referenced Deflate +/// **Implemented**: JPIP HTJ2K Referenced Deflate +#[cfg(feature = "deflate")] +pub const JPIP_HTJ2K_REFERENCED_DEFLATE: TransferSyntax = TransferSyntax::new( + "1.2.840.10008.1.2.4.205", + "JPIP HT2JK Referenced Deflate", + Endianness::Little, + true, + Codec::Dataset(Some(FlateAdapter)), +); + +/// **Stub descriptor:** JPIP HTJ2K Referenced Deflate +#[cfg(not(feature = "deflate"))] pub const JPIP_HTJ2K_REFERENCED_DEFLATE: Ts = Ts::new_ele( "1.2.840.10008.1.2.4.205", "JPIP HTJ2K Referenced Deflate", diff --git a/transfer-syntax-registry/src/lib.rs b/transfer-syntax-registry/src/lib.rs index 8314af69..817a0b4a 100644 --- a/transfer-syntax-registry/src/lib.rs +++ b/transfer-syntax-registry/src/lib.rs @@ -114,6 +114,8 @@ pub use dicom_encoding::TransferSyntax; pub mod entries; mod adapters; +#[cfg(feature = "deflate")] +mod deflate; #[cfg(feature = "inventory-registry")] pub use dicom_encoding::inventory; diff --git a/transfer-syntax-registry/tests/deflate.rs b/transfer-syntax-registry/tests/deflate.rs new file mode 100644 index 00000000..a4fd783c --- /dev/null +++ b/transfer-syntax-registry/tests/deflate.rs @@ -0,0 +1,69 @@ +use std::{ + fs::{metadata, File}, + io::BufReader, +}; + +use dicom_core::Tag; +use dicom_object::OpenFileOptions; + +#[test] +fn test_read_data_deflated() { + let path = + dicom_test_files::path("pydicom/image_dfl.dcm").expect("test DICOM file should exist"); + let source = BufReader::new(File::open(path).unwrap()); + + // should read preamble even though it's from a reader + let object = OpenFileOptions::new() + .from_reader(source) + .expect("Should read from source successfully"); + + // inspect some attributes + + // SOP Instance UID + assert_eq!( + object.get(Tag(0x0008, 0x0018)).unwrap().to_str().unwrap(), + "1.3.6.1.4.1.5962.1.1.0.0.0.977067309.6001.0", + ); + + // photometric interpretation + assert_eq!( + object.get(Tag(0x0028, 0x0004)).unwrap().to_str().unwrap(), + "MONOCHROME2", + ); + + let rows: u16 = object.get(Tag(0x0028, 0x0010)).unwrap().to_int().unwrap(); + let cols: u16 = object.get(Tag(0x0028, 0x0011)).unwrap().to_int().unwrap(); + let spp: u16 = object.get(Tag(0x0028, 0x0002)).unwrap().to_int().unwrap(); + assert_eq!((rows, cols, spp), (512, 512, 1)); + + // pixel data + + let pixel_data = object.get(Tag(0x7FE0, 0x0010)).unwrap().to_bytes().unwrap(); + + assert_eq!( + pixel_data.len(), + rows as usize * cols as usize * spp as usize, + ); + + // poke some of the pixel samples + assert_eq!(pixel_data[0], 0xd5); + assert_eq!(pixel_data[0x0080], 0x29); + assert_eq!(pixel_data[0x0804], 0xff); +} + +#[test] +#[ignore = "test is unsound"] +fn write_deflated() { + let path = + dicom_test_files::path("pydicom/image_dfl.dcm").expect("test DICOM file should exist"); + let source = BufReader::new(File::open(path.clone()).unwrap()); + + // should read preamble even though it's from a reader + let object = OpenFileOptions::new() + .from_reader(source) + .expect("Should read from source successfully"); + + let mut buf = Vec::::new(); + object.write_all(&mut buf).unwrap(); + assert_eq!(buf.len(), metadata(path).unwrap().len() as usize); +} diff --git a/transfer-syntax-registry/tests/submit_dataset.rs b/transfer-syntax-registry/tests/submit_dataset.rs index dfd52bd9..22a238d2 100644 --- a/transfer-syntax-registry/tests/submit_dataset.rs +++ b/transfer-syntax-registry/tests/submit_dataset.rs @@ -15,22 +15,14 @@ use std::io::{Read, Write}; #[derive(Debug)] struct DummyCodecAdapter; -impl DataRWAdapter for DummyCodecAdapter { - type Reader = Box; - type Writer = Box; +impl DataRWAdapter for DummyCodecAdapter { - fn adapt_reader(&self, reader: R) -> Self::Reader - where - R: Read, - { - Box::new(reader) as Box<_> + fn adapt_reader<'r>(&self, reader: Box) -> Box { + reader } - fn adapt_writer(&self, writer: W) -> Self::Writer - where - W: Write, - { - Box::new(writer) as Box<_> + fn adapt_writer<'w>(&self, writer: Box) -> Box { + writer } } diff --git a/transfer-syntax-registry/tests/submit_replace.rs b/transfer-syntax-registry/tests/submit_replace.rs index 824c463b..e78847da 100644 --- a/transfer-syntax-registry/tests/submit_replace.rs +++ b/transfer-syntax-registry/tests/submit_replace.rs @@ -1,8 +1,9 @@ //! Independent test for submission of a dummy TS implementation //! to replace a built-in stub. //! -//! Only applicable to the inventory-based registry. -#![cfg(feature = "inventory-registry")] +//! Only applicable to the inventory-based registry, +//! and only if JPIP Referenced Deflate is not yet supported. +#![cfg(all(feature = "inventory-registry", not(feature = "deflate")))] use dicom_encoding::{ submit_transfer_syntax, Codec, DataRWAdapter, Endianness, NeverPixelAdapter, TransferSyntax, @@ -15,21 +16,12 @@ use std::io::{Read, Write}; #[derive(Debug)] struct DummyCodecAdapter; -impl DataRWAdapter for DummyCodecAdapter { - type Reader = Box; - type Writer = Box; - - fn adapt_reader(&self, _reader: R) -> Self::Reader - where - R: Read, - { +impl DataRWAdapter for DummyCodecAdapter { + fn adapt_reader<'r>(&self, _reader: Box) -> Box { unimplemented!() } - fn adapt_writer(&self, _writer: W) -> Self::Writer - where - W: Write, - { + fn adapt_writer<'w>(&self, _writer: Box) -> Box { unimplemented!() } } diff --git a/transfer-syntax-registry/tests/submit_replace_precondition.rs b/transfer-syntax-registry/tests/submit_replace_precondition.rs index 3c97910c..8fe231a7 100644 --- a/transfer-syntax-registry/tests/submit_replace_precondition.rs +++ b/transfer-syntax-registry/tests/submit_replace_precondition.rs @@ -4,15 +4,15 @@ //! Only applicable to the inventory-based registry. #![cfg(feature = "inventory-registry")] -use dicom_encoding::{Codec, TransferSyntaxIndex}; -use dicom_transfer_syntax_registry::TransferSyntaxRegistry; - /// Assert that this transfer syntax is provided built-in as a stub. /// -/// If this changes, please replace the transfer syntax to test against -/// and override. +/// This only applies if Cargo feature "deflate" is disabled. +#[cfg(not(feature = "deflate"))] #[test] fn registry_has_stub_ts_by_default() { + use dicom_encoding::{Codec, TransferSyntaxIndex}; + use dicom_transfer_syntax_registry::TransferSyntaxRegistry; + // this TS is provided by default, but not fully supported let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.4.95"); assert!(ts.is_some());