diff --git a/pixeldata/src/bin/dicom-transcode.rs b/pixeldata/src/bin/dicom-transcode.rs index 018a51ca..df71c3fd 100644 --- a/pixeldata/src/bin/dicom-transcode.rs +++ b/pixeldata/src/bin/dicom-transcode.rs @@ -89,6 +89,11 @@ struct TargetTransferSyntax { #[cfg(feature = "jpegxl")] #[clap(long = "jpeg-xl")] jpeg_xl: bool, + + /// Transcode to Deflated Image Frame + #[cfg(feature = "deflate")] + #[clap(long = "deflated-image-frame")] + deflated_image_frame: bool, } impl TargetTransferSyntax { @@ -109,6 +114,8 @@ impl TargetTransferSyntax { jpeg_xl_lossless: false, #[cfg(feature = "jpegxl")] jpeg_xl: false, + #[cfg(feature = "deflate")] + deflated_image_frame: false, } => snafu::whatever!("No target transfer syntax specified"), // explicit VR little endian TargetTransferSyntax { @@ -158,6 +165,11 @@ impl TargetTransferSyntax { TargetTransferSyntax { jpeg_xl: true, .. } => TransferSyntaxRegistry .get(uids::JPEGXL) .whatever_context("Missing specifier for JPEG XL"), + // Deflated Image Frame Compression + #[cfg(feature = "deflate")] + TargetTransferSyntax { deflated_image_frame: true, .. } => TransferSyntaxRegistry + .get(uids::DEFLATED_IMAGE_FRAME_COMPRESSION) + .whatever_context("Missing specifier for Deflated Image Frame Compression"), TargetTransferSyntax { ts: Some(ts), .. } => TransferSyntaxRegistry .get(ts) .whatever_context("Unknown transfer syntax"), diff --git a/transfer-syntax-registry/src/adapters/deflated.rs b/transfer-syntax-registry/src/adapters/deflated.rs new file mode 100644 index 00000000..b5b86f4f --- /dev/null +++ b/transfer-syntax-registry/src/adapters/deflated.rs @@ -0,0 +1,145 @@ +//! Support for deflated image frame compression via pixel data adapter. + +use dicom_core::{ + ops::{AttributeAction, AttributeOp}, + PrimitiveValue, Tag, +}; +use dicom_encoding::{ + adapters::{ + DecodeResult, EncodeOptions, EncodeResult, PixelDataObject, PixelDataReader, PixelDataWriter, decode_error, encode_error + }, + snafu::{OptionExt, ResultExt}, +}; + +use flate2::{Compression, read::DeflateDecoder, write::DeflateEncoder}; + +/// Adapter for [Deflated Image Frame Compression][1] +/// [1]: https://dicom.nema.org/medical/dicom/2025c/output/chtml/part05/sect_10.20.html +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DeflatedImageFrameAdapter; + +impl PixelDataReader for DeflatedImageFrameAdapter { + fn decode(&self, src: &dyn PixelDataObject, dst: &mut Vec) -> DecodeResult<()> { + // decod each fragment into the output vector + let pixeldata = src + .raw_pixel_data() + .context(decode_error::MissingAttributeSnafu { name: "Pixel Data" })?; + + if pixeldata.fragments.is_empty() { + return Ok(()); + } + + // set up decoder with the first frame + let mut it = pixeldata.fragments.iter(); + let mut decoder = DeflateDecoder::new( + &it.next().expect("fragments should not be empty")[..] + ); + std::io::copy(&mut decoder, dst) + .whatever_context("failed to deflate frame")?; + for fragment in it { + decoder.reset(&fragment[..]); + std::io::copy(&mut decoder, dst) + .whatever_context("failed to deflate frame")?; + } + + Ok(()) + } + + fn decode_frame( + &self, + src: &dyn PixelDataObject, + frame: u32, + dst: &mut Vec, + ) -> DecodeResult<()> { + + // just copy the specific fragment into the output vector + let pixeldata = src + .raw_pixel_data() + .context(decode_error::MissingAttributeSnafu { name: "Pixel Data" })?; + + let fragment = pixeldata + .fragments + .get(frame as usize) + .context(decode_error::FrameRangeOutOfBoundsSnafu)?; + + let mut decoder = DeflateDecoder::new(&fragment[..]); + std::io::copy(&mut decoder, dst) + .whatever_context("failed to deflate frame")?; + + Ok(()) + } +} + +impl PixelDataWriter for DeflatedImageFrameAdapter { + fn encode_frame( + &self, + src: &dyn PixelDataObject, + frame: u32, + options: EncodeOptions, + dst: &mut Vec, + ) -> EncodeResult> { + use std::io::Write; + + let cols = src + .cols() + .context(encode_error::MissingAttributeSnafu { name: "Columns" })?; + let rows = src + .rows() + .context(encode_error::MissingAttributeSnafu { name: "Rows" })?; + let samples_per_pixel = + src.samples_per_pixel() + .context(encode_error::MissingAttributeSnafu { + name: "SamplesPerPixel", + })?; + let bits_allocated = src + .bits_allocated() + .context(encode_error::MissingAttributeSnafu { + name: "BitsAllocated", + })?; + + let bytes_per_sample = (bits_allocated / 8) as usize; + let frame_size = + cols as usize * rows as usize * samples_per_pixel as usize * bytes_per_sample; + + // identify frame data using the frame index + let pixeldata_uncompressed = &src + .raw_pixel_data() + .context(encode_error::MissingAttributeSnafu { name: "Pixel Data" })? + .fragments[0]; + + let frame_data = pixeldata_uncompressed + .get(frame_size * frame as usize..frame_size * (frame as usize + 1)) + .whatever_context("Frame index out of bounds")?; + + let len_before = dst.len(); + + // Deflate the data to the output + let compression = match options.effort { + None => Compression::default(), + Some(0) | Some(1) => Compression::fast(), + // map 0..=100 to 0..=9 + Some(e) => Compression::new((e.min(100) / 11) as u32), + }; + let mut encoder = DeflateEncoder::new(&mut *dst, compression); + encoder.write_all(frame_data) + .whatever_context("failed to encode deflated data")?; + encoder.finish() + .whatever_context("failed to finish deflated data encoding")?; + + if dst.len() % 2 == 1 { + // add null byte to maintain even length + dst.push(0); + } + + let fragment_len = dst.len() - len_before; + + // provide attribute changes + Ok(vec![ + // Encapsulated Pixel Data Value Total Length + AttributeOp::new( + Tag(0x7FE0, 0x0003), + AttributeAction::Set(PrimitiveValue::from(fragment_len as u64)), + ), + ]) + } +} diff --git a/transfer-syntax-registry/src/adapters/mod.rs b/transfer-syntax-registry/src/adapters/mod.rs index 16027777..3437da4e 100644 --- a/transfer-syntax-registry/src/adapters/mod.rs +++ b/transfer-syntax-registry/src/adapters/mod.rs @@ -35,6 +35,8 @@ pub mod jpegls; pub mod jpegxl; #[cfg(feature = "rle")] pub mod rle_lossless; +#[cfg(feature = "deflate")] +pub mod deflated; pub mod uncompressed; diff --git a/transfer-syntax-registry/src/adapters/uncompressed.rs b/transfer-syntax-registry/src/adapters/uncompressed.rs index f70b5f90..9a6e92ad 100644 --- a/transfer-syntax-registry/src/adapters/uncompressed.rs +++ b/transfer-syntax-registry/src/adapters/uncompressed.rs @@ -94,7 +94,7 @@ impl PixelDataWriter for UncompressedAdapter { .get(frame_size * frame as usize..frame_size * (frame as usize + 1)) .whatever_context("Frame index out of bounds")?; - // Copy the the data to the output + // Copy the data to the output dst.extend_from_slice(frame_data); // provide attribute changes diff --git a/transfer-syntax-registry/src/entries.rs b/transfer-syntax-registry/src/entries.rs index a0267f1f..4f919ae7 100644 --- a/transfer-syntax-registry/src/entries.rs +++ b/transfer-syntax-registry/src/entries.rs @@ -41,6 +41,8 @@ use crate::adapters::jpegxl::{JpegXlAdapter, JpegXlLosslessEncoder}; #[cfg(feature = "rle")] use crate::adapters::rle_lossless::RleLosslessAdapter; #[cfg(feature = "deflate")] +use crate::adapters::deflated::DeflatedImageFrameAdapter; +#[cfg(feature = "deflate")] use crate::deflate::FlateAdapter; // -- the three base transfer syntaxes, fully supported -- @@ -446,6 +448,21 @@ pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: Ts = create_ts_stub( "JPEG-LS Lossy (Near-Lossless) Image Compression", ); +/// **Stub descriptor:** Deflated Image Frame Compression +#[cfg(not(feature = "deflate"))] +pub const DEFLATED_IMAGE_FRAME_COMPRESSION: Ts = create_ts_stub( + "1.2.840.10008.1.2.8.1", + "Deflated Image Frame Compression", +); + +/// **Implemented:** Deflated Image Frame Compression +#[cfg(feature = "deflate")] +pub const DEFLATED_IMAGE_FRAME_COMPRESSION: TransferSyntax = TransferSyntax::new_ele( + "1.2.840.10008.1.2.8.1", + "Deflated Image Frame Compression", + Codec::EncapsulatedPixelData(Some(DeflatedImageFrameAdapter), Some(DeflatedImageFrameAdapter)), +); + /// **Stub descriptor:** JPIP Referenced pub const JPIP_REFERENCED: Ts = create_ts_stub("1.2.840.10008.1.2.4.94", "JPIP Referenced"); diff --git a/transfer-syntax-registry/src/lib.rs b/transfer-syntax-registry/src/lib.rs index 11bc25ed..4d779d40 100644 --- a/transfer-syntax-registry/src/lib.rs +++ b/transfer-syntax-registry/src/lib.rs @@ -72,8 +72,10 @@ //! | JPEG XL Recompression | Cargo feature `jpegxl` | x | //! | JPEG XL | Cargo feature `jpegxl` | ✓ | //! | RLE Lossless | Cargo feature `rle` | x | +//! | Deflated Image Frame | Cargo feature `deflate` | ✓ | //! -//! Cargo features behind `native` (`jpeg`, `rle`) are added by default. +//! Cargo features behind `native` (`jpeg`, `rle`, `deflate`) +//! are added by default in the [`dicom-pixeldata` crate][dicom-pixeldata]. //! They provide implementations that are written in pure Rust //! and are likely available in all supported platforms without issues. //! Additional codecs are opt-in by enabling Cargo features, @@ -104,6 +106,7 @@ //! if using the inventory-based registry. //! //! [inventory]: https://docs.rs/inventory/0.3.15/inventory +//! [dicom-pixeldata]: https://docs.rs/dicom-pixeldata use dicom_encoding::transfer_syntax::{AdapterFreeTransferSyntax as Ts, Codec}; use lazy_static::lazy_static; @@ -236,11 +239,11 @@ lazy_static! { static ref REGISTRY: TransferSyntaxRegistryImpl = { let mut registry = TransferSyntaxRegistryImpl { - m: HashMap::with_capacity(32), + m: HashMap::with_capacity(64), }; use self::entries::*; - let built_in_ts: [TransferSyntax; 45] = [ + let built_in_ts: [TransferSyntax; 46] = [ IMPLICIT_VR_LITTLE_ENDIAN.erased(), EXPLICIT_VR_LITTLE_ENDIAN.erased(), EXPLICIT_VR_BIG_ENDIAN.erased(), @@ -286,6 +289,7 @@ lazy_static! { HEVC_H265_MAIN_PROFILE.erased(), HEVC_H265_MAIN_10_PROFILE.erased(), RLE_LOSSLESS.erased(), + DEFLATED_IMAGE_FRAME_COMPRESSION.erased(), SMPTE_ST_2110_20_UNCOMPRESSED_PROGRESSIVE.erased(), SMPTE_ST_2110_20_UNCOMPRESSED_INTERLACED.erased(), SMPTE_ST_2110_30_PCM.erased(), diff --git a/transfer-syntax-registry/tests/deflated_frame.rs b/transfer-syntax-registry/tests/deflated_frame.rs new file mode 100644 index 00000000..8c034d77 --- /dev/null +++ b/transfer-syntax-registry/tests/deflated_frame.rs @@ -0,0 +1,102 @@ +//! Test suite for Deflated Image Frame Compression pixel data reading and writing +#![cfg(feature = "deflate")] + +mod adapters; + +use adapters::TestDataObject; +use dicom_core::value::PixelFragmentSequence; +use dicom_encoding::{ + adapters::{EncodeOptions, PixelDataReader, PixelDataWriter}, + Codec, +}; +use dicom_transfer_syntax_registry::entries::DEFLATED_IMAGE_FRAME_COMPRESSION; + +/// writing to Deflated Image Frame Compression and back +/// should yield exactly the same pixel data +#[test] +fn write_and_read_deflated_frames() { + let rows: u16 = 128; + let columns: u16 = 256; + + // build some random RGB image + let mut samples = vec![0; rows as usize * columns as usize * 3]; + + // use linear congruence to make RGB noise + let mut seed = 0xcfcf_acab_u32; + let mut gen_sample = || { + let r = 4_294_967_291_u32; + let b = 67291_u32; + seed = seed.wrapping_mul(r).wrapping_add(b); + // grab a portion from the seed + (seed >> 7) as u8 + }; + + let slab = 8; + for y in (0..rows as usize).step_by(slab) { + let scan_r = gen_sample(); + let scan_g = gen_sample(); + let scan_b = gen_sample(); + + for x in 0..columns as usize { + for k in 0..slab { + let offset = ((y + k) * columns as usize + x) * 3; + samples[offset] = scan_r; + samples[offset + 1] = scan_g; + samples[offset + 2] = scan_b; + } + } + } + + // create test object of native encoding + let obj = TestDataObject { + // Explicit VR Little Endian + ts_uid: "1.2.840.10008.1.2.1".to_string(), + rows, + columns, + bits_allocated: 8, + bits_stored: 8, + samples_per_pixel: 3, + photometric_interpretation: "RGB", + number_of_frames: 1, + flat_pixel_data: Some(samples.clone()), + pixel_data_sequence: None, + }; + + // fetch adapters for Deflated Image Frame Compression + + let Codec::EncapsulatedPixelData(Some(reader), Some(writer)) = DEFLATED_IMAGE_FRAME_COMPRESSION.codec() else { + panic!("Deflated Image Frame Compression pixel data adapters not found") + }; + + let mut encoded = vec![]; + + let _ops = writer + .encode_frame(&obj, 0, EncodeOptions::default(), &mut encoded) + .expect("Deflated Image Frame encoding failed"); + + // instantiate new object representing the compressed version + + let obj = TestDataObject { + // Deflated Image Frame Compression + ts_uid: "1.2.840.10008.1.2.8.1".to_string(), + rows, + columns, + bits_allocated: 8, + bits_stored: 8, + samples_per_pixel: 3, + photometric_interpretation: "RGB", + number_of_frames: 1, + flat_pixel_data: None, + pixel_data_sequence: Some(PixelFragmentSequence::new(vec![], vec![encoded])), + }; + + // decode frame + let mut decoded = vec![]; + + reader + .decode_frame(&obj, 0, &mut decoded) + .expect("Deflated Image Frame decoding failed"); + + // compare pixels, lossless encoding should yield exactly the same data + assert_eq!(samples, decoded, "pixel data mismatch"); +}