#![allow(clippy::derive_partial_eq_without_eq)]
pub mod file;
pub mod mem;
pub mod meta;
pub mod ops;
pub mod tokens;
pub use crate::file::{from_reader, open_file, OpenFileOptions};
pub use crate::mem::InMemDicomObject;
pub use crate::meta::{FileMetaTable, FileMetaTableBuilder};
use dicom_core::ops::AttributeSelector;
use dicom_core::DataDictionary;
pub use dicom_core::Tag;
pub use dicom_dictionary_std::StandardDataDictionary;
pub type DefaultDicomObject<D = StandardDataDictionary> = FileDicomObject<mem::InMemDicomObject<D>>;
use dicom_core::header::{GroupNumber, Header};
use dicom_encoding::adapters::{PixelDataObject, RawPixelData};
use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
use dicom_parser::dataset::{DataSetWriter, IntoTokens};
use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
use smallvec::SmallVec;
use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
use std::borrow::Cow;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
pub const IMPLEMENTATION_CLASS_UID: &str = "2.25.262086406829110419931297894772577063974";
pub const IMPLEMENTATION_VERSION_NAME: &str = "DICOM-rs 0.8.1";
pub trait DicomObject {
type Element: Header;
fn element(&self, tag: Tag) -> Result<Self::Element, AccessError>;
fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError>;
fn meta(&self) -> Option<&FileMetaTable> {
None
}
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum ReadError {
#[snafu(display("Could not open file '{}'", filename.display()))]
OpenFile {
filename: std::path::PathBuf,
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not read from file '{}'", filename.display()))]
ReadFile {
filename: std::path::PathBuf,
backtrace: Backtrace,
source: std::io::Error,
},
ReadPreambleBytes {
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not parse meta group data set"))]
ParseMetaDataSet {
#[snafu(backtrace)]
source: crate::meta::Error,
},
#[snafu(display("Could not parse sop attribute"))]
ParseSopAttribute {
#[snafu(source(from(dicom_core::value::ConvertValueError, Box::from)))]
source: Box<dicom_core::value::ConvertValueError>,
backtrace: Backtrace,
},
#[snafu(display("Could not create data set parser"))]
CreateParser {
#[snafu(backtrace)]
source: dicom_parser::dataset::read::Error,
},
#[snafu(display("Could not read data set token"))]
ReadToken {
#[snafu(backtrace)]
source: dicom_parser::dataset::read::Error,
},
#[snafu(display("Missing element value after header token"))]
MissingElementValue { backtrace: Backtrace },
#[snafu(display("Unsupported transfer syntax `{}`", uid))]
ReadUnsupportedTransferSyntax { uid: String, backtrace: Backtrace },
#[snafu(display("Unexpected token {:?}", token))]
UnexpectedToken {
token: Box<dicom_parser::dataset::DataToken>,
backtrace: Backtrace,
},
#[snafu(display("Premature data set end"))]
PrematureEnd { backtrace: Backtrace },
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum WriteError {
#[snafu(display("Could not write to file '{}'", filename.display()))]
WriteFile {
filename: std::path::PathBuf,
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not write object preamble"))]
WritePreamble {
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not write magic code"))]
WriteMagicCode {
backtrace: Backtrace,
source: std::io::Error,
},
#[snafu(display("Could not create data set printer"))]
CreatePrinter {
#[snafu(backtrace)]
source: dicom_parser::dataset::write::Error,
},
#[snafu(display("Could not print meta group data set"))]
PrintMetaDataSet {
#[snafu(backtrace)]
source: crate::meta::Error,
},
#[snafu(display("Could not print data set"))]
PrintDataSet {
#[snafu(backtrace)]
source: dicom_parser::dataset::write::Error,
},
#[snafu(display("Unsupported transfer syntax `{}`", uid))]
WriteUnsupportedTransferSyntax { uid: String, backtrace: Backtrace },
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum PrivateElementError {
#[snafu(display("Group number must be odd, found {:#06x}", group))]
InvalidGroup { group: GroupNumber },
#[snafu(display("Private creator {} not found in group {:#06x}", creator, group))]
PrivateCreatorNotFound { creator: String, group: GroupNumber },
#[snafu(display(
"Private Creator {} found in group {:#06x}, but elem {:#06x} not found",
creator,
group,
elem
))]
ElementNotFound {
creator: String,
group: GroupNumber,
elem: u8,
},
#[snafu(display("No space available in group {:#06x}", group))]
NoSpace { group: GroupNumber },
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum AccessError {
#[snafu(display("No such data element with tag {}", tag))]
NoSuchDataElementTag { tag: Tag, backtrace: Backtrace },
}
impl AccessError {
pub fn into_access_by_name(self, alias: impl Into<String>) -> AccessByNameError {
match self {
AccessError::NoSuchDataElementTag { tag, backtrace } => {
AccessByNameError::NoSuchDataElementAlias {
tag,
alias: alias.into(),
backtrace,
}
}
}
}
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
#[snafu(visibility(pub(crate)))]
pub enum AtAccessError {
MissingSequence {
selector: AttributeSelector,
step_index: u32,
},
NotASequence {
selector: AttributeSelector,
step_index: u32,
},
MissingLeafElement { selector: AttributeSelector },
}
#[derive(Debug, Snafu)]
pub enum AccessByNameError {
#[snafu(display("No such data element {} (with tag {})", alias, tag))]
NoSuchDataElementAlias {
tag: Tag,
alias: String,
backtrace: Backtrace,
},
#[snafu(display("Unknown data attribute named `{}`", name))]
NoSuchAttributeName { name: String, backtrace: Backtrace },
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum WithMetaError {
BuildMetaTable {
#[snafu(backtrace)]
source: crate::meta::Error,
},
PrepareMetaTable {
#[snafu(source(from(dicom_core::value::ConvertValueError, Box::from)))]
source: Box<dicom_core::value::ConvertValueError>,
backtrace: Backtrace,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct FileDicomObject<O> {
meta: FileMetaTable,
obj: O,
}
impl<O> FileDicomObject<O> {
pub fn meta(&self) -> &FileMetaTable {
&self.meta
}
pub fn meta_mut(&mut self) -> &mut FileMetaTable {
&mut self.meta
}
pub fn update_meta(&mut self, f: impl FnOnce(&mut FileMetaTable)) {
f(&mut self.meta);
self.meta.update_information_group_length();
}
pub fn into_inner(self) -> O {
self.obj
}
}
impl<O> FileDicomObject<O>
where
for<'a> &'a O: IntoTokens,
{
pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), WriteError> {
let path = path.as_ref();
let file = File::create(path).context(WriteFileSnafu { filename: path })?;
let mut to = BufWriter::new(file);
to.write_all(&[0_u8; 128][..])
.context(WriteFileSnafu { filename: path })?;
to.write_all(b"DICM")
.context(WriteFileSnafu { filename: path })?;
self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
let ts = TransferSyntaxRegistry
.get(&self.meta.transfer_syntax)
.with_context(|| WriteUnsupportedTransferSyntaxSnafu {
uid: self.meta.transfer_syntax.clone(),
})?;
let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
dset_writer
.write_sequence((&self.obj).into_tokens())
.context(PrintDataSetSnafu)?;
Ok(())
}
pub fn write_all<W: Write>(&self, to: W) -> Result<(), WriteError> {
let mut to = BufWriter::new(to);
to.write_all(&[0_u8; 128][..]).context(WritePreambleSnafu)?;
to.write_all(b"DICM").context(WriteMagicCodeSnafu)?;
self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
let ts = TransferSyntaxRegistry
.get(&self.meta.transfer_syntax)
.with_context(|| WriteUnsupportedTransferSyntaxSnafu {
uid: self.meta.transfer_syntax.clone(),
})?;
let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
dset_writer
.write_sequence((&self.obj).into_tokens())
.context(PrintDataSetSnafu)?;
Ok(())
}
pub fn write_meta<W: Write>(&self, to: W) -> Result<(), WriteError> {
self.meta.write(to).context(PrintMetaDataSetSnafu)
}
pub fn write_dataset<W: Write>(&self, to: W) -> Result<(), WriteError> {
let to = BufWriter::new(to);
let ts = TransferSyntaxRegistry
.get(&self.meta.transfer_syntax)
.with_context(|| WriteUnsupportedTransferSyntaxSnafu {
uid: self.meta.transfer_syntax.clone(),
})?;
let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
dset_writer
.write_sequence((&self.obj).into_tokens())
.context(PrintDataSetSnafu)?;
Ok(())
}
}
impl<O> ::std::ops::Deref for FileDicomObject<O> {
type Target = O;
fn deref(&self) -> &Self::Target {
&self.obj
}
}
impl<O> ::std::ops::DerefMut for FileDicomObject<O> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.obj
}
}
impl<O> DicomObject for FileDicomObject<O>
where
O: DicomObject,
{
type Element = <O as DicomObject>::Element;
fn element(&self, tag: Tag) -> Result<Self::Element, AccessError> {
self.obj.element(tag)
}
fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError> {
self.obj.element_by_name(name)
}
fn meta(&self) -> Option<&FileMetaTable> {
Some(&self.meta)
}
}
impl<'a, O: 'a> DicomObject for &'a FileDicomObject<O>
where
O: DicomObject,
{
type Element = <O as DicomObject>::Element;
fn element(&self, tag: Tag) -> Result<Self::Element, AccessError> {
self.obj.element(tag)
}
fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError> {
self.obj.element_by_name(name)
}
}
impl<O> IntoIterator for FileDicomObject<O>
where
O: IntoIterator,
{
type Item = <O as IntoIterator>::Item;
type IntoIter = <O as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.obj.into_iter()
}
}
impl<'a, O> IntoIterator for &'a FileDicomObject<O>
where
&'a O: IntoIterator,
{
type Item = <&'a O as IntoIterator>::Item;
type IntoIter = <&'a O as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
(&self.obj).into_iter()
}
}
impl<D> PixelDataObject for FileDicomObject<InMemDicomObject<D>>
where
D: DataDictionary + Clone,
{
fn transfer_syntax_uid(&self) -> &str {
self.meta.transfer_syntax()
}
fn rows(&self) -> Option<u16> {
self.get(dicom_dictionary_std::tags::ROWS)?.uint16().ok()
}
fn cols(&self) -> Option<u16> {
self.get(dicom_dictionary_std::tags::COLUMNS)?.uint16().ok()
}
fn samples_per_pixel(&self) -> Option<u16> {
self.get(dicom_dictionary_std::tags::SAMPLES_PER_PIXEL)?
.uint16()
.ok()
}
fn bits_allocated(&self) -> Option<u16> {
self.get(dicom_dictionary_std::tags::BITS_ALLOCATED)?
.uint16()
.ok()
}
fn bits_stored(&self) -> Option<u16> {
self.get(dicom_dictionary_std::tags::BITS_STORED)?
.uint16()
.ok()
}
fn photometric_interpretation(&self) -> Option<&str> {
self.get(dicom_dictionary_std::tags::PHOTOMETRIC_INTERPRETATION)?
.string()
.ok()
.map(|s| s.trim_end())
}
fn number_of_frames(&self) -> Option<u32> {
self.get(dicom_dictionary_std::tags::NUMBER_OF_FRAMES)?
.to_int()
.ok()
}
fn number_of_fragments(&self) -> Option<u32> {
let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
match pixel_data.value() {
dicom_core::DicomValue::Primitive(_p) => Some(1),
dicom_core::DicomValue::PixelSequence(v) => Some(v.fragments().len() as u32),
dicom_core::DicomValue::Sequence(..) => None,
}
}
fn fragment(&self, fragment: usize) -> Option<Cow<[u8]>> {
let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
match pixel_data.value() {
dicom_core::DicomValue::PixelSequence(v) => {
Some(Cow::Borrowed(v.fragments()[fragment].as_ref()))
}
dicom_core::DicomValue::Primitive(p) if fragment == 0 => Some(p.to_bytes()),
_ => None,
}
}
fn offset_table(&self) -> Option<Cow<[u32]>> {
let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
match pixel_data.value() {
dicom_core::DicomValue::Primitive(_) => None,
dicom_core::DicomValue::Sequence(_) => None,
dicom_core::DicomValue::PixelSequence(seq) => Some(Cow::from(seq.offset_table())),
}
}
fn raw_pixel_data(&self) -> Option<RawPixelData> {
let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
match pixel_data.value() {
dicom_core::DicomValue::Primitive(p) => {
let fragment = p.to_bytes().to_vec();
let mut fragments = SmallVec::new();
fragments.push(fragment);
Some(RawPixelData {
fragments,
offset_table: SmallVec::new(),
})
}
dicom_core::DicomValue::PixelSequence(v) => {
let (offset_table, fragments) = v.clone().into_parts();
Some(RawPixelData {
fragments,
offset_table,
})
}
dicom_core::DicomValue::Sequence(..) => None,
}
}
}
#[cfg(test)]
mod tests {
use dicom_core::{DataElement, PrimitiveValue, VR};
use crate::meta::FileMetaTableBuilder;
use crate::{AccessError, FileDicomObject, InMemDicomObject};
fn assert_type_not_too_large<T>(max_size: usize) {
let size = std::mem::size_of::<T>();
if size > max_size {
panic!(
"Type {} of byte size {} exceeds acceptable size {}",
std::any::type_name::<T>(),
size,
max_size
);
}
}
#[test]
fn errors_not_too_large() {
assert_type_not_too_large::<AccessError>(64);
}
#[test]
fn smoke_test() {
const FILE_NAME: &str = ".smoke-test.dcm";
let meta = FileMetaTableBuilder::new()
.transfer_syntax(
dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.uid(),
)
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
.media_storage_sop_instance_uid("1.2.3.456")
.implementation_class_uid("1.2.345.6.7890.1.234")
.build()
.unwrap();
let obj = FileDicomObject::new_empty_with_meta(meta);
obj.write_to_file(FILE_NAME).unwrap();
let obj2 = FileDicomObject::open_file(FILE_NAME).unwrap();
assert_eq!(obj, obj2);
let _ = std::fs::remove_file(FILE_NAME);
}
#[test]
fn file_dicom_object_can_use_inner() {
let mut obj = InMemDicomObject::new_empty();
obj.put(DataElement::new(
dicom_dictionary_std::tags::PATIENT_NAME,
VR::PN,
PrimitiveValue::from("John Doe"),
));
let mut obj = obj
.with_meta(
FileMetaTableBuilder::new()
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7")
.media_storage_sop_instance_uid("1.2.23456789")
.transfer_syntax("1.2.840.10008.1.2.1"),
)
.unwrap();
assert_eq!(
obj.element(dicom_dictionary_std::tags::PATIENT_NAME)
.unwrap()
.value()
.to_str()
.unwrap(),
"John Doe",
);
obj.take_element(dicom_dictionary_std::tags::PATIENT_NAME)
.unwrap();
assert!(matches!(
obj.element(dicom_dictionary_std::tags::PATIENT_NAME),
Err(AccessError::NoSuchDataElementTag { .. }),
));
}
#[test]
fn file_dicom_object_can_iterate_over_elements() {
let mut obj = InMemDicomObject::new_empty();
obj.put(DataElement::new(
dicom_dictionary_std::tags::PATIENT_NAME,
VR::PN,
PrimitiveValue::from("John Doe"),
));
obj.put(DataElement::new(
dicom_dictionary_std::tags::SOP_INSTANCE_UID,
VR::PN,
PrimitiveValue::from("1.2.987654321"),
));
let obj = obj
.with_meta(
FileMetaTableBuilder::new()
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7")
.media_storage_sop_instance_uid("1.2.987654321")
.transfer_syntax("1.2.840.10008.1.2.1"),
)
.unwrap();
let mut iter = (&obj).into_iter();
assert_eq!(
iter.next().unwrap().header().tag,
dicom_dictionary_std::tags::SOP_INSTANCE_UID
);
assert_eq!(
iter.next().unwrap().header().tag,
dicom_dictionary_std::tags::PATIENT_NAME
);
assert_eq!(iter.next(), None);
let mut iter = obj.into_iter();
assert_eq!(
iter.next().unwrap().header().tag,
dicom_dictionary_std::tags::SOP_INSTANCE_UID
);
assert_eq!(
iter.next().unwrap().header().tag,
dicom_dictionary_std::tags::PATIENT_NAME
);
assert_eq!(iter.next(), None);
}
#[test]
pub fn file_dicom_can_update_meta() {
let meta = FileMetaTableBuilder::new()
.transfer_syntax(
dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.uid(),
)
.media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
.media_storage_sop_instance_uid("2.25.280986007517028771599125034987786349815")
.implementation_class_uid("1.2.345.6.7890.1.234")
.build()
.unwrap();
let mut obj = FileDicomObject::new_empty_with_meta(meta);
obj.update_meta(|meta| {
meta.receiving_application_entity_title = Some("SOMETHING".to_string());
});
assert_eq!(
obj.meta().receiving_application_entity_title.as_deref(),
Some("SOMETHING"),
);
}
}