Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transcoder API #408

Merged
merged 12 commits into from
Oct 15, 2023
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
toolchain: ${{ matrix.rust }}
cache: true
# test project with default + extra features
- run: cargo test --features image,ndarray,sop-class,rle
- run: cargo test --features image,ndarray,sop-class,rle,cli
# test dicom-pixeldata with gdcm-rs
- run: cargo test -p dicom-pixeldata --features gdcm
# test dicom-pixeldata without default features
Expand Down
17 changes: 9 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions pixeldata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ categories = ["multimedia::images"]
keywords = ["dicom"]
readme = "README.md"

[[bin]]
name = "dicom-transcode"
path = "src/bin/dicom-transcode.rs"
required-features = ["cli"]

[dependencies]
dicom-object = { path = "../object", version = "0.6.1" }
dicom-core = { path = "../core", version = "0.6.1" }
Expand All @@ -30,6 +35,15 @@ default-features = false
features = ["jpeg", "png", "pnm", "tiff", "webp", "bmp", "openexr"]
optional = true

[dependencies.clap]
version = "4.4.2"
optional = true
features = ["cargo", "derive"]

[dependencies.tracing-subscriber]
version = "0.3.17"
optional = true

[dev-dependencies]
rstest = "0.18.1"
dicom-test-files = "0.2.1"
Expand All @@ -44,5 +58,7 @@ native = ["dicom-transfer-syntax-registry/native"]
gdcm = ["gdcm-rs"]
rayon = ["dep:rayon", "image?/jpeg_rayon", "dicom-transfer-syntax-registry/rayon"]

cli = ["dep:clap", "dep:tracing-subscriber"]

[package.metadata.docs.rs]
features = ["image", "ndarray"]
169 changes: 169 additions & 0 deletions pixeldata/src/bin/dicom-transcode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//! A CLI tool for transcoding a DICOM file
//! to another transfer syntax.
use clap::Parser;
use dicom_dictionary_std::uids;
use dicom_encoding::{TransferSyntaxIndex, TransferSyntax};
use dicom_encoding::adapters::EncodeOptions;
use dicom_object::open_file;
use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
use snafu::{Report, Whatever, OptionExt};
use tracing::Level;
use std::path::PathBuf;
use dicom_pixeldata::Transcode;

/// Exit code for when an error emerged while reading the DICOM file.
const ERROR_READ: i32 = -2;
/// Exit code for when an error emerged while transcoding the file.
const ERROR_TRANSCODE: i32 = -3;
/// Exit code for when an error emerged while writing the file.
const ERROR_WRITE: i32 = -4;
/// Exit code for when an error emerged while writing the file.
const ERROR_OTHER: i32 = -128;

/// Transcode a DICOM file
#[derive(Debug, Parser)]
#[command(version)]
struct App {
file: PathBuf,
/// The output file (default is to change the extension to .new.dcm)
#[clap(short = 'o', long = "output")]
output: Option<PathBuf>,

/// The encoding quality (from 0 to 100)
#[clap(long = "quality")]
quality: Option<u8>,
/// The encoding effort (from 0 to 100)
#[clap(long = "effort")]
effort: Option<u8>,

/// Target transfer syntax
#[clap(flatten)]
target_ts: TargetTransferSyntax,

/// Verbose mode
#[clap(short = 'v', long = "verbose")]
verbose: bool,
}

/// Specifier for the target transfer syntax
#[derive(Debug, Parser)]

#[group(required = true, multiple = false)]
struct TargetTransferSyntax {
/// Transcode to the Transfer Syntax indicated by UID
#[clap(long = "ts")]
ts: Option<String>,

/// Transcode to Explicit VR Little Endian
#[clap(long = "expl-vr-le")]
explicit_vr_le: bool,

/// Transcode to Implicit VR Little Endian
#[clap(long = "impl-vr-le")]
implicit_vr_le: bool,

/// Transcode to JPEG baseline (8-bit)
#[clap(long = "jpeg-baseline")]
jpeg_baseline: bool,
}

impl TargetTransferSyntax {
fn resolve(&self) -> Result<&'static TransferSyntax, Whatever> {

// explicit VR little endian
if self.explicit_vr_le {
return Ok(TransferSyntaxRegistry.get(uids::EXPLICIT_VR_LITTLE_ENDIAN)
.expect("Explicit VR Little Endian is missing???"));
}

// implicit VR little endian
if self.implicit_vr_le {
return Ok(TransferSyntaxRegistry.get(uids::IMPLICIT_VR_LITTLE_ENDIAN)
.expect("Implicit VR Little Endian is missing???"));
}

// JPEG baseline
if self.jpeg_baseline {
return TransferSyntaxRegistry.get(uids::JPEG_BASELINE8_BIT)
.whatever_context("Missing specifier for JPEG Baseline (8-bit)");
}

// by TS UID
let Some(ts) = &self.ts else {
snafu::whatever!("No target transfer syntax specified");
};

TransferSyntaxRegistry.get(ts)
.whatever_context("Unknown transfer syntax")
}
}


fn main() {
run().unwrap_or_else(|e| {
eprintln!("{}", Report::from_error(e));
std::process::exit(ERROR_OTHER);
});
}

fn run() -> Result<(), Whatever> {
let App {
file,
output,
quality,
effort,
target_ts,
verbose,
} = App::parse();

tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(if verbose { Level::DEBUG } else { Level::INFO })
.finish(),
)
.unwrap_or_else(|e| {
eprintln!("{}", snafu::Report::from_error(e));
});

let output = output.unwrap_or_else(|| {
let mut file = file.clone();
file.set_extension("new.dcm");
file
});

let mut obj = open_file(file).unwrap_or_else(|e| {
eprintln!("{}", Report::from_error(e));
std::process::exit(ERROR_READ);
});

// lookup transfer syntax
let ts = target_ts.resolve()?;

let mut options = EncodeOptions::default();
options.quality = quality;
options.effort = effort;

obj.transcode_with_options(ts, options).unwrap_or_else(|e| {
eprintln!("{}", Report::from_error(e));
std::process::exit(ERROR_TRANSCODE);
});

// write to file
obj.write_to_file(output).unwrap_or_else(|e| {
eprintln!("{}", Report::from_error(e));
std::process::exit(ERROR_WRITE);
});

Ok(())
}

#[cfg(test)]
mod tests {
use crate::App;
use clap::CommandFactory;

#[test]
fn verify_cli() {
App::command().debug_assert();
}
}
2 changes: 2 additions & 0 deletions pixeldata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,15 @@ pub use ndarray;

mod attribute;
mod lut;
mod transcode;

pub mod encapsulation;
pub(crate) mod transform;

// re-exports
pub use attribute::{PhotometricInterpretation, PixelRepresentation, PlanarConfiguration};
pub use lut::{CreateLutError, Lut};
pub use transcode::{Error as TranscodeError, Result as TranscodeResult, Transcode};
pub use transform::{Rescale, VoiLutFunction, WindowLevel, WindowLevelTransform};

#[cfg(feature = "gdcm")]
Expand Down
Loading
Loading