diff --git a/fromimage/README.md b/fromimage/README.md index 2aa9140e3..e74ddeb71 100644 --- a/fromimage/README.md +++ b/fromimage/README.md @@ -11,21 +11,51 @@ This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. ## Usage ```none -dicom-fromimage 0.1.0 -Convert and replace a DICOM file's image with another image +Usage: dicom-fromimage [OPTIONS] + +Arguments: + Path to the base DICOM file to read + Path to the image file to replace the DICOM file + +Options: + -o, --out + Path to the output image (default is to replace input extension with `.new.dcm`) + --transfer-syntax + Override the transfer syntax UID + --encapsulate + Encapsulate the image file raw data in a fragment sequence instead of writing native pixel data + --retain-implementation + Retain the implementation class UID and version name from base DICOM + -v, --verbose + Print more information about the image and the output file + -h, --help + Print help + -V, --version + Print version +``` + +### Example -USAGE: - dicom-fromimage.exe [FLAGS] [OPTIONS] +Given a template DICOM file `base.dcm`, +replace the image data with the image in `image.png`: -FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -v, --verbose Print more information about the image and the output file +```none +dicom-fromimage base.dcm image.png -o image.dcm +``` -OPTIONS: - -o, --out Path to the output image (default is to replace input extension with `.new.dcm`) +This will read the image file in the second argument +and save it as native pixel data in Explicit VR Little Endian to `image.dcm`. -ARGS: - Path to the base DICOM file to read - Path to the image file to replace the DICOM file +You can also encapsulate the image file into a pixel data fragment, +without converting to native pixel data. +This allows you to create a DICOM file in JPEG baseline: + +```none +dicom-fromimage base.dcm image.jpg --transfer-syntax 1.2.840.10008.1.2.4.50 --encapsulate -o image.dcm ``` + +**Note:** `--transfer-syntax` is just a UID override, +it will not automatically transcode the pixel data +to conform to the given transfer syntax. +To transcode files between transfer syntaxes, +see [`dicom-transcode`](https://github.com/Enet4/dicom-rs/tree/master/pixeldata). diff --git a/fromimage/src/main.rs b/fromimage/src/main.rs index e05ed46da..b9bec3bcd 100644 --- a/fromimage/src/main.rs +++ b/fromimage/src/main.rs @@ -16,9 +16,15 @@ use std::path::PathBuf; use clap::Parser; -use dicom_core::{value::PrimitiveValue, DataElement, VR}; +use dicom_core::{ + value::{PixelFragmentSequence, PrimitiveValue}, + DataElement, DicomValue, VR, +}; use dicom_dictionary_std::tags; -use dicom_object::{open_file, FileMetaTableBuilder}; +use dicom_object::{open_file, DefaultDicomObject, FileMetaTableBuilder}; +use image::DynamicImage; + +type Result = std::result::Result; /// Convert and replace a DICOM file's image with another image #[derive(Debug, Parser)] @@ -32,6 +38,13 @@ struct App { /// (default is to replace input extension with `.new.dcm`) #[arg(short = 'o', long = "out")] output: Option, + /// Override the transfer syntax UID (pixel data is not converted) + #[arg(long = "transfer-syntax", alias = "ts")] + transfer_syntax: Option, + /// Encapsulate the image file raw data in a fragment sequence + /// instead of writing native pixel data + #[arg(long)] + encapsulate: bool, /// Retain the implementation class UID and version name from base DICOM #[arg(long)] retain_implementation: bool, @@ -50,6 +63,8 @@ fn main() { dcm_file, img_file, output, + encapsulate, + transfer_syntax, retain_implementation, verbose, } = App::parse(); @@ -65,11 +80,147 @@ fn main() { std::process::exit(-1); }); - let img = image::open(img_file).unwrap_or_else(|e| { + if encapsulate { + inject_encapsulated(&mut obj, img_file, verbose) + } else { + inject_image(&mut obj, img_file, verbose) + } + .unwrap_or_else(|e| { + tracing::error!("{}", snafu::Report::from_error(e)); + std::process::exit(-2); + }); + + let class_uid = obj.meta().media_storage_sop_class_uid.clone(); + + let mut meta_builder = FileMetaTableBuilder::new() + // currently the tool will always decode the image's pixel data, + // so encode it as Explicit VR Little Endian + .transfer_syntax("1.2.840.10008.1.2.1") + .media_storage_sop_class_uid(class_uid); + + if let Some(ts) = transfer_syntax { + meta_builder = meta_builder.transfer_syntax(ts); + } + + // recover implementation class UID and version name from base object + if retain_implementation { + let implementation_class_uid = &obj.meta().implementation_class_uid; + meta_builder = meta_builder.implementation_class_uid(implementation_class_uid); + + if let Some(implementation_version_name) = obj.meta().implementation_version_name.as_ref() { + meta_builder = meta_builder.implementation_version_name(implementation_version_name); + } + } + + let obj = obj + .into_inner() + .with_meta(meta_builder) + .unwrap_or_else(|e| { + tracing::error!("{}", snafu::Report::from_error(e)); + std::process::exit(-3); + }); + + obj.write_to_file(&output).unwrap_or_else(|e| { + tracing::error!("{}", snafu::Report::from_error(e)); + std::process::exit(-4); + }); + + if verbose { + println!("DICOM file saved to {}", output.display()); + } +} + +fn inject_image(obj: &mut DefaultDicomObject, img_file: PathBuf, verbose: bool) -> Result<()> { + let image_reader = image::ImageReader::open(img_file).unwrap_or_else(|e| { + tracing::error!("{}", snafu::Report::from_error(e)); + std::process::exit(-1); + }); + + let img = image_reader.decode().unwrap_or_else(|e| { + tracing::error!("{}", snafu::Report::from_error(e)); + std::process::exit(-1); + }); + + let color = img.color(); + + let bits_stored: u16 = match color { + image::ColorType::L8 => 8, + image::ColorType::L16 => 16, + image::ColorType::Rgb8 => 8, + image::ColorType::Rgb16 => 16, + _ => { + eprintln!("Unsupported image format {:?}", color); + std::process::exit(-2); + } + }; + + update_from_img(obj, &img, verbose); + + for tag in [ + tags::NUMBER_OF_FRAMES, + tags::PIXEL_ASPECT_RATIO, + tags::SMALLEST_IMAGE_PIXEL_VALUE, + tags::LARGEST_IMAGE_PIXEL_VALUE, + tags::PIXEL_PADDING_RANGE_LIMIT, + tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DATA, + tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR, + tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA, + tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR, + tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA, + tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR, + tags::ICC_PROFILE, + tags::COLOR_SPACE, + tags::PIXEL_DATA_PROVIDER_URL, + tags::EXTENDED_OFFSET_TABLE, + tags::EXTENDED_OFFSET_TABLE_LENGTHS, + ] { + obj.remove_element(tag); + } + + let pixeldata = img.into_bytes(); + + obj.put(DataElement::new( + tags::PIXEL_DATA, + if bits_stored == 8 { VR::OB } else { VR::OW }, + PrimitiveValue::from(pixeldata), + )); + + Ok(()) +} + +fn inject_encapsulated( + dcm: &mut DefaultDicomObject, + img_file: PathBuf, + verbose: bool, +) -> Result<()> { + let image_reader = image::ImageReader::open(&img_file).unwrap_or_else(|e| { tracing::error!("{}", snafu::Report::from_error(e)); std::process::exit(-1); }); + // collect img file data + let all_data = std::fs::read(img_file).unwrap_or_else(|e| { + tracing::error!("{}", snafu::Report::from_error(e)); + std::process::exit(-2); + }); + + if let Ok(img) = image_reader.decode() { + // insert attributes but not pixel data + + update_from_img(&mut *dcm, &img, verbose); + } + + // insert pixel data in a sequence + dcm.put(DataElement::new( + tags::PIXEL_DATA, + VR::OB, + DicomValue::PixelSequence(PixelFragmentSequence::new_fragments(vec![all_data])), + )); + + Ok(()) +} + +fn update_from_img(obj: &mut DefaultDicomObject, img: &DynamicImage, verbose: bool) { let width = img.width(); let height = img.height(); let color = img.color(); @@ -85,8 +236,6 @@ fn main() { } }; - let pixeldata = img.into_bytes(); - if verbose { println!("{}x{} {:?} image", width, height, color); } @@ -151,68 +300,6 @@ fn main() { VR::US, PrimitiveValue::from(0_u16), )); - - for tag in [ - tags::NUMBER_OF_FRAMES, - tags::PIXEL_ASPECT_RATIO, - tags::SMALLEST_IMAGE_PIXEL_VALUE, - tags::LARGEST_IMAGE_PIXEL_VALUE, - tags::PIXEL_PADDING_RANGE_LIMIT, - tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DATA, - tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR, - tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA, - tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR, - tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA, - tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR, - tags::ICC_PROFILE, - tags::COLOR_SPACE, - tags::PIXEL_DATA_PROVIDER_URL, - tags::EXTENDED_OFFSET_TABLE, - tags::EXTENDED_OFFSET_TABLE_LENGTHS, - ] { - obj.remove_element(tag); - } - - obj.put(DataElement::new( - tags::PIXEL_DATA, - if bits_stored == 8 { VR::OB } else { VR::OW }, - PrimitiveValue::from(pixeldata), - )); - - let class_uid = obj.meta().media_storage_sop_class_uid.clone(); - - let mut meta_builder = FileMetaTableBuilder::new() - // currently the tool will always decode the image's pixel data, - // so encode it as Explicit VR Little Endian - .transfer_syntax("1.2.840.10008.1.2.1") - .media_storage_sop_class_uid(class_uid); - - // recover implementation class UID and version name from base object - if retain_implementation { - let implementation_class_uid = &obj.meta().implementation_class_uid; - meta_builder = meta_builder.implementation_class_uid(implementation_class_uid); - - if let Some(implementation_version_name) = obj.meta().implementation_version_name.as_ref() { - meta_builder = meta_builder.implementation_version_name(implementation_version_name); - } - } - - let obj = obj - .into_inner() - .with_meta(meta_builder) - .unwrap_or_else(|e| { - tracing::error!("{}", snafu::Report::from_error(e)); - std::process::exit(-3); - }); - - obj.write_to_file(&output).unwrap_or_else(|e| { - tracing::error!("{}", snafu::Report::from_error(e)); - std::process::exit(-4); - }); - - if verbose { - println!("DICOM file saved to {}", output.display()); - } } #[cfg(test)] diff --git a/pixeldata/README.md b/pixeldata/README.md index 3eb6cdda9..3c47e7d94 100644 --- a/pixeldata/README.md +++ b/pixeldata/README.md @@ -9,3 +9,30 @@ and is responsible for decoding pixel data elements into images or multi-dimensional arrays. This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. + +## Binary + +`dicom-pixeldata` also offers the `dicom-transcode` command-line tool +(enable Cargo feature `cli`). +You can use it to transcode a DICOM file to another transfer syntax, +transforming pixel data along the way. + +```none +Usage: dicom-transcode [OPTIONS] <--ts |--expl-vr-le|--impl-vr-le|--jpeg-baseline> + +Arguments: + + +Options: + -o, --output The output file (default is to change the extension to .new.dcm) + --quality The encoding quality (from 0 to 100) + --effort The encoding effort (from 0 to 100) + --ts Transcode to the Transfer Syntax indicated by UID + --expl-vr-le Transcode to Explicit VR Little Endian + --impl-vr-le Transcode to Implicit VR Little Endian + --jpeg-baseline Transcode to JPEG baseline (8-bit) + --retain-implementation Retain the original implementation class UID and version name + -v, --verbose Verbose mode + -h, --help Print help + -V, --version Print version +```