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

[dicom-dump] Add json output format #440

Merged
merged 11 commits into from
Aug 13, 2024
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions dump/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ dicom-object = { path = "../object/", version = "0.7.0" }
dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.7.0", default-features = false }
dicom-dictionary-std = { path = "../dictionary-std/", version = "0.7.0" }
owo-colors = { version = "4.0.0-rc.1", features = ["supports-colors"] }
serde_json = "1.0.108"
terminal_size = "0.3.0"
dicom-json = { version = "0.7.0", path = "../json" }
107 changes: 77 additions & 30 deletions dump/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
//! options.width(100).dump_file(&obj)?;
//! # Result::<(), Box<dyn std::error::Error>>::Ok(())
//! ```
#[cfg(feature = "cli")]
use clap::ValueEnum;
#[cfg(feature = "sop-class")]
use dicom_core::dictionary::UidDictionary;
use dicom_core::dictionary::{DataDictionary, DataDictionaryEntry};
Expand All @@ -41,6 +43,7 @@ use dicom_core::VR;
#[cfg(feature = "sop-class")]
use dicom_dictionary_std::StandardSopClassDictionary;
use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
use dicom_json::DicomJson;
use dicom_object::mem::{InMemDicomObject, InMemElement};
use dicom_object::{FileDicomObject, FileMetaTable, StandardDataDictionary};
use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
Expand All @@ -50,12 +53,11 @@ use std::fmt::{self, Display, Formatter};
use std::io::{stdout, Result as IoResult, Write};
use std::str::FromStr;

/// An enum of all supported output formats for dumping DICOM data.
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "cli", derive(ValueEnum))]
pub enum DumpFormat {
/// The main DICOM dump format adopted by the project.
///
/// Text dump of DICOM file
///
/// It is primarily designed to be human readable,
/// although its output can be used to recover the original object
/// in its uncut form (no limit width).
Expand All @@ -64,15 +66,13 @@ pub enum DumpFormat {
///
/// Note that this format is not stabilized,
/// and may change with subsequent versions of the crate.
Main,
#[default]
Text,
/// DICOM part 18 chapter F JSON format,
/// provided via [`dicom_json`]
Json,
}

/// The [main output format](DumpFormat::Main) is used by default.
impl Default for DumpFormat {
fn default() -> Self {
DumpFormat::Main
}
}

/// Options and flags to configure how to dump a DICOM file or object.
///
Expand Down Expand Up @@ -227,14 +227,23 @@ impl DumpOptions {
} else {
(true, true)
};
match self.format {
DumpFormat::Text => {
meta_dump(&mut to, meta, if no_limit { u32::MAX } else { width })?;

meta_dump(&mut to, meta, if no_limit { u32::MAX } else { width })?;
writeln!(to, "{:-<58}", "")?;

writeln!(to, "{:-<58}", "")?;
dump(&mut to, obj, width, 0, no_text_limit, no_limit)?;

dump(&mut to, obj, width, 0, no_text_limit, no_limit)?;
Ok(())
},
DumpFormat::Json => {
let json_obj = DicomJson::from(obj);
serde_json::to_writer_pretty(stdout(), &json_obj)?;
Ok(())
}
}

Ok(())
}

/// Dump the contents of a DICOM object to standard output.
Expand Down Expand Up @@ -264,24 +273,33 @@ impl DumpOptions {
where
D: DataDictionary,
{
match (self.color, to_stdout) {
(ColorMode::Never, _) => colored::control::set_override(false),
(ColorMode::Always, _) => colored::control::set_override(true),
(ColorMode::Auto, false) => colored::control::set_override(false),
(ColorMode::Auto, true) => colored::control::unset_override(),
}
match self.format {
DumpFormat::Text => {
match (self.color, to_stdout) {
(ColorMode::Never, _) => colored::control::set_override(false),
(ColorMode::Always, _) => colored::control::set_override(true),
(ColorMode::Auto, false) => colored::control::set_override(false),
(ColorMode::Auto, true) => colored::control::unset_override(),
}

let width = determine_width(self.width);
let width = determine_width(self.width);

let (no_text_limit, no_limit) = if to_stdout {
(self.no_text_limit, self.no_limit)
} else {
(true, true)
};
let (no_text_limit, no_limit) = if to_stdout {
(self.no_text_limit, self.no_limit)
} else {
(true, true)
};

dump(&mut to, obj, width, 0, no_text_limit, no_limit)?;
dump(&mut to, obj, width, 0, no_text_limit, no_limit)?;

Ok(())
Ok(())
}
DumpFormat::Json => {
let json_obj = DicomJson::from(obj);
serde_json::to_writer_pretty(to, &json_obj)?;
Ok(())
}
}
}
}

Expand Down Expand Up @@ -1096,4 +1114,33 @@ mod tests {
assert_eq!(value, expected.3);
}
}

#[test]
fn dump_json() {
// create object
let obj = InMemDicomObject::from_element_iter(vec![DataElement::new(
tags::SOP_INSTANCE_UID,
VR::UI,
PrimitiveValue::from("1.2.888.123"),
)]);

let mut out = Vec::new();
DumpOptions::new()
.color_mode(ColorMode::Never)
.format(crate::DumpFormat::Json)
.dump_object_to(&mut out, &obj)
.unwrap();

let json = std::str::from_utf8(&out).expect("output is not valid UTF-8");
assert_eq!(
json,
r#"{
"00080018": {
"vr": "UI",
"Value": [
"1.2.888.123"
]
}
}"#);
}
}
33 changes: 26 additions & 7 deletions dump/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
//! A CLI tool for inspecting the contents of a DICOM file
//! by printing it in a human readable format.
use clap::Parser;
use dicom_dump::{ColorMode, DumpOptions};
use dicom_dump::{ColorMode, DumpOptions, DumpFormat};
use dicom_object::open_file;
use snafu::{Report, Whatever};
use std::io::ErrorKind;
use std::io::{ErrorKind, IsTerminal};
use std::path::PathBuf;

/// 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 dumping the file.
const ERROR_PRINT: i32 = -3;


/// Dump the contents of DICOM files
#[derive(Debug, Parser)]
#[command(version)]
Expand All @@ -20,15 +21,21 @@ struct App {
#[clap(required = true)]
files: Vec<PathBuf>,
/// Print text values to the end
/// (limited to `width` by default)
/// (limited to `width` by default).
///
/// Does not apply if output is not a tty
/// or if output type is json
#[clap(long = "no-text-limit")]
no_text_limit: bool,
/// Print all values to the end
/// (implies `no_text_limit`, limited to `width` by default)
#[clap(long = "no-limit")]
no_limit: bool,
/// The width of the display
/// (default is to check automatically)
/// (default is to check automatically).
///
/// Does not apply if output is not a tty
/// or if output type is json
#[clap(short = 'w', long = "width")]
width: Option<u32>,
/// The color mode
Expand All @@ -37,6 +44,14 @@ struct App {
/// Fail if any errors are encountered
#[clap(long = "fail-first")]
fail_first: bool,
/// Output format
#[arg(value_enum)]
#[clap(short = 'f', long = "format", default_value = "text")]
format: DumpFormat
}

fn is_terminal() -> bool {
std::io::stdout().is_terminal()
}

fn main() {
Expand All @@ -54,6 +69,7 @@ fn run() -> Result<(), Whatever> {
width,
color,
fail_first,
format,
} = App::parse();

let width = width
Expand All @@ -63,14 +79,17 @@ fn run() -> Result<(), Whatever> {
let mut options = DumpOptions::new();
options
.no_text_limit(no_text_limit)
.no_limit(no_limit)
// No limit when output is not a terminal
.no_limit(if !is_terminal() { true } else {no_limit})
.width(width)
.color_mode(color);
.color_mode(color)
.format(format);
let fail_first = filenames.len() == 1 || fail_first;
let mut errors: i32 = 0;

for filename in &filenames {
println!("{}: ", filename.display());
// Write filename to stderr to make piping easier, i.e. dicom-dump -o json file.dcm | jq
eprintln!("{}: ", filename.display());
match open_file(filename) {
Err(e) => {
eprintln!("{}", Report::from_error(e));
Expand Down
12 changes: 5 additions & 7 deletions echoscu/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,13 @@ fn run() -> Result<(), Whatever> {
}

// msg ID response, should be equal to sent msg ID
let msg_id_elem = obj
let got_msg_id: u16 = obj
.element(tags::MESSAGE_ID_BEING_RESPONDED_TO)
.whatever_context("Could not retrieve Message ID from response")?;
.whatever_context("Could not retrieve Message ID from response")?
.to_int()
.whatever_context("Message ID is not a valid integer")?;

if message_id
!= msg_id_elem
.to_int()
.whatever_context("Message ID is not a valid integer")?
{
if message_id != got_msg_id {
whatever!("Message ID mismatch");
}
}
Expand Down
24 changes: 18 additions & 6 deletions json/src/ser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
use std::io::Write;

use crate::DicomJson;
use dicom_core::{header::Header, DicomValue, PrimitiveValue, Tag, VR};
use dicom_core::{header::Header, value::PixelFragmentSequence, DicomValue, PrimitiveValue, Tag, VR};
use dicom_dictionary_std::StandardDataDictionary;
use dicom_object::{mem::InMemElement, DefaultDicomObject, InMemDicomObject};
use serde::{
ser::{Error as _, SerializeMap},
ser::SerializeMap,
Serialize, Serializer,
};

Expand Down Expand Up @@ -219,10 +219,8 @@ impl<D> Serialize for DicomJson<&'_ InMemElement<D>> {
DicomValue::Sequence(seq) => {
serializer.serialize_entry("Value", &DicomJson(seq.items()))?;
}
DicomValue::PixelSequence(_seq) => {
return Err(S::Error::custom(
"serialization of encapsulated pixel data is not supported",
));
DicomValue::PixelSequence(seq) => {
serializer.serialize_entry("Value", &DicomJson(seq))?;
}
DicomValue::Primitive(PrimitiveValue::Empty) => {
// no-op
Expand Down Expand Up @@ -286,6 +284,20 @@ impl<D> Serialize for DicomJson<InMemElement<D>> {
}
}

impl Serialize for DicomJson<&PixelFragmentSequence<Vec<u8>>> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let offset_table = self.inner().offset_table();
let fragments = self.inner().fragments();
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("Offset Table", offset_table)?;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mixed feelings about this. :) It may be interesting for encapsulated pixel data serialization to be supported in some way, but it may also be surprising for this to exist as a non-standard extension provided specifically by this implementation.

I think we'd be able to serve both worlds if we did the following in this order:

  1. At dicom-dump, we can check whether Pixel Data is encapsulated, raise a warning that it will be ignored, and remove it from the dataset before the dump.
  2. Later on, we can make it possible to provide parameters to the serialization process, so that the decision of whether to serialize encapsulated pixel data or not can be explicitly stated by the API user or the dicom-dump user.
    • IIRC we don't quite have a way to do this yet. Intuitively I would make this a field of DicomJson, but since it is defined as a single item tuple, adding new fields is likely breaking change (which could probably be mitigated through some tricker, but a breaking change nevertheless).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I left the serialize implementation for PixelFragmentSequence for when/if we do want to support that, but I can remove if you don't want unused code

map.serialize_entry("Pixel Fragments", fragments)?;
map.end()
}
}

impl From<Tag> for DicomJson<Tag> {
fn from(value: Tag) -> Self {
Self(value)
Expand Down
Loading