Skip to content

Commit 4f2c0d8

Browse files
authored
Merge pull request #534 from dougyau/jpegls-decode
Add support for JPEG-LS decoding
2 parents de7dc58 + e593b97 commit 4f2c0d8

File tree

9 files changed

+179
-7
lines changed

9 files changed

+179
-7
lines changed

.github/workflows/rust.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ jobs:
2727
- run: cargo test --features image,ndarray,sop-class,rle,cli
2828
# test dicom-pixeldata with openjp2
2929
- run: cargo test -p dicom-pixeldata --features openjp2
30-
# test dicom-pixeldata with openjpeg-sys
31-
- run: cargo test -p dicom-pixeldata --features openjpeg-sys
30+
# test dicom-pixeldata with openjpeg-sys and charls
31+
- run: cargo test -p dicom-pixeldata --features openjpeg-sys,charls
3232
# test dicom-pixeldata with gdcm-rs
3333
- run: cargo test -p dicom-pixeldata --features gdcm
3434
# test dicom-pixeldata without default features

Cargo.lock

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pixeldata/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ rle = ["dicom-transfer-syntax-registry/rle"]
6464
openjpeg-sys = ["dicom-transfer-syntax-registry/openjpeg-sys"]
6565
# JPEG 2000 decoding via Rust port of OpenJPEG
6666
openjp2 = ["dicom-transfer-syntax-registry/openjp2"]
67+
# JpegLS via CharLS
68+
charls = ["dicom-transfer-syntax-registry/charls"]
6769

6870
# replace pixel data decoding to use GDCM
6971
gdcm = ["gdcm-rs"]

pixeldata/src/lib.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -2714,11 +2714,12 @@ mod tests {
27142714
case("pydicom/JPEG2000.dcm", 1)
27152715
)]
27162716
//
2717-
// jpeg-ls encoding not supported
2718-
#[should_panic(expected = "UnsupportedTransferSyntax { ts: \"1.2.840.10008.1.2.4.80\"")]
2719-
#[case("pydicom/emri_small_jpeg_ls_lossless.dcm", 10)]
2720-
#[should_panic(expected = "UnsupportedTransferSyntax { ts: \"1.2.840.10008.1.2.4.80\"")]
2721-
#[case("pydicom/MR_small_jpeg_ls_lossless.dcm", 1)]
2717+
// jpeg-ls encoding
2718+
#[cfg_attr(
2719+
feature = "charls",
2720+
case("pydicom/emri_small_jpeg_ls_lossless.dcm", 10)
2721+
)]
2722+
#[cfg_attr(feature = "charls", case("pydicom/MR_small_jpeg_ls_lossless.dcm", 1))]
27222723
//
27232724
// sample precision of 12 not supported yet
27242725
#[should_panic(expected = "Unsupported(SamplePrecision(12))")]
@@ -2775,6 +2776,8 @@ mod tests {
27752776
#[case("pydicom/SC_rgb_rle_2frame.dcm", 0)]
27762777
#[case("pydicom/SC_rgb_rle_2frame.dcm", 1)]
27772778
#[case("pydicom/JPEG2000_UNC.dcm", 0)]
2779+
#[cfg_attr(feature = "charls", case("pydicom/emri_small_jpeg_ls_lossless.dcm", 5))]
2780+
#[cfg_attr(feature = "charls", case("pydicom/MR_small_jpeg_ls_lossless.dcm", 0))]
27782781
fn test_decode_pixel_data_individual_frames(#[case] value: &str, #[case] frame: u32) {
27792782
use crate::PixelDecoder as _;
27802783
use std::path::Path;

transfer-syntax-registry/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ simd = ["jpeg-encoder?/simd"]
3535
# conflicts with `openjp2`
3636
openjpeg-sys = ["dep:jpeg2k", "jpeg2k/openjpeg-sys"]
3737

38+
# jpeg LS support via charls bindings
39+
charls = ["dep:charls"]
40+
3841
# build OpenJPEG with multithreading,
3942
# implies "rayon"
4043
openjpeg-sys-threads = ["rayon", "jpeg2k?/threads"]
@@ -59,6 +62,11 @@ optional = true
5962
version = "0.6"
6063
optional = true
6164

65+
[dependencies.charls]
66+
version = "0.3"
67+
optional = true
68+
features = ["static"]
69+
6270
[package.metadata.docs.rs]
6371
features = ["native"]
6472

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Support for JPEG-LS image decoding.
2+
3+
use charls::CharLS;
4+
use dicom_encoding::adapters::{decode_error, DecodeResult, PixelDataObject, PixelDataReader};
5+
use dicom_encoding::snafu::prelude::*;
6+
use std::borrow::Cow;
7+
8+
/// Pixel data adapter for JPEG-LS transfer syntax.
9+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
10+
pub struct JpegLSAdapter;
11+
12+
impl PixelDataReader for JpegLSAdapter {
13+
/// Decode a single frame in JPEG-LS from a DICOM object.
14+
fn decode_frame(
15+
&self,
16+
src: &dyn PixelDataObject,
17+
frame: u32,
18+
dst: &mut Vec<u8>,
19+
) -> DecodeResult<()> {
20+
let bits_allocated = src
21+
.bits_allocated()
22+
.context(decode_error::MissingAttributeSnafu {
23+
name: "BitsAllocated",
24+
})?;
25+
26+
ensure_whatever!(
27+
bits_allocated == 8 || bits_allocated == 16,
28+
"BitsAllocated other than 8 or 16 is not supported"
29+
);
30+
31+
let nr_frames = src.number_of_frames().unwrap_or(1) as usize;
32+
33+
ensure!(
34+
nr_frames > frame as usize,
35+
decode_error::FrameRangeOutOfBoundsSnafu
36+
);
37+
38+
let raw = src
39+
.raw_pixel_data()
40+
.whatever_context("Expected to have raw pixel data available")?;
41+
42+
let frame_data = if raw.fragments.len() == 1 || raw.fragments.len() == nr_frames {
43+
// assuming 1:1 frame-to-fragment mapping
44+
Cow::Borrowed(
45+
raw.fragments
46+
.get(frame as usize)
47+
.with_whatever_context(|| {
48+
format!("Missing fragment #{} for the frame requested", frame)
49+
})?,
50+
)
51+
} else {
52+
// Some embedded JPEGs might span multiple fragments.
53+
// In this case we look up the basic offset table
54+
// and gather all of the frame's fragments in a single vector.
55+
// Note: not the most efficient way to do this,
56+
// consider optimizing later with byte chunk readers
57+
let base_offset = raw.offset_table.get(frame as usize).copied();
58+
let base_offset = if frame == 0 {
59+
base_offset.unwrap_or(0) as usize
60+
} else {
61+
base_offset
62+
.with_whatever_context(|| format!("Missing offset for frame #{}", frame))?
63+
as usize
64+
};
65+
let next_offset = raw.offset_table.get(frame as usize + 1);
66+
67+
let mut offset = 0;
68+
let mut fragments = Vec::new();
69+
for fragment in &raw.fragments {
70+
// include it
71+
if offset >= base_offset {
72+
fragments.extend_from_slice(fragment);
73+
}
74+
offset += fragment.len() + 8;
75+
if let Some(&next_offset) = next_offset {
76+
if offset >= next_offset as usize {
77+
// next fragment is for the next frame
78+
break;
79+
}
80+
}
81+
}
82+
83+
Cow::Owned(fragments)
84+
};
85+
86+
let mut decoded = CharLS::default()
87+
.decode(&frame_data)
88+
.map_err(|error| error.to_string())
89+
.with_whatever_context(|error| error.to_string())?;
90+
91+
dst.append(&mut decoded);
92+
93+
Ok(())
94+
}
95+
}

transfer-syntax-registry/src/adapters/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
pub mod jpeg;
2828
#[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))]
2929
pub mod jpeg2k;
30+
#[cfg(feature = "charls")]
31+
pub mod jpegls;
3032
#[cfg(feature = "rle")]
3133
pub mod rle_lossless;
3234

@@ -46,3 +48,8 @@ pub mod jpeg2k {}
4648
/// Enable the `rle` feature to use this module.
4749
#[cfg(not(feature = "rle"))]
4850
pub mod rle {}
51+
52+
/// **Note:** This module is a stub.
53+
/// Enable the `charls` feature to use this module.
54+
#[cfg(not(feature = "charls"))]
55+
pub mod jpegls {}

transfer-syntax-registry/src/entries.rs

+32
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ use dicom_encoding::NeverPixelAdapter;
3434
use crate::adapters::jpeg::JpegAdapter;
3535
#[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))]
3636
use crate::adapters::jpeg2k::Jpeg2000Adapter;
37+
#[cfg(feature = "charls")]
38+
use crate::adapters::jpegls::JpegLSAdapter;
3739
#[cfg(feature = "rle")]
3840
use crate::adapters::rle_lossless::RleLosslessAdapter;
3941

@@ -262,12 +264,42 @@ pub const JPEG_2000_PART2_MULTI_COMPONENT_IMAGE_COMPRESSION: Ts = create_ts_stub
262264

263265
// --- partially supported transfer syntaxes, pixel data encapsulation not supported ---
264266

267+
/// An alias for a transfer syntax specifier with [`JpegLSAdapter`]
268+
#[cfg(feature = "charls")]
269+
type JpegLSTs<R = JpegLSAdapter, W = NeverPixelAdapter> = TransferSyntax<NeverAdapter, R, W>;
270+
271+
/// Create JPEG-LS TransferSyntax
272+
#[cfg(feature = "charls")]
273+
const fn create_ts_jpegls(uid: &'static str, name: &'static str) -> JpegLSTs {
274+
TransferSyntax::new_ele(
275+
uid,
276+
name,
277+
Codec::EncapsulatedPixelData(Some(JpegLSAdapter), None),
278+
)
279+
}
280+
281+
/// **Decoder Implementation:** JPEG-LS Lossless Image Compression
282+
#[cfg(feature = "charls")]
283+
pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls(
284+
"1.2.840.10008.1.2.4.80",
285+
"JPEG-LS Lossless Image Compression",
286+
);
287+
265288
/// **Stub descriptor:** JPEG-LS Lossless Image Compression
289+
#[cfg(not(feature = "charls"))]
266290
pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: Ts = create_ts_stub(
267291
"1.2.840.10008.1.2.4.80",
268292
"JPEG-LS Lossless Image Compression",
269293
);
294+
/// **Decoder Implementation:** JPEG-LS Lossy (Near-Lossless) Image Compression
295+
#[cfg(feature = "charls")]
296+
pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls(
297+
"1.2.840.10008.1.2.4.81",
298+
"JPEG-LS Lossy (Near-Lossless) Image Compression",
299+
);
300+
270301
/// **Stub descriptor:** JPEG-LS Lossy (Near-Lossless) Image Compression
302+
#[cfg(not(feature = "charls"))]
271303
pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: Ts = create_ts_stub(
272304
"1.2.840.10008.1.2.4.81",
273305
"JPEG-LS Lossy (Near-Lossless) Image Compression",

transfer-syntax-registry/src/lib.rs

+6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
//! | JPEG Extended (Process 2 & 4) | Cargo feature `jpeg` | x |
5757
//! | JPEG Lossless, Non-Hierarchical (Process 14) | Cargo feature `jpeg` | x |
5858
//! | JPEG Lossless, Non-Hierarchical, First-Order Prediction (Process 14 [Selection Value 1]) | Cargo feature `jpeg` | x |
59+
//! | JPEG-LS Lossless | Cargo feature `charls` | x |
60+
//! | JPEG-LS Lossy (Near-Lossless) | Cargo feature `charls` | x |
5961
//! | JPEG 2000 (Lossless Only) | Cargo feature `openjp2` or `openjpeg-sys` | x |
6062
//! | JPEG 2000 | Cargo feature `openjp2` or `openjpeg-sys` | x |
6163
//! | JPEG 2000 Part 2 Multi-component Image Compression (Lossless Only) | Cargo feature `openjp2` or `openjpeg-sys` | x |
@@ -68,6 +70,10 @@
6870
//! However, a native implementation might not always be available,
6971
//! or alternative implementations may be preferred:
7072
//!
73+
//! - `charls` provides support for JPEG-LS
74+
//! by linking to the CharLS reference implementation,
75+
//! which is written in C++.
76+
//! No alternative JPEG-LS implementations are available at the moment.
7177
//! - `openjpeg-sys` provides a binding to the OpenJPEG reference implementation,
7278
//! which is written in C and is statically linked.
7379
//! It may offer better performance than the pure Rust implementation,

0 commit comments

Comments
 (0)