use byteordered::byteorder::{ByteOrder, LittleEndian};
use dicom_core::dicom_value;
use dicom_core::header::{DataElement, EmptyObject, HasLength, Header};
use dicom_core::ops::{ApplyOp, AttributeAction, AttributeOp, AttributeSelectorStep};
use dicom_core::value::{PrimitiveValue, Value, ValueType};
use dicom_core::{Length, Tag, VR};
use dicom_dictionary_std::tags;
use dicom_encoding::decode::{self, DecodeFrom};
use dicom_encoding::encode::explicit_le::ExplicitVRLittleEndianEncoder;
use dicom_encoding::encode::EncoderFor;
use dicom_encoding::text::{self, TextCodec};
use dicom_encoding::TransferSyntax;
use dicom_parser::dataset::{DataSetWriter, IntoTokens};
use snafu::{ensure, Backtrace, OptionExt, ResultExt, Snafu};
use std::io::{Read, Write};
use crate::ops::{
ApplyError, ApplyResult, IllegalExtendSnafu, IncompatibleTypesSnafu, MandatorySnafu,
UnsupportedActionSnafu, UnsupportedAttributeSnafu,
};
use crate::{IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME};
const DICM_MAGIC_CODE: [u8; 4] = [b'D', b'I', b'C', b'M'];
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum Error {
#[snafu(display("Could not start reading DICOM data"))]
ReadMagicCode {
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not read data value"))]
ReadValueData {
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not allocate memory"))]
AllocationSize {
backtrace: Backtrace,
source: std::collections::TryReserveError,
},
#[snafu(display("Could not decode text in {}", name))]
DecodeText {
name: std::borrow::Cow<'static, str>,
#[snafu(backtrace)]
source: dicom_encoding::text::DecodeTextError,
},
#[snafu(display("Invalid DICOM file (magic code check failed)"))]
NotDicom { backtrace: Backtrace },
#[snafu(display("Could not decode data element"))]
DecodeElement {
#[snafu(backtrace)]
source: dicom_encoding::decode::Error,
},
#[snafu(display("Unexpected data element tagged {}", tag))]
UnexpectedTag { tag: Tag, backtrace: Backtrace },
#[snafu(display("Missing data element `{}`", alias))]
MissingElement {
alias: &'static str,
backtrace: Backtrace,
},
#[snafu(display("Unexpected length {} for data element tagged {}", length, tag))]
UnexpectedDataValueLength {
tag: Tag,
length: Length,
backtrace: Backtrace,
},
#[snafu(display("Undefined value length for data element tagged {}", tag))]
UndefinedValueLength { tag: Tag, backtrace: Backtrace },
#[snafu(display("Could not write file meta group data set"))]
WriteSet {
#[snafu(backtrace)]
source: dicom_parser::dataset::write::Error,
},
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq)]
pub struct FileMetaTable {
pub information_group_length: u32,
pub information_version: [u8; 2],
pub media_storage_sop_class_uid: String,
pub media_storage_sop_instance_uid: String,
pub transfer_syntax: String,
pub implementation_class_uid: String,
pub implementation_version_name: Option<String>,
pub source_application_entity_title: Option<String>,
pub sending_application_entity_title: Option<String>,
pub receiving_application_entity_title: Option<String>,
pub private_information_creator_uid: Option<String>,
pub private_information: Option<Vec<u8>>,
}
fn read_str_body<'s, S, T>(source: &'s mut S, text: &T, len: u32) -> Result<String>
where
S: Read + 's,
T: TextCodec,
{
let mut v = Vec::new();
v.try_reserve_exact(len as usize)
.context(AllocationSizeSnafu)?;
v.resize(len as usize, 0);
source.read_exact(&mut v).context(ReadValueDataSnafu)?;
text.decode(&v)
.context(DecodeTextSnafu { name: text.name() })
}
impl FileMetaTable {
pub fn from_reader<R: Read>(file: R) -> Result<Self> {
FileMetaTable::read_from(file)
}
pub fn transfer_syntax(&self) -> &str {
self.transfer_syntax
.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
}
pub fn media_storage_sop_instance_uid(&self) -> &str {
self.media_storage_sop_instance_uid
.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
}
pub fn media_storage_sop_class_uid(&self) -> &str {
self.media_storage_sop_class_uid
.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
}
pub fn implementation_class_uid(&self) -> &str {
self.implementation_class_uid
.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
}
pub fn private_information_creator_uid(&self) -> Option<&str> {
self.private_information_creator_uid
.as_ref()
.map(|s| s.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
}
pub fn set_transfer_syntax<D, R, W>(&mut self, ts: &TransferSyntax<D, R, W>) {
self.transfer_syntax = ts
.uid()
.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
.to_string();
self.update_information_group_length();
}
pub fn update_information_group_length(&mut self) {
self.information_group_length = self.calculate_information_group_length();
}
fn apply(&mut self, op: AttributeOp) -> ApplyResult {
let AttributeSelectorStep::Tag(tag) = op.selector.first_step() else {
return UnsupportedAttributeSnafu.fail();
};
match *tag {
tags::TRANSFER_SYNTAX_UID => Self::apply_required_string(op, &mut self.transfer_syntax),
tags::MEDIA_STORAGE_SOP_CLASS_UID => {
Self::apply_required_string(op, &mut self.media_storage_sop_class_uid)
}
tags::MEDIA_STORAGE_SOP_INSTANCE_UID => {
Self::apply_required_string(op, &mut self.media_storage_sop_instance_uid)
}
tags::IMPLEMENTATION_CLASS_UID => {
Self::apply_required_string(op, &mut self.implementation_class_uid)
}
tags::IMPLEMENTATION_VERSION_NAME => {
Self::apply_optional_string(op, &mut self.implementation_version_name)
}
tags::SOURCE_APPLICATION_ENTITY_TITLE => {
Self::apply_optional_string(op, &mut self.source_application_entity_title)
}
tags::SENDING_APPLICATION_ENTITY_TITLE => {
Self::apply_optional_string(op, &mut self.sending_application_entity_title)
}
tags::RECEIVING_APPLICATION_ENTITY_TITLE => {
Self::apply_optional_string(op, &mut self.receiving_application_entity_title)
}
tags::PRIVATE_INFORMATION_CREATOR_UID => {
Self::apply_optional_string(op, &mut self.private_information_creator_uid)
}
_ if matches!(
op.action,
AttributeAction::Remove | AttributeAction::Empty | AttributeAction::Truncate(_)
) =>
{
Ok(())
}
_ => UnsupportedAttributeSnafu.fail(),
}?;
self.update_information_group_length();
Ok(())
}
fn apply_required_string(op: AttributeOp, target_attribute: &mut String) -> ApplyResult {
match op.action {
AttributeAction::Remove | AttributeAction::Empty => MandatorySnafu.fail(),
AttributeAction::SetVr(_) | AttributeAction::Truncate(_) => {
Ok(())
}
AttributeAction::Set(value) | AttributeAction::Replace(value) => {
if let Ok(value) = value.string() {
*target_attribute = value.to_string();
Ok(())
} else {
IncompatibleTypesSnafu {
kind: ValueType::Str,
}
.fail()
}
}
AttributeAction::SetStr(string) | AttributeAction::ReplaceStr(string) => {
*target_attribute = string.to_string();
Ok(())
}
AttributeAction::SetIfMissing(_) | AttributeAction::SetStrIfMissing(_) => {
Ok(())
}
AttributeAction::PushStr(_) => IllegalExtendSnafu.fail(),
AttributeAction::PushI32(_)
| AttributeAction::PushU32(_)
| AttributeAction::PushI16(_)
| AttributeAction::PushU16(_)
| AttributeAction::PushF32(_)
| AttributeAction::PushF64(_) => IncompatibleTypesSnafu {
kind: ValueType::Str,
}
.fail(),
_ => UnsupportedActionSnafu.fail(),
}
}
fn apply_optional_string(
op: AttributeOp,
target_attribute: &mut Option<String>,
) -> ApplyResult {
match op.action {
AttributeAction::Remove => {
target_attribute.take();
Ok(())
}
AttributeAction::Empty => {
if let Some(s) = target_attribute.as_mut() {
s.clear();
}
Ok(())
}
AttributeAction::SetVr(_) => {
Ok(())
}
AttributeAction::Set(value) => {
if let Ok(value) = value.string() {
*target_attribute = Some(value.to_string());
Ok(())
} else {
IncompatibleTypesSnafu {
kind: ValueType::Str,
}
.fail()
}
}
AttributeAction::SetStr(value) => {
*target_attribute = Some(value.to_string());
Ok(())
}
AttributeAction::SetIfMissing(value) => {
if target_attribute.is_some() {
return Ok(());
}
if let Ok(value) = value.string() {
*target_attribute = Some(value.to_string());
Ok(())
} else {
IncompatibleTypesSnafu {
kind: ValueType::Str,
}
.fail()
}
}
AttributeAction::SetStrIfMissing(value) => {
if target_attribute.is_none() {
*target_attribute = Some(value.to_string());
}
Ok(())
}
AttributeAction::Replace(value) => {
if target_attribute.is_none() {
return Ok(());
}
if let Ok(value) = value.string() {
*target_attribute = Some(value.to_string());
Ok(())
} else {
IncompatibleTypesSnafu {
kind: ValueType::Str,
}
.fail()
}
}
AttributeAction::ReplaceStr(value) => {
if target_attribute.is_some() {
*target_attribute = Some(value.to_string());
}
Ok(())
}
AttributeAction::PushStr(_) => IllegalExtendSnafu.fail(),
AttributeAction::PushI32(_)
| AttributeAction::PushU32(_)
| AttributeAction::PushI16(_)
| AttributeAction::PushU16(_)
| AttributeAction::PushF32(_)
| AttributeAction::PushF64(_) => IncompatibleTypesSnafu {
kind: ValueType::Str,
}
.fail(),
_ => UnsupportedActionSnafu.fail(),
}
}
fn calculate_information_group_length(&self) -> u32 {
14 + 8
+ dicom_len(&self.media_storage_sop_class_uid)
+ 8
+ dicom_len(&self.media_storage_sop_instance_uid)
+ 8
+ dicom_len(&self.transfer_syntax)
+ 8
+ dicom_len(&self.implementation_class_uid)
+ self
.implementation_version_name
.as_ref()
.map(|s| 8 + dicom_len(s))
.unwrap_or(0)
+ self
.source_application_entity_title
.as_ref()
.map(|s| 8 + dicom_len(s))
.unwrap_or(0)
+ self
.sending_application_entity_title
.as_ref()
.map(|s| 8 + dicom_len(s))
.unwrap_or(0)
+ self
.receiving_application_entity_title
.as_ref()
.map(|s| 8 + dicom_len(s))
.unwrap_or(0)
+ self
.private_information_creator_uid
.as_ref()
.map(|s| 8 + dicom_len(s))
.unwrap_or(0)
+ self
.private_information
.as_ref()
.map(|x| 12 + ((x.len() as u32 + 1) & !1))
.unwrap_or(0)
}
fn read_from<S: Read>(mut file: S) -> Result<Self> {
let mut buff: [u8; 4] = [0; 4];
{
file.read_exact(&mut buff).context(ReadMagicCodeSnafu)?;
ensure!(buff == DICM_MAGIC_CODE, NotDicomSnafu);
}
let decoder = decode::file_header_decoder();
let text = text::DefaultCharacterSetCodec;
let builder = FileMetaTableBuilder::new();
let group_length: u32 = {
let (elem, _bytes_read) = decoder
.decode_header(&mut file)
.context(DecodeElementSnafu)?;
if elem.tag() != Tag(0x0002, 0x0000) {
return UnexpectedTagSnafu { tag: elem.tag() }.fail();
}
if elem.length() != Length(4) {
return UnexpectedDataValueLengthSnafu {
tag: elem.tag(),
length: elem.length(),
}
.fail();
}
let mut buff: [u8; 4] = [0; 4];
file.read_exact(&mut buff).context(ReadValueDataSnafu)?;
LittleEndian::read_u32(&buff)
};
let mut total_bytes_read = 0;
let mut builder = builder.group_length(group_length);
while total_bytes_read < group_length {
let (elem, header_bytes_read) = decoder
.decode_header(&mut file)
.context(DecodeElementSnafu)?;
let elem_len = match elem.length().get() {
None => {
return UndefinedValueLengthSnafu { tag: elem.tag() }.fail();
}
Some(len) => len,
};
builder = match elem.tag() {
Tag(0x0002, 0x0001) => {
if elem.length() != Length(2) {
return UnexpectedDataValueLengthSnafu {
tag: elem.tag(),
length: elem.length(),
}
.fail();
}
let mut hbuf = [0u8; 2];
file.read_exact(&mut hbuf[..]).context(ReadValueDataSnafu)?;
builder.information_version(hbuf)
}
Tag(0x0002, 0x0002) => {
builder.media_storage_sop_class_uid(read_str_body(&mut file, &text, elem_len)?)
}
Tag(0x0002, 0x0003) => builder
.media_storage_sop_instance_uid(read_str_body(&mut file, &text, elem_len)?),
Tag(0x0002, 0x0010) => {
builder.transfer_syntax(read_str_body(&mut file, &text, elem_len)?)
}
Tag(0x0002, 0x0012) => {
builder.implementation_class_uid(read_str_body(&mut file, &text, elem_len)?)
}
Tag(0x0002, 0x0013) => {
let mut v = Vec::new();
v.try_reserve_exact(elem_len as usize)
.context(AllocationSizeSnafu)?;
v.resize(elem_len as usize, 0);
file.read_exact(&mut v).context(ReadValueDataSnafu)?;
builder.implementation_version_name(
text.decode(&v)
.context(DecodeTextSnafu { name: text.name() })?,
)
}
Tag(0x0002, 0x0016) => {
let mut v = Vec::new();
v.try_reserve_exact(elem_len as usize)
.context(AllocationSizeSnafu)?;
v.resize(elem_len as usize, 0);
file.read_exact(&mut v).context(ReadValueDataSnafu)?;
builder.source_application_entity_title(
text.decode(&v)
.context(DecodeTextSnafu { name: text.name() })?,
)
}
Tag(0x0002, 0x0017) => {
let mut v = Vec::new();
v.try_reserve_exact(elem_len as usize)
.context(AllocationSizeSnafu)?;
v.resize(elem_len as usize, 0);
file.read_exact(&mut v).context(ReadValueDataSnafu)?;
builder.sending_application_entity_title(
text.decode(&v)
.context(DecodeTextSnafu { name: text.name() })?,
)
}
Tag(0x0002, 0x0018) => {
let mut v = Vec::new();
v.try_reserve_exact(elem_len as usize)
.context(AllocationSizeSnafu)?;
v.resize(elem_len as usize, 0);
file.read_exact(&mut v).context(ReadValueDataSnafu)?;
builder.receiving_application_entity_title(
text.decode(&v)
.context(DecodeTextSnafu { name: text.name() })?,
)
}
Tag(0x0002, 0x0100) => {
let mut v = Vec::new();
v.try_reserve_exact(elem_len as usize)
.context(AllocationSizeSnafu)?;
v.resize(elem_len as usize, 0);
file.read_exact(&mut v).context(ReadValueDataSnafu)?;
builder.private_information_creator_uid(
text.decode(&v)
.context(DecodeTextSnafu { name: text.name() })?,
)
}
Tag(0x0002, 0x0102) => {
let mut v = Vec::new();
v.try_reserve_exact(elem_len as usize)
.context(AllocationSizeSnafu)?;
v.resize(elem_len as usize, 0);
file.read_exact(&mut v).context(ReadValueDataSnafu)?;
builder.private_information(v)
}
tag @ Tag(0x0002, _) => {
tracing::info!("Unknown tag {}", tag);
let bytes_read =
std::io::copy(&mut (&mut file).take(elem_len as u64), &mut std::io::sink())
.context(ReadValueDataSnafu)?;
if bytes_read != elem_len as u64 {
return UnexpectedDataValueLengthSnafu {
tag: elem.tag(),
length: elem_len,
}
.fail();
}
builder
}
tag => {
tracing::warn!("Unexpected off-group tag {}", tag);
let bytes_read =
std::io::copy(&mut (&mut file).take(elem_len as u64), &mut std::io::sink())
.context(ReadValueDataSnafu)?;
if bytes_read != elem_len as u64 {
return UnexpectedDataValueLengthSnafu {
tag: elem.tag(),
length: elem_len,
}
.fail();
}
builder
}
};
total_bytes_read = total_bytes_read
.saturating_add(header_bytes_read as u32)
.saturating_add(elem_len);
}
builder.build()
}
pub fn into_element_iter(self) -> impl Iterator<Item = DataElement<EmptyObject, [u8; 0]>> {
let mut elems = vec![
DataElement::new(
Tag(0x0002, 0x0000),
VR::UL,
Value::Primitive(self.information_group_length.into()),
),
DataElement::new(
Tag(0x0002, 0x0001),
VR::OB,
Value::Primitive(dicom_value!(
U8,
[self.information_version[0], self.information_version[1]]
)),
),
DataElement::new(
Tag(0x0002, 0x0002),
VR::UI,
Value::Primitive(self.media_storage_sop_class_uid.into()),
),
DataElement::new(
Tag(0x0002, 0x0003),
VR::UI,
Value::Primitive(self.media_storage_sop_instance_uid.into()),
),
DataElement::new(
Tag(0x0002, 0x0010),
VR::UI,
Value::Primitive(self.transfer_syntax.into()),
),
DataElement::new(
Tag(0x0002, 0x0012),
VR::UI,
Value::Primitive(self.implementation_class_uid.into()),
),
];
if let Some(v) = self.implementation_version_name {
elems.push(DataElement::new(
Tag(0x0002, 0x0013),
VR::SH,
Value::Primitive(v.into()),
));
}
if let Some(v) = self.source_application_entity_title {
elems.push(DataElement::new(
Tag(0x0002, 0x0016),
VR::AE,
Value::Primitive(v.into()),
));
}
if let Some(v) = self.sending_application_entity_title {
elems.push(DataElement::new(
Tag(0x0002, 0x0017),
VR::AE,
Value::Primitive(v.into()),
));
}
if let Some(v) = self.receiving_application_entity_title {
elems.push(DataElement::new(
Tag(0x0002, 0x0018),
VR::AE,
Value::Primitive(v.into()),
));
}
if let Some(v) = self.private_information_creator_uid {
elems.push(DataElement::new(
Tag(0x0002, 0x0100),
VR::UI,
Value::Primitive(v.into()),
));
}
if let Some(v) = self.private_information {
elems.push(DataElement::new(
Tag(0x0002, 0x0102),
VR::OB,
Value::Primitive(PrimitiveValue::U8(v.into())),
));
}
elems.into_iter()
}
pub fn to_element_iter(&self) -> impl Iterator<Item = DataElement<EmptyObject, [u8; 0]>> + '_ {
self.clone().into_element_iter()
}
pub fn write<W: Write>(&self, writer: W) -> Result<()> {
let mut dset = DataSetWriter::new(
writer,
EncoderFor::new(ExplicitVRLittleEndianEncoder::default()),
);
dset.write_sequence(
self.clone()
.into_element_iter()
.flat_map(IntoTokens::into_tokens),
)
.context(WriteSetSnafu)
}
}
impl ApplyOp for FileMetaTable {
type Err = ApplyError;
fn apply(&mut self, op: AttributeOp) -> ApplyResult {
self.apply(op)
}
}
#[derive(Debug, Default, Clone)]
pub struct FileMetaTableBuilder {
information_group_length: Option<u32>,
information_version: Option<[u8; 2]>,
media_storage_sop_class_uid: Option<String>,
media_storage_sop_instance_uid: Option<String>,
transfer_syntax: Option<String>,
implementation_class_uid: Option<String>,
implementation_version_name: Option<String>,
source_application_entity_title: Option<String>,
sending_application_entity_title: Option<String>,
receiving_application_entity_title: Option<String>,
private_information_creator_uid: Option<String>,
private_information: Option<Vec<u8>>,
}
#[inline]
fn padded<T>(s: T, pad: char) -> String
where
T: Into<String>,
{
let mut s = s.into();
if s.len() % 2 == 1 {
s.push(pad);
}
s
}
fn ui_padded<T>(s: T) -> String
where
T: Into<String>,
{
padded(s, '\0')
}
fn txt_padded<T>(s: T) -> String
where
T: Into<String>,
{
padded(s, ' ')
}
impl FileMetaTableBuilder {
pub fn new() -> FileMetaTableBuilder {
FileMetaTableBuilder::default()
}
pub fn group_length(mut self, value: u32) -> FileMetaTableBuilder {
self.information_group_length = Some(value);
self
}
pub fn information_version(mut self, value: [u8; 2]) -> FileMetaTableBuilder {
self.information_version = Some(value);
self
}
pub fn media_storage_sop_class_uid<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.media_storage_sop_class_uid = Some(ui_padded(value));
self
}
pub fn media_storage_sop_instance_uid<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.media_storage_sop_instance_uid = Some(ui_padded(value));
self
}
pub fn transfer_syntax<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.transfer_syntax = Some(ui_padded(value));
self
}
pub fn implementation_class_uid<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.implementation_class_uid = Some(ui_padded(value));
self
}
pub fn implementation_version_name<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.implementation_version_name = Some(txt_padded(value));
self
}
pub fn source_application_entity_title<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.source_application_entity_title = Some(txt_padded(value));
self
}
pub fn sending_application_entity_title<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.sending_application_entity_title = Some(txt_padded(value));
self
}
pub fn receiving_application_entity_title<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.receiving_application_entity_title = Some(txt_padded(value));
self
}
pub fn private_information_creator_uid<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<String>,
{
self.private_information_creator_uid = Some(ui_padded(value));
self
}
pub fn private_information<T>(mut self, value: T) -> FileMetaTableBuilder
where
T: Into<Vec<u8>>,
{
self.private_information = Some(value.into());
self
}
pub fn build(self) -> Result<FileMetaTable> {
let information_version = self.information_version.unwrap_or(
[0, 1],
);
let media_storage_sop_class_uid = self.media_storage_sop_class_uid.unwrap_or_else(|| {
tracing::warn!("MediaStorageSOPClassUID is missing. Defaulting to empty string.");
String::default()
});
let media_storage_sop_instance_uid =
self.media_storage_sop_instance_uid.unwrap_or_else(|| {
tracing::warn!(
"MediaStorageSOPInstanceUID is missing. Defaulting to empty string."
);
String::default()
});
let transfer_syntax = self.transfer_syntax.context(MissingElementSnafu {
alias: "TransferSyntax",
})?;
let mut implementation_version_name = self.implementation_version_name;
let implementation_class_uid = self.implementation_class_uid.unwrap_or_else(|| {
implementation_version_name = Some(IMPLEMENTATION_VERSION_NAME.to_string());
IMPLEMENTATION_CLASS_UID.to_string()
});
let mut table = FileMetaTable {
information_group_length: 0x00,
information_version,
media_storage_sop_class_uid,
media_storage_sop_instance_uid,
transfer_syntax,
implementation_class_uid,
implementation_version_name,
source_application_entity_title: self.source_application_entity_title,
sending_application_entity_title: self.sending_application_entity_title,
receiving_application_entity_title: self.receiving_application_entity_title,
private_information_creator_uid: self.private_information_creator_uid,
private_information: self.private_information,
};
table.update_information_group_length();
debug_assert!(table.information_group_length > 0);
Ok(table)
}
}
fn dicom_len<T: AsRef<str>>(x: T) -> u32 {
(x.as_ref().len() as u32 + 1) & !1
}
#[cfg(test)]
mod tests {
use crate::{IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME};
use super::{dicom_len, FileMetaTable, FileMetaTableBuilder};
use dicom_core::ops::{AttributeAction, AttributeOp};
use dicom_core::value::Value;
use dicom_core::{dicom_value, DataElement, PrimitiveValue, Tag, VR};
use dicom_dictionary_std::tags;
const TEST_META_1: &'static [u8] = &[
b'D', b'I', b'C', b'M',
0x02, 0x00, 0x00, 0x00, b'U', b'L', 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00,
0x02, 0x00, 0x01, 0x00, b'O', b'B', 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
0x02, 0x00, 0x02, 0x00, b'U', b'I', 0x1a, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x38, 0x34, 0x30,
0x2e, 0x31, 0x30, 0x30, 0x30, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e,
0x31, 0x2e, 0x31, 0x00,
0x02, 0x00, 0x03, 0x00, b'U', b'I', 0x38, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x2e, 0x34,
0x2e, 0x35, 0x2e, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x2e, 0x31, 0x32, 0x33,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x2e, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x2e, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2e, 0x31, 0x32, 0x33, 0x34,
0x35, 0x36, 0x37, 0x00,
0x02, 0x00, 0x10, 0x00, b'U', b'I', 0x14, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x38, 0x34, 0x30,
0x2e, 0x31, 0x30, 0x30, 0x30, 0x38, 0x2e, 0x31, 0x2e, 0x32, 0x2e, 0x31, 0x00,
0x02, 0x00, 0x12, 0x00, b'U', b'I', 0x14, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x34, 0x35,
0x2e, 0x36, 0x2e, 0x37, 0x38, 0x39, 0x30, 0x2e, 0x31, 0x2e, 0x32, 0x33, 0x34,
0x02, 0x00, 0x13, 0x00, b'S', b'H', 0x10, 0x00, 0x52, 0x55, 0x53, 0x54, 0x59, 0x5f, 0x44,
0x49, 0x43, 0x4f, 0x4d, 0x5f, 0x32, 0x36, 0x39, 0x20,
0x02, 0x00, 0x16, 0x00, b'A', b'E', 0x00, 0x00,
];
#[test]
fn read_meta_table_from_reader() {
let mut source = TEST_META_1;
let table = FileMetaTable::from_reader(&mut source).unwrap();
let gt = FileMetaTable {
information_group_length: 200,
information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
media_storage_sop_instance_uid:
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
source_application_entity_title: Some("".to_owned()),
sending_application_entity_title: None,
receiving_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
assert_eq!(table.information_group_length, 200);
assert_eq!(table.information_version, [0u8, 1u8]);
assert_eq!(
table.media_storage_sop_class_uid,
"1.2.840.10008.5.1.4.1.1.1\0"
);
assert_eq!(
table.media_storage_sop_instance_uid,
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0"
);
assert_eq!(table.transfer_syntax, "1.2.840.10008.1.2.1\0");
assert_eq!(table.implementation_class_uid, "1.2.345.6.7890.1.234");
assert_eq!(
table.implementation_version_name,
Some("RUSTY_DICOM_269 ".to_owned())
);
assert_eq!(table.source_application_entity_title, Some("".into()));
assert_eq!(table.sending_application_entity_title, None);
assert_eq!(table.receiving_application_entity_title, None);
assert_eq!(table.private_information_creator_uid, None);
assert_eq!(table.private_information, None);
assert_eq!(table, gt);
}
#[test]
fn create_meta_table_with_builder() {
let table = FileMetaTableBuilder::new()
.information_version([0, 1])
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
.media_storage_sop_instance_uid(
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567",
)
.transfer_syntax("1.2.840.10008.1.2.1")
.implementation_class_uid("1.2.345.6.7890.1.234")
.implementation_version_name("RUSTY_DICOM_269")
.source_application_entity_title("")
.build()
.unwrap();
let gt = FileMetaTable {
information_group_length: 200,
information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
media_storage_sop_instance_uid:
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
source_application_entity_title: Some("".to_owned()),
sending_application_entity_title: None,
receiving_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
assert_eq!(table.information_group_length, gt.information_group_length);
assert_eq!(table, gt);
}
#[test]
fn create_meta_table_with_builder_minimal() {
let table = FileMetaTableBuilder::new()
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
.media_storage_sop_instance_uid(
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567",
)
.transfer_syntax("1.2.840.10008.1.2")
.build()
.unwrap();
let gt = FileMetaTable {
information_group_length: 154
+ dicom_len(IMPLEMENTATION_CLASS_UID)
+ dicom_len(IMPLEMENTATION_VERSION_NAME),
information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
media_storage_sop_instance_uid:
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
transfer_syntax: "1.2.840.10008.1.2\0".to_owned(),
implementation_class_uid: IMPLEMENTATION_CLASS_UID.to_owned(),
implementation_version_name: Some(IMPLEMENTATION_VERSION_NAME.to_owned()),
source_application_entity_title: None,
sending_application_entity_title: None,
receiving_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
assert_eq!(table.information_group_length, gt.information_group_length);
assert_eq!(table, gt);
}
#[test]
fn change_transfer_syntax_update_table() {
let mut table = FileMetaTableBuilder::new()
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
.media_storage_sop_instance_uid(
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567",
)
.transfer_syntax("1.2.840.10008.1.2.1")
.build()
.unwrap();
assert_eq!(
table.information_group_length,
156 + dicom_len(IMPLEMENTATION_CLASS_UID) + dicom_len(IMPLEMENTATION_VERSION_NAME)
);
table.set_transfer_syntax(
&dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN,
);
assert_eq!(
table.information_group_length,
154 + dicom_len(IMPLEMENTATION_CLASS_UID) + dicom_len(IMPLEMENTATION_VERSION_NAME)
);
}
#[test]
fn read_meta_table_into_iter() {
let table = FileMetaTable {
information_group_length: 200,
information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
media_storage_sop_instance_uid:
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
source_application_entity_title: Some("".to_owned()),
sending_application_entity_title: None,
receiving_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
assert_eq!(table.calculate_information_group_length(), 200);
let gt = vec![
DataElement::new(Tag(0x0002, 0x0000), VR::UL, dicom_value!(U32, 200)),
DataElement::new(Tag(0x0002, 0x0001), VR::OB, dicom_value!(U8, [0, 1])),
DataElement::new(
Tag(0x0002, 0x0002),
VR::UI,
Value::Primitive("1.2.840.10008.5.1.4.1.1.1\0".into()),
),
DataElement::new(
Tag(0x0002, 0x0003),
VR::UI,
Value::Primitive(
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".into(),
),
),
DataElement::new(
Tag(0x0002, 0x0010),
VR::UI,
Value::Primitive("1.2.840.10008.1.2.1\0".into()),
),
DataElement::new(
Tag(0x0002, 0x0012),
VR::UI,
Value::Primitive("1.2.345.6.7890.1.234".into()),
),
DataElement::new(
Tag(0x0002, 0x0013),
VR::SH,
Value::Primitive("RUSTY_DICOM_269 ".into()),
),
DataElement::new(Tag(0x0002, 0x0016), VR::AE, Value::Primitive("".into())),
];
let elems: Vec<_> = table.into_element_iter().collect();
assert_eq!(elems, gt);
}
#[test]
fn update_table_with_length() {
let mut table = FileMetaTable {
information_group_length: 55, information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
media_storage_sop_instance_uid:
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
source_application_entity_title: Some("".to_owned()),
sending_application_entity_title: None,
receiving_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
table.update_information_group_length();
assert_eq!(table.information_group_length, 200);
}
#[test]
fn table_ops() {
let mut table = FileMetaTable {
information_group_length: 200,
information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
media_storage_sop_instance_uid:
"1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
implementation_version_name: None,
source_application_entity_title: None,
sending_application_entity_title: None,
receiving_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
table
.apply(AttributeOp::new(
tags::IMPLEMENTATION_VERSION_NAME,
AttributeAction::ReplaceStr("MY_DICOM_1.1".into()),
))
.unwrap();
assert_eq!(table.implementation_version_name, None);
table
.apply(AttributeOp::new(
tags::IMPLEMENTATION_VERSION_NAME,
AttributeAction::SetStr("MY_DICOM_1.1".into()),
))
.unwrap();
assert_eq!(
table.implementation_version_name.as_deref(),
Some("MY_DICOM_1.1"),
);
table
.apply(AttributeOp::new(
tags::SOURCE_APPLICATION_ENTITY_TITLE,
AttributeAction::Set(PrimitiveValue::Str("RICOOGLE-STORAGE".into())),
))
.unwrap();
assert_eq!(
table.source_application_entity_title.as_deref(),
Some("RICOOGLE-STORAGE"),
);
table
.apply(AttributeOp::new(
tags::SOURCE_APPLICATION_ENTITY_TITLE,
AttributeAction::SetStrIfMissing("STORE-SCU".into()),
))
.unwrap();
assert_eq!(
table.source_application_entity_title.as_deref(),
Some("RICOOGLE-STORAGE"),
);
table
.apply(AttributeOp::new(
tags::SENDING_APPLICATION_ENTITY_TITLE,
AttributeAction::SetStrIfMissing("STORE-SCU".into()),
))
.unwrap();
assert_eq!(
table.sending_application_entity_title.as_deref(),
Some("STORE-SCU"),
);
table
.apply(AttributeOp::new(
tags::MEDIA_STORAGE_SOP_CLASS_UID,
AttributeAction::Replace(PrimitiveValue::Str("1.2.840.10008.5.1.4.1.1.7".into())),
))
.unwrap();
assert_eq!(
table.media_storage_sop_class_uid(),
"1.2.840.10008.5.1.4.1.1.7",
);
}
#[test]
fn write_read_does_not_fail() {
let mut table = FileMetaTable {
information_group_length: 0,
information_version: [0u8, 1u8],
media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.7".to_owned(),
media_storage_sop_instance_uid: "2.25.137731752600317795446120660167595746868".to_owned(),
transfer_syntax: "1.2.840.10008.1.2.4.91".to_owned(),
implementation_class_uid: "2.25.305828488182831875890203105390285383139".to_owned(),
implementation_version_name: Some("MYTOOL100".to_owned()),
source_application_entity_title: Some("RUSTY".to_owned()),
receiving_application_entity_title: None,
sending_application_entity_title: None,
private_information_creator_uid: None,
private_information: None,
};
table.update_information_group_length();
let mut buf = vec![b'D', b'I', b'C', b'M'];
table.write(&mut buf).unwrap();
let table2 = FileMetaTable::from_reader(&mut buf.as_slice())
.expect("Should not fail to read the table from the written data");
assert_eq!(table.information_group_length, table2.information_group_length);
}
}