diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a29bd6712..5a22da251 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,8 +27,8 @@ jobs: - run: cargo test --features image,ndarray,sop-class,rle,cli # test dicom-pixeldata with openjp2 - run: cargo test -p dicom-pixeldata --features openjp2 - # test dicom-pixeldata with openjpeg-sys - - run: cargo test -p dicom-pixeldata --features openjpeg-sys + # test dicom-pixeldata with openjpeg-sys and charls + - run: cargo test -p dicom-pixeldata --features openjpeg-sys,charls # test dicom-pixeldata with gdcm-rs - run: cargo test -p dicom-pixeldata --features gdcm # test dicom-pixeldata without default features diff --git a/Cargo.lock b/Cargo.lock index 55e8e28e5..7ad92404f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,24 @@ version = "1.0.0" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charls" +version = "0.3.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "bab83a34f8145921567f034416dd9d0396956aff3c53eb7a32998d4e0a25ac9e" +dependencies = [ + "charls-sys", +] + +[[package]] +name = "charls-sys" +version = "2.4.2" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "63d9445022ff0c7b9016e7035bb5291bdbf6eacc993b25d1378844c0f0a7c82f" +dependencies = [ + "cmake", +] + [[package]] name = "chrono" version = "0.4.38" @@ -592,6 +610,7 @@ name = "dicom-transfer-syntax-registry" version = "0.7.0" dependencies = [ "byteordered", + "charls", "dicom-core", "dicom-encoding", "dicom-test-files", diff --git a/pixeldata/Cargo.toml b/pixeldata/Cargo.toml index b7eb69146..55d021a53 100644 --- a/pixeldata/Cargo.toml +++ b/pixeldata/Cargo.toml @@ -64,6 +64,8 @@ rle = ["dicom-transfer-syntax-registry/rle"] openjpeg-sys = ["dicom-transfer-syntax-registry/openjpeg-sys"] # JPEG 2000 decoding via Rust port of OpenJPEG openjp2 = ["dicom-transfer-syntax-registry/openjp2"] +# JpegLS via CharLS +charls = ["dicom-transfer-syntax-registry/charls"] # replace pixel data decoding to use GDCM gdcm = ["gdcm-rs"] diff --git a/pixeldata/src/lib.rs b/pixeldata/src/lib.rs index b54b9ae4b..039d592ab 100644 --- a/pixeldata/src/lib.rs +++ b/pixeldata/src/lib.rs @@ -2714,11 +2714,12 @@ mod tests { case("pydicom/JPEG2000.dcm", 1) )] // - // jpeg-ls encoding not supported - #[should_panic(expected = "UnsupportedTransferSyntax { ts: \"1.2.840.10008.1.2.4.80\"")] - #[case("pydicom/emri_small_jpeg_ls_lossless.dcm", 10)] - #[should_panic(expected = "UnsupportedTransferSyntax { ts: \"1.2.840.10008.1.2.4.80\"")] - #[case("pydicom/MR_small_jpeg_ls_lossless.dcm", 1)] + // jpeg-ls encoding + #[cfg_attr( + feature = "charls", + case("pydicom/emri_small_jpeg_ls_lossless.dcm", 10) + )] + #[cfg_attr(feature = "charls", case("pydicom/MR_small_jpeg_ls_lossless.dcm", 1))] // // sample precision of 12 not supported yet #[should_panic(expected = "Unsupported(SamplePrecision(12))")] @@ -2775,6 +2776,8 @@ mod tests { #[case("pydicom/SC_rgb_rle_2frame.dcm", 0)] #[case("pydicom/SC_rgb_rle_2frame.dcm", 1)] #[case("pydicom/JPEG2000_UNC.dcm", 0)] + #[cfg_attr(feature = "charls", case("pydicom/emri_small_jpeg_ls_lossless.dcm", 5))] + #[cfg_attr(feature = "charls", case("pydicom/MR_small_jpeg_ls_lossless.dcm", 0))] fn test_decode_pixel_data_individual_frames(#[case] value: &str, #[case] frame: u32) { use crate::PixelDecoder as _; use std::path::Path; diff --git a/transfer-syntax-registry/Cargo.toml b/transfer-syntax-registry/Cargo.toml index b52056869..3cf06dcf3 100644 --- a/transfer-syntax-registry/Cargo.toml +++ b/transfer-syntax-registry/Cargo.toml @@ -35,6 +35,9 @@ simd = ["jpeg-encoder?/simd"] # conflicts with `openjp2` openjpeg-sys = ["dep:jpeg2k", "jpeg2k/openjpeg-sys"] +# jpeg LS support via charls bindings +charls = ["dep:charls"] + # build OpenJPEG with multithreading, # implies "rayon" openjpeg-sys-threads = ["rayon", "jpeg2k?/threads"] @@ -59,6 +62,11 @@ optional = true version = "0.6" optional = true +[dependencies.charls] +version = "0.3" +optional = true +features = ["static"] + [package.metadata.docs.rs] features = ["native"] diff --git a/transfer-syntax-registry/src/adapters/jpegls.rs b/transfer-syntax-registry/src/adapters/jpegls.rs new file mode 100644 index 000000000..2ab87483a --- /dev/null +++ b/transfer-syntax-registry/src/adapters/jpegls.rs @@ -0,0 +1,95 @@ +//! Support for JPEG-LS image decoding. + +use charls::CharLS; +use dicom_encoding::adapters::{decode_error, DecodeResult, PixelDataObject, PixelDataReader}; +use dicom_encoding::snafu::prelude::*; +use std::borrow::Cow; + +/// Pixel data adapter for JPEG-LS transfer syntax. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct JpegLSAdapter; + +impl PixelDataReader for JpegLSAdapter { + /// Decode a single frame in JPEG-LS from a DICOM object. + fn decode_frame( + &self, + src: &dyn PixelDataObject, + frame: u32, + dst: &mut Vec, + ) -> DecodeResult<()> { + let bits_allocated = src + .bits_allocated() + .context(decode_error::MissingAttributeSnafu { + name: "BitsAllocated", + })?; + + ensure_whatever!( + bits_allocated == 8 || bits_allocated == 16, + "BitsAllocated other than 8 or 16 is not supported" + ); + + let nr_frames = src.number_of_frames().unwrap_or(1) as usize; + + ensure!( + nr_frames > frame as usize, + decode_error::FrameRangeOutOfBoundsSnafu + ); + + let raw = src + .raw_pixel_data() + .whatever_context("Expected to have raw pixel data available")?; + + let frame_data = if raw.fragments.len() == 1 || raw.fragments.len() == nr_frames { + // assuming 1:1 frame-to-fragment mapping + Cow::Borrowed( + raw.fragments + .get(frame as usize) + .with_whatever_context(|| { + format!("Missing fragment #{} for the frame requested", frame) + })?, + ) + } else { + // Some embedded JPEGs might span multiple fragments. + // In this case we look up the basic offset table + // and gather all of the frame's fragments in a single vector. + // Note: not the most efficient way to do this, + // consider optimizing later with byte chunk readers + let base_offset = raw.offset_table.get(frame as usize).copied(); + let base_offset = if frame == 0 { + base_offset.unwrap_or(0) as usize + } else { + base_offset + .with_whatever_context(|| format!("Missing offset for frame #{}", frame))? + as usize + }; + let next_offset = raw.offset_table.get(frame as usize + 1); + + let mut offset = 0; + let mut fragments = Vec::new(); + for fragment in &raw.fragments { + // include it + if offset >= base_offset { + fragments.extend_from_slice(fragment); + } + offset += fragment.len() + 8; + if let Some(&next_offset) = next_offset { + if offset >= next_offset as usize { + // next fragment is for the next frame + break; + } + } + } + + Cow::Owned(fragments) + }; + + let mut decoded = CharLS::default() + .decode(&frame_data) + .map_err(|error| error.to_string()) + .with_whatever_context(|error| error.to_string())?; + + dst.append(&mut decoded); + + Ok(()) + } +} diff --git a/transfer-syntax-registry/src/adapters/mod.rs b/transfer-syntax-registry/src/adapters/mod.rs index 612d71451..4ab7bf734 100644 --- a/transfer-syntax-registry/src/adapters/mod.rs +++ b/transfer-syntax-registry/src/adapters/mod.rs @@ -27,6 +27,8 @@ pub mod jpeg; #[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))] pub mod jpeg2k; +#[cfg(feature = "charls")] +pub mod jpegls; #[cfg(feature = "rle")] pub mod rle_lossless; @@ -46,3 +48,8 @@ pub mod jpeg2k {} /// Enable the `rle` feature to use this module. #[cfg(not(feature = "rle"))] pub mod rle {} + +/// **Note:** This module is a stub. +/// Enable the `charls` feature to use this module. +#[cfg(not(feature = "charls"))] +pub mod jpegls {} diff --git a/transfer-syntax-registry/src/entries.rs b/transfer-syntax-registry/src/entries.rs index 6549f9d06..280a5c6a3 100644 --- a/transfer-syntax-registry/src/entries.rs +++ b/transfer-syntax-registry/src/entries.rs @@ -34,6 +34,8 @@ use dicom_encoding::NeverPixelAdapter; use crate::adapters::jpeg::JpegAdapter; #[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))] use crate::adapters::jpeg2k::Jpeg2000Adapter; +#[cfg(feature = "charls")] +use crate::adapters::jpegls::JpegLSAdapter; #[cfg(feature = "rle")] use crate::adapters::rle_lossless::RleLosslessAdapter; @@ -262,12 +264,42 @@ pub const JPEG_2000_PART2_MULTI_COMPONENT_IMAGE_COMPRESSION: Ts = create_ts_stub // --- partially supported transfer syntaxes, pixel data encapsulation not supported --- +/// An alias for a transfer syntax specifier with [`JpegLSAdapter`] +#[cfg(feature = "charls")] +type JpegLSTs = TransferSyntax; + +/// Create JPEG-LS TransferSyntax +#[cfg(feature = "charls")] +const fn create_ts_jpegls(uid: &'static str, name: &'static str) -> JpegLSTs { + TransferSyntax::new_ele( + uid, + name, + Codec::EncapsulatedPixelData(Some(JpegLSAdapter), None), + ) +} + +/// **Decoder Implementation:** JPEG-LS Lossless Image Compression +#[cfg(feature = "charls")] +pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls( + "1.2.840.10008.1.2.4.80", + "JPEG-LS Lossless Image Compression", +); + /// **Stub descriptor:** JPEG-LS Lossless Image Compression +#[cfg(not(feature = "charls"))] pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: Ts = create_ts_stub( "1.2.840.10008.1.2.4.80", "JPEG-LS Lossless Image Compression", ); +/// **Decoder Implementation:** JPEG-LS Lossy (Near-Lossless) Image Compression +#[cfg(feature = "charls")] +pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls( + "1.2.840.10008.1.2.4.81", + "JPEG-LS Lossy (Near-Lossless) Image Compression", +); + /// **Stub descriptor:** JPEG-LS Lossy (Near-Lossless) Image Compression +#[cfg(not(feature = "charls"))] pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: Ts = create_ts_stub( "1.2.840.10008.1.2.4.81", "JPEG-LS Lossy (Near-Lossless) Image Compression", diff --git a/transfer-syntax-registry/src/lib.rs b/transfer-syntax-registry/src/lib.rs index b1e529be1..82a852308 100644 --- a/transfer-syntax-registry/src/lib.rs +++ b/transfer-syntax-registry/src/lib.rs @@ -56,6 +56,8 @@ //! | JPEG Extended (Process 2 & 4) | Cargo feature `jpeg` | x | //! | JPEG Lossless, Non-Hierarchical (Process 14) | Cargo feature `jpeg` | x | //! | JPEG Lossless, Non-Hierarchical, First-Order Prediction (Process 14 [Selection Value 1]) | Cargo feature `jpeg` | x | +//! | JPEG-LS Lossless | Cargo feature `charls` | x | +//! | JPEG-LS Lossy (Near-Lossless) | Cargo feature `charls` | x | //! | JPEG 2000 (Lossless Only) | Cargo feature `openjp2` or `openjpeg-sys` | x | //! | JPEG 2000 | Cargo feature `openjp2` or `openjpeg-sys` | x | //! | JPEG 2000 Part 2 Multi-component Image Compression (Lossless Only) | Cargo feature `openjp2` or `openjpeg-sys` | x | @@ -68,6 +70,10 @@ //! However, a native implementation might not always be available, //! or alternative implementations may be preferred: //! +//! - `charls` provides support for JPEG-LS +//! by linking to the CharLS reference implementation, +//! which is written in C++. +//! No alternative JPEG-LS implementations are available at the moment. //! - `openjpeg-sys` provides a binding to the OpenJPEG reference implementation, //! which is written in C and is statically linked. //! It may offer better performance than the pure Rust implementation,