Skip to content

Commit 6bf78a1

Browse files
committed
Support dicomweb fields without Value/uri/binary
1 parent 77cebc6 commit 6bf78a1

7 files changed

Lines changed: 50 additions & 14 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "nu_plugin_dcm"
33
version = "0.3.1"
44
license = "MIT"
5-
description = "A nushell plugin to parse Dicom files"
5+
description = "A nushell plugin to parse Dicom files and DICOMweb records"
66
homepage = "https://github.com/realcundo/nu_plugin_dcm"
77
repository = "https://github.com/realcundo/nu_plugin_dcm"
88
readme = "README.md"

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ Use `ls *.dcm | dcm`, `ls *.dcm | get name` or `ls *.dcm | select name type | dc
88
A [nushell](https://www.nushell.sh/) plugin to parse [DICOM](https://en.wikipedia.org/wiki/DICOM) objects.
99

1010
This plugin is in the early stage of the development. It is usable but it might not be able to cope
11-
with all DICOM objects or DicomWeb inputs. See [Known limitations for details](#known-limitations).
11+
with all DICOM objects or DICOMweb records. See [Known limitations for details](#known-limitations).
1212

1313
I'm still trying to figure out what is the most useful way of using this plugin. Please feel free to try it out,
1414
send feedback in [Discussions](https://github.com/realcundo/nu_plugin_dcm/discussions) or report problems in [Issues](https://github.com/realcundo/nu_plugin_dcm/issues).
1515

1616
## Usage
1717
`dcm` plugin reads its input from single values, or from list of values:
18-
- `dcm`: expects a string/filename, file record (must contain `name` and `type`), dicomweb record, or binary DICOM data
18+
- `dcm`: expects a string/filename, file record (must contain `name` and `type`), DICOMweb record, or binary DICOM data
1919
- `ls *.dcm | dcm`: process a list of files, resulting in a list of dicom records
2020
- `ls *.dcm | select name type | dcm`: process a list of files specified by their filename, resulting in a list of dicom records
2121
- `open --raw file.dcm | into binary | dcm`: process a binary stream, resulting in a dicom record
@@ -40,8 +40,8 @@ See Examples for more details.
4040
```
4141

4242
Without `into binary`, `dcm` would see a list of strings, assuming it's a list of filenames.
43-
- For dicomweb inputs, only the first of "Alphabetic", "Ideographic", "Phonetic" Patient Names is extracted.
44-
- For dicomweb inputs, `BulkDataURI` and `InlineBinary` are not extracted and `nothing` is returned as their values.
43+
- For DICOMweb inputs, only the first of "Alphabetic", "Ideographic", "Phonetic" Patient Names is extracted.
44+
- For DICOMweb inputs, `BulkDataURI` and `InlineBinary` are not extracted and `nothing` is returned as their values.
4545

4646

4747
## Examples

src/dicomweb.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ pub fn is_dicom_record(record: &Record) -> bool {
3333
// Check https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.2.html
3434
if let Ok(potential_record) = potential_value.as_record() {
3535
potential_tag.len() == 8
36+
// the tag must be a valid hex string
3637
&& potential_tag
3738
.chars()
3839
.all(|c| c.is_ascii_hexdigit())
40+
// must contain "vr"
3941
&& potential_record.contains("vr")
40-
&& (potential_record.contains("Value") || potential_record.contains("BulkDataURI") || potential_record.contains("InlineBinary"))
42+
// tags must be one of the following
43+
&& (potential_record
44+
.iter()
45+
.all(|(key, _value)| key == "Value" || key == "BulkDataURI" || key == "InlineBinary" || key == "vr"))
4146
} else {
4247
false
4348
}
@@ -143,8 +148,8 @@ impl DicomWebDump<'_, '_> {
143148
let value = match record.get("Value") {
144149
Some(value) => value,
145150
None => {
146-
// don't return error if BulkDataURI/InlineBinary exist
147-
if record.contains("BulkDataURI") || record.contains("InlineBinary") {
151+
// don't return error if BulkDataURI/InlineBinary exist or if no other fields are present (just having "vr" with nothing else is valid)
152+
if record.len() == 1 || record.contains("BulkDataURI") || record.contains("InlineBinary") {
148153
return Ok(Value::nothing(record_span));
149154
} else {
150155
return Err(DicomWebError::MissingRequiredColumn { column: "Value|BulkDataURI|InlineBinary", span: record_span });

src/plugin.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ impl DcmPluginCommand {
258258
}
259259

260260
// Output generic error
261-
Err(LabeledError::new("Cannot process records directly, unless they are DicomWeb records")
262-
.with_label("For files, select file name or binary data from the record before passing it to dcm", *internal_span))
261+
Err(LabeledError::new("Cannot process records directly, unless they are Fila or DicomWeb records")
262+
.with_label("For files, select file name, binary data, or use records with `name` and `type`", *internal_span))
263263
}
264264
Value::Binary { val, internal_span, .. } => {
265265
let cursor = Cursor::new(val);

tests/assets/dicomweb-example.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,7 @@
119119
"Value": ["ISO_IR 192"]
120120
},
121121
"00080020": {
122-
"vr": "DT",
123-
"Value": ["20130309"]
122+
"vr": "DT"
124123
},
125124
"00080030": {
126125
"vr": "TM",
@@ -136,7 +135,7 @@
136135
},
137136
"00080061": {
138137
"vr": "CS",
139-
"Value": ["CT", "PET"]
138+
"Value": ["CT", "MG"]
140139
},
141140
"00080090": {
142141
"vr": "PN",

tests/dicomweb.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use nu_protocol::Span;
2-
use test_utils::{get_string_by_cell_path, setup_plugin_for_test};
2+
use test_utils::{assert_nothing_by_cell_path, get_string_by_cell_path, get_string_list_by_cell_path, setup_plugin_for_test};
33

44
mod test_utils;
55

@@ -12,9 +12,19 @@ fn read_dicomweb_list() -> Result<(), nu_protocol::ShellError> {
1212
let result = plugin_test.eval("open dicomweb-example.json | dcm")?;
1313
let result = result.into_value(TEST_SPAN)?;
1414

15+
assert_eq!(
16+
result
17+
.as_list()
18+
.unwrap()
19+
.len(),
20+
2
21+
);
22+
1523
{
1624
assert_eq!(get_string_by_cell_path(&result, "0.StudyInstanceUID"), "1.2.392.200036.9116.2.2.2.1762893313.1029997326.945873");
25+
assert_eq!(get_string_list_by_cell_path(&result, "0.ModalitiesInStudy"), vec!["CT".to_string(), "PET".to_string()]);
1726
assert_eq!(get_string_by_cell_path(&result, "0.PatientName"), "Wang^XiaoDong");
27+
assert_eq!(get_string_by_cell_path(&result, "0.StudyDate"), "20130409");
1828
assert_eq!(get_string_by_cell_path(&result, "0.OtherPatientIDsSequence.0.PatientID"), "54321");
1929
assert_eq!(get_string_by_cell_path(&result, "0.OtherPatientIDsSequence.0.IssuerOfPatientID"), "Hospital B");
2030
assert_eq!(get_string_by_cell_path(&result, "0.OtherPatientIDsSequence.1.PatientID"), "24680");
@@ -23,7 +33,9 @@ fn read_dicomweb_list() -> Result<(), nu_protocol::ShellError> {
2333

2434
{
2535
assert_eq!(get_string_by_cell_path(&result, "1.StudyInstanceUID"), "1.2.392.200036.9116.2.2.2.2162893313.1029997326.945876");
36+
assert_eq!(get_string_list_by_cell_path(&result, "1.ModalitiesInStudy"), vec!["CT".to_string(), "MG".to_string()]);
2637
assert_eq!(get_string_by_cell_path(&result, "1.PatientName"), "Wang^XiaoDong");
38+
assert_nothing_by_cell_path(&result, "1.StudyDate");
2739
assert_eq!(get_string_by_cell_path(&result, "1.OtherPatientIDsSequence.0.PatientID"), "54321");
2840
assert_eq!(get_string_by_cell_path(&result, "1.OtherPatientIDsSequence.0.IssuerOfPatientID"), "Hospital B2");
2941
assert_eq!(get_string_by_cell_path(&result, "1.OtherPatientIDsSequence.1.PatientID"), "24680");

tests/test_utils.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,26 @@ pub fn get_string_by_cell_path(
8888
.to_owned()
8989
}
9090

91+
/// Asserts that the value at `path` is a list of strings and returns it. Panics on failure.
92+
#[allow(dead_code)]
93+
pub fn get_string_list_by_cell_path(
94+
value: &Value,
95+
path: &str,
96+
) -> Vec<String> {
97+
let result_value = get_value_by_cell_path(value, path);
98+
result_value
99+
.as_list()
100+
.and_then(|list| {
101+
list.iter()
102+
.map(|v| {
103+
v.as_str()
104+
.map(|s| s.to_string())
105+
})
106+
.collect::<Result<Vec<_>, _>>()
107+
})
108+
.unwrap_or_else(|e| panic!("Expected list<string> at path '{}', but found '{}'. Error: {}", path, result_value.get_type(), e))
109+
}
110+
91111
/// Asserts that the value at `path` is an int and returns it. Panics on failure.
92112
#[allow(dead_code)]
93113
pub fn get_int_by_cell_path(

0 commit comments

Comments
 (0)