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

ENH: Add lookup and putting of private elements #508

Merged
merged 11 commits into from
Apr 25, 2024
2 changes: 1 addition & 1 deletion Cargo.lock

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

24 changes: 23 additions & 1 deletion object/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ pub use dicom_dictionary_std::StandardDataDictionary;
/// The default implementation of a root DICOM object.
pub type DefaultDicomObject<D = StandardDataDictionary> = FileDicomObject<mem::InMemDicomObject<D>>;

use dicom_core::header::Header;
use dicom_core::header::{GroupNumber, Header};
use dicom_encoding::adapters::{PixelDataObject, RawPixelData};
use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
use dicom_parser::dataset::{DataSetWriter, IntoTokens};
Expand Down Expand Up @@ -287,6 +287,28 @@ pub enum WriteError {
WriteUnsupportedTransferSyntax { uid: String, backtrace: Backtrace },
}

#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum PrivateElementError {
#[snafu(display("Group number must be odd, found {}", group))]
naterichman marked this conversation as resolved.
Show resolved Hide resolved
InvalidGroup { group: GroupNumber },
#[snafu(display("Private creator {} not found in group {}", creator, group))]
PrivateCreatorNotFound { creator: String, group: GroupNumber },
#[snafu(display(
"Private Creator {} found in group {}, but elem {} not found",
creator,
group,
elem
))]
ElementNotFound {
creator: String,
group: GroupNumber,
elem: u8,
},
#[snafu(display("No space available in group {}", group))]
NoSpace { group: GroupNumber },
}

/// An error which may occur when looking up a DICOM object's attributes.
#[derive(Debug, Snafu)]
#[non_exhaustive]
Expand Down
157 changes: 143 additions & 14 deletions object/src/mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
};
use itertools::Itertools;
use smallvec::SmallVec;
use snafu::{OptionExt, ResultExt};
use snafu::{ensure, OptionExt, ResultExt};
use std::borrow::Cow;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
Expand All @@ -55,15 +55,16 @@
use crate::{meta::FileMetaTable, FileMetaTableBuilder};
use crate::{
AccessByNameError, AccessError, AtAccessError, BuildMetaTableSnafu, CreateParserSnafu,
CreatePrinterSnafu, DicomObject, FileDicomObject, MissingElementValueSnafu,
MissingLeafElementSnafu, NoSuchAttributeNameSnafu, NoSuchDataElementAliasSnafu,
NoSuchDataElementTagSnafu, NotASequenceSnafu, OpenFileSnafu, ParseMetaDataSetSnafu,
PrematureEndSnafu, PrepareMetaTableSnafu, PrintDataSetSnafu, ReadError, ReadFileSnafu,
CreatePrinterSnafu, DicomObject, ElementNotFoundSnafu, FileDicomObject, InvalidGroupSnafu,
MissingElementValueSnafu, MissingLeafElementSnafu, NoSpaceSnafu, NoSuchAttributeNameSnafu,
NoSuchDataElementAliasSnafu, NoSuchDataElementTagSnafu, NotASequenceSnafu, OpenFileSnafu,
ParseMetaDataSetSnafu, PrematureEndSnafu, PrepareMetaTableSnafu, PrintDataSetSnafu,
PrivateCreatorNotFoundSnafu, PrivateElementError, ReadError, ReadFileSnafu,
ReadPreambleBytesSnafu, ReadTokenSnafu, ReadUnsupportedTransferSyntaxSnafu,
UnexpectedTokenSnafu, WithMetaError, WriteError,
};
use dicom_core::dictionary::{DataDictionary, DataDictionaryEntry};
use dicom_core::header::{HasLength, Header};
use dicom_core::header::{GroupNumber, HasLength, Header};
use dicom_core::value::{DataSetSequence, PixelFragmentSequence, Value, ValueType, C};
use dicom_core::{DataElement, Length, PrimitiveValue, Tag, VR};
use dicom_dictionary_std::{tags, StandardDataDictionary};
Expand Down Expand Up @@ -372,8 +373,7 @@

// read rest of data according to metadata, feed it to object
if let Some(ts) = ts_index.get(&meta.transfer_syntax) {
let mut dataset =
DataSetReader::new_with_ts(file, ts).context(CreateParserSnafu)?;
let mut dataset = DataSetReader::new_with_ts(file, ts).context(CreateParserSnafu)?;

Ok(FileDicomObject {
meta,
Expand Down Expand Up @@ -457,8 +457,7 @@

// read rest of data according to metadata, feed it to object
if let Some(ts) = ts_index.get(&meta.transfer_syntax) {
let mut dataset =
DataSetReader::new_with_ts(file, ts).context(CreateParserSnafu)?;
let mut dataset = DataSetReader::new_with_ts(file, ts).context(CreateParserSnafu)?;
let obj = InMemDicomObject::build_object(
&mut dataset,
dict,
Expand Down Expand Up @@ -704,6 +703,43 @@
}
}

fn find_private_creator(&self, group: GroupNumber, creator: &str) -> Option<&Tag> {
let range = Tag(group, 0)..Tag(group, 0xFF);
for (tag, elem) in self.entries.range(range) {
// Private Creators are always LO
// https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.8.html
naterichman marked this conversation as resolved.
Show resolved Hide resolved
if elem.header().vr() == VR::LO && elem.to_str().unwrap_or_default() == creator {
return Some(tag);
}
}
None
}

pub fn private_element(
naterichman marked this conversation as resolved.
Show resolved Hide resolved
&self,
group: GroupNumber,
creator: &str,
element: u8,
) -> Result<&InMemElement<D>, PrivateElementError> {
let tag = self.find_private_creator(group, creator).ok_or_else(|| {
PrivateCreatorNotFoundSnafu {
group,
creator: creator.to_string(),
}
.build()
})?;

let element_num = (tag.element() << 8) | (element as u16);
self.get(Tag(group, element_num)).ok_or_else(|| {
ElementNotFoundSnafu {
group,
creator: creator.to_string(),
elem: element,
}
.build()
})
}

/// Insert a data element to the object, replacing (and returning) any
/// previous element of the same attribute.
/// This might invalidate all sequence and item lengths if the charset of the
Expand All @@ -722,6 +758,42 @@
self.entries.insert(elt.tag(), elt)
}

pub fn put_private_element(
&mut self,
group: GroupNumber,
creator: &str,
element: u8,
vr: VR,
value: PrimitiveValue,
) -> Result<Option<InMemElement<D>>, PrivateElementError> {
ensure!(group % 2 == 1, InvalidGroupSnafu { group });
let private_creator = self.find_private_creator(group, creator);
if let Some(tag) = private_creator {
// Private creator already exists
let tag = Tag(group, tag.element() << 8 | (element as u16));
return Ok(self.put_element(DataElement::new(tag, vr, value)));

Check warning on line 774 in object/src/mem.rs

View workflow job for this annotation

GitHub Actions / Test (default) (stable)

unneeded `return` statement
naterichman marked this conversation as resolved.
Show resolved Hide resolved
} else {
// Find last reserved block of tags.
let range = Tag(group, 0)..Tag(group, 0xFF);
let last_entry = self.entries.range(range).rev().next();

Check warning on line 778 in object/src/mem.rs

View workflow job for this annotation

GitHub Actions / Test (default) (stable)

manual backwards iteration
naterichman marked this conversation as resolved.
Show resolved Hide resolved
let next_available = match last_entry {
Some((tag, _)) => tag.element() + 1,
None => 0x01,
};
if next_available < 0xFF {
// Put private creator
let tag = Tag(group, next_available);
self.put_str(tag, VR::LO, creator);

// Put private element
let tag = Tag(group, next_available << 8 | (element as u16));
return Ok(self.put_element(DataElement::new(tag, vr, value)));

Check warning on line 790 in object/src/mem.rs

View workflow job for this annotation

GitHub Actions / Test (default) (stable)

unneeded `return` statement
naterichman marked this conversation as resolved.
Show resolved Hide resolved
} else {
return NoSpaceSnafu { group }.fail();

Check warning on line 792 in object/src/mem.rs

View workflow job for this annotation

GitHub Actions / Test (default) (stable)

unneeded `return` statement
naterichman marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

/// Insert a new element with a string value to the object,
/// replacing (and returning) any previous element of the same attribute.
pub fn put_str(
Expand Down Expand Up @@ -1926,10 +1998,7 @@
use byteordered::Endianness;
use dicom_core::chrono::FixedOffset;
use dicom_core::value::{DicomDate, DicomDateTime, DicomTime};
use dicom_core::{
dicom_value,
header::DataElementHeader,
};
use dicom_core::{dicom_value, header::DataElementHeader};
use dicom_encoding::{
decode::{basic::BasicDecoder, implicit_le::ImplicitVRLittleEndianDecoder},
encode::{implicit_le::ImplicitVRLittleEndianEncoder, EncoderFor},
Expand Down Expand Up @@ -3626,4 +3695,64 @@
converted_tokens
);
}

#[test]
fn private_elements() {
let mut ds = InMemDicomObject::from_element_iter(vec![
DataElement::new(
Tag(0x0009, 0x0010),
VR::LO,
PrimitiveValue::from("CREATOR 1"),
),
DataElement::new(
Tag(0x0009, 0x0011),
VR::LO,
PrimitiveValue::from("CREATOR 2"),
),
DataElement::new(
Tag(0x0011, 0x0010),
VR::LO,
PrimitiveValue::from("CREATOR 3"),
),
]);
ds.put_private_element(
0x0009,
"CREATOR 1",
0x01,
VR::DS,
PrimitiveValue::Str("1.0".to_string()),
)
.unwrap();
ds.put_private_element(
0x0009,
"CREATOR 4",
0x02,
VR::DS,
PrimitiveValue::Str("1.0".to_string()),
)
.unwrap();
assert_eq!(
ds.private_element(0x0009, "CREATOR 1", 0x01)
.unwrap()
.value()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
ds.private_element(0x0009, "CREATOR 4", 0x02)
.unwrap()
.value()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
ds.private_element(0x0009, "CREATOR 4", 0x02)
.unwrap()
.header()
.tag(),
Tag(0x0009, 0x1202)
);
}
}