From 840d0b63574d99cde94421eaa3fcf4efea09c5e9 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Fri, 3 Oct 2025 21:42:28 +0200 Subject: [PATCH 01/12] Add now methods to DicomDate and DicomTime --- core/src/value/partial.rs | 21 +- object/src/mem.rs | 4333 ------------------------------------- object/src/meta.rs | 1802 --------------- 3 files changed, 20 insertions(+), 6136 deletions(-) delete mode 100644 object/src/mem.rs delete mode 100644 object/src/meta.rs diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index d8ded521..bfdf355b 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1,7 +1,7 @@ //! Handling of partial precision of Date, Time and DateTime values. use crate::value::AsRange; -use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; use std::fmt; @@ -307,6 +307,15 @@ impl DicomDate { Ok(DicomDate(DicomDateImpl::Day(year, month, day))) } + // Constructs a new `DicomDate` now from the local timezone + pub fn now_local() -> Result { + return DicomDate::try_from(&Local::now()); + } + // Constructs a new `DicomDate` now from the utc timezone + pub fn now_utc() -> Result { + return DicomDate::try_from(&Utc::now()); + } + /// Retrieves the year from a date as a reference pub fn year(&self) -> &u16 { match self { @@ -450,6 +459,16 @@ impl DicomTime { 6, ))) } + + // Constructs a new `DicomDate` now from the local timezone + pub fn now_local() -> Result { + return DicomTime::try_from(&Local::now()); + } + // Constructs a new `DicomDate` now from the utc timezone + pub fn now_utc() -> Result { + return DicomTime::try_from(&Utc::now()); + } + /** Retrieves the hour from a time as a reference */ pub fn hour(&self) -> &u8 { match self { diff --git a/object/src/mem.rs b/object/src/mem.rs deleted file mode 100644 index 376f72b1..00000000 --- a/object/src/mem.rs +++ /dev/null @@ -1,4333 +0,0 @@ -//! This module contains the implementation for an in-memory DICOM object. -//! -//! Use [`InMemDicomObject`] for your DICOM data set construction needs. -//! Values of this type support infallible insertion, removal, and retrieval -//! of elements by DICOM tag, -//! or name (keyword) with a data element dictionary look-up. -//! -//! If you wish to build a complete DICOM file, -//! you can start from an `InMemDicomObject` -//! and complement it with a [file meta group table](crate::meta) -//! (see [`with_meta`](InMemDicomObject::with_meta) -//! and [`with_exact_meta`](InMemDicomObject::with_exact_meta)). -//! -//! # Example -//! -//! A new DICOM data set can be built by providing a sequence of data elements. -//! Insertion and removal methods are also available. -//! -//! ``` -//! # use dicom_core::{DataElement, VR, dicom_value}; -//! # use dicom_dictionary_std::tags; -//! # use dicom_dictionary_std::uids; -//! # use dicom_object::InMemDicomObject; -//! let mut obj = InMemDicomObject::from_element_iter([ -//! DataElement::new(tags::SOP_CLASS_UID, VR::UI, uids::COMPUTED_RADIOGRAPHY_IMAGE_STORAGE), -//! DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.60156688944589400766024286894543900794"), -//! // ... -//! ]); -//! -//! // continue adding elements -//! obj.put(DataElement::new(tags::MODALITY, VR::CS, "CR")); -//! ``` -//! -//! In-memory DICOM objects may have a byte length recorded, -//! if it was part of a data set sequence with explicit length. -//! If necessary, this number can be obtained via the [`HasLength`] trait. -//! However, any modifications made to the object will reset this length -//! to [_undefined_](dicom_core::Length::UNDEFINED). -use dicom_core::ops::{ - ApplyOp, AttributeAction, AttributeOp, AttributeSelector, AttributeSelectorStep, -}; -use dicom_encoding::Codec; -use dicom_parser::dataset::read::{DataSetReaderOptions, OddLengthStrategy}; -use dicom_parser::dataset::write::DataSetWriterOptions; -use dicom_parser::stateful::decode::CharacterSetOverride; -use itertools::Itertools; -use smallvec::SmallVec; -use snafu::{ensure, OptionExt, ResultExt}; -use std::borrow::Cow; -use std::fs::File; -use std::io::{BufRead, BufReader, Read}; -use std::path::Path; -use std::{collections::BTreeMap, io::Write}; - -use crate::file::ReadPreamble; -use crate::ops::{ - ApplyError, ApplyResult, IncompatibleTypesSnafu, ModifySnafu, UnsupportedActionSnafu, -}; -use crate::{meta::FileMetaTable, FileMetaTableBuilder}; -use crate::{ - AccessByNameError, AccessError, AtAccessError, BuildMetaTableSnafu, CreateParserSnafu, - CreatePrinterSnafu, DicomObject, ElementNotFoundSnafu, FileDicomObject, InvalidGroupSnafu, - MissingElementValueSnafu, MissingLeafElementSnafu, NoSpaceSnafu, NoSuchAttributeNameSnafu, - NoSuchDataElementAliasSnafu, NoSuchDataElementTagSnafu, NotASequenceSnafu, OpenFileSnafu, - ParseMetaDataSetSnafu, ParseSopAttributeSnafu, PrematureEndSnafu, PrepareMetaTableSnafu, - PrintDataSetSnafu, PrivateCreatorNotFoundSnafu, PrivateElementError, ReadError, ReadFileSnafu, - ReadPreambleBytesSnafu, ReadTokenSnafu, ReadUnrecognizedTransferSyntaxSnafu, - ReadUnsupportedTransferSyntaxSnafu, ReadUnsupportedTransferSyntaxWithSuggestionSnafu, - UnexpectedTokenSnafu, WithMetaError, WriteError, -}; -use dicom_core::dictionary::{DataDictionary, DataDictionaryEntry}; -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, uids, StandardDataDictionary}; -use dicom_encoding::transfer_syntax::TransferSyntaxIndex; -use dicom_encoding::{encode::EncodeTo, text::SpecificCharacterSet, TransferSyntax}; -use dicom_parser::dataset::{DataSetReader, DataToken, IntoTokensOptions}; -use dicom_parser::{ - dataset::{read::Error as ParserError, DataSetWriter, IntoTokens}, - StatefulDecode, -}; -use dicom_transfer_syntax_registry::TransferSyntaxRegistry; - -/// A full in-memory DICOM data element. -pub type InMemElement = DataElement, InMemFragment>; - -/// The type of a pixel data fragment. -pub type InMemFragment = dicom_core::value::InMemFragment; - -type Result = std::result::Result; - -type ParserResult = std::result::Result; - -/// A DICOM object that is fully contained in memory. -/// -/// See the [module-level documentation](self) -/// for more details. -#[derive(Debug, Clone)] -pub struct InMemDicomObject { - /// the element map - entries: BTreeMap>, - /// the data dictionary - dict: D, - /// The length of the DICOM object in bytes. - /// It is usually undefined, unless it is part of an item - /// in a sequence with a specified length in its item header. - len: Length, - /// In case the SpecificCharSet changes we need to mark the object as dirty, - /// because changing the character set may change the length in bytes of - /// stored text. It has to be public for now because we need - pub(crate) charset_changed: bool, -} - -impl PartialEq for InMemDicomObject { - // This implementation ignores the data dictionary. - fn eq(&self, other: &Self) -> bool { - self.entries == other.entries - } -} - -impl HasLength for InMemDicomObject { - fn length(&self) -> Length { - self.len - } -} - -impl HasLength for &InMemDicomObject { - fn length(&self) -> Length { - self.len - } -} - -impl DicomObject for InMemDicomObject -where - D: DataDictionary, - D: Clone, -{ - type Attribute<'a> = &'a Value, InMemFragment> - where Self: 'a; - - type LeafAttribute<'a> = &'a Value, InMemFragment> - where Self: 'a; - - #[inline] - fn attr_opt(&self, tag: Tag) -> Result>> { - let elem = InMemDicomObject::element_opt(self, tag)?; - Ok(elem.map(|e| e.value())) - } - - #[inline] - fn attr_by_name_opt( - &self, - name: &str, - ) -> Result>, AccessByNameError> { - let elem = InMemDicomObject::element_by_name_opt(self, name)?; - Ok(elem.map(|e| e.value())) - } - - #[inline] - fn attr(&self, tag: Tag) -> Result> { - let elem = InMemDicomObject::element(self, tag)?; - Ok(elem.value()) - } - - #[inline] - fn attr_by_name(&self, name: &str) -> Result, AccessByNameError> { - let elem = InMemDicomObject::element_by_name(self, name)?; - Ok(elem.value()) - } - - #[inline] - fn at(&self, selector: impl Into) -> Result, AtAccessError> { - self.value_at(selector) - } -} - -impl<'s, D: 's> DicomObject for &'s InMemDicomObject -where - D: DataDictionary, - D: Clone, -{ - type Attribute<'a> = &'a Value, InMemFragment> - where Self: 'a, - 's: 'a; - - type LeafAttribute<'a> = &'a Value, InMemFragment> - where Self: 'a, - 's: 'a; - - #[inline] - fn attr_opt(&self, tag: Tag) -> Result>> { - let elem = InMemDicomObject::element_opt(*self, tag)?; - Ok(elem.map(|e| e.value())) - } - - #[inline] - fn attr_by_name_opt( - &self, - name: &str, - ) -> Result>, AccessByNameError> { - let elem = InMemDicomObject::element_by_name_opt(*self, name)?; - Ok(elem.map(|e| e.value())) - } - - #[inline] - fn attr(&self, tag: Tag) -> Result> { - let elem = InMemDicomObject::element(*self, tag)?; - Ok(elem.value()) - } - - #[inline] - fn attr_by_name(&self, name: &str) -> Result, AccessByNameError> { - let elem = InMemDicomObject::element_by_name(*self, name)?; - Ok(elem.value()) - } - - #[inline] - fn at(&self, selector: impl Into) -> Result, AtAccessError> { - self.value_at(selector) - } -} - -impl FileDicomObject> { - /// Create a DICOM object by reading from a file. - /// - /// This function assumes the standard file encoding structure: - /// first it automatically detects whether the 128-byte preamble is present, - /// skipping it if found. - /// Then it reads the file meta group, - /// followed by the rest of the data set. - pub fn open_file>(path: P) -> Result { - Self::open_file_with_dict(path, StandardDataDictionary) - } - - /// Create a DICOM object by reading from a byte source. - /// - /// This function assumes the standard file encoding structure: - /// first it automatically detects whether the 128-byte preamble is present, - /// skipping it if found. - /// Then it reads the file meta group, - /// followed by the rest of the data set. - pub fn from_reader(src: S) -> Result - where - S: Read, - { - Self::from_reader_with_dict(src, StandardDataDictionary) - } -} - -impl InMemDicomObject { - /// Create a new empty DICOM object. - pub fn new_empty() -> Self { - InMemDicomObject { - entries: BTreeMap::new(), - dict: StandardDataDictionary, - len: Length::UNDEFINED, - charset_changed: false, - } - } - - /// Construct a DICOM object from a fallible source of structured elements. - #[inline] - pub fn from_element_source(iter: I) -> Result - where - I: IntoIterator>>, - { - Self::from_element_source_with_dict(iter, StandardDataDictionary) - } - - /// Construct a DICOM object from a non-fallible source of structured elements. - #[inline] - pub fn from_element_iter(iter: I) -> Self - where - I: IntoIterator>, - { - Self::from_iter_with_dict(iter, StandardDataDictionary) - } - - /// Construct a DICOM object representing a command set, - /// from a non-fallible iterator of structured elements. - /// - /// This method will automatically insert - /// a _Command Group Length_ (0000,0000) element - /// based on the command elements found in the sequence. - #[inline] - pub fn command_from_element_iter(iter: I) -> Self - where - I: IntoIterator>, - { - Self::command_from_iter_with_dict(iter, StandardDataDictionary) - } - - /// Read an object from a source using the given decoder. - /// - /// Note: [`read_dataset_with_ts`] and [`read_dataset_with_ts_cs`] - /// may be easier to use. - /// - /// [`read_dataset_with_ts`]: InMemDicomObject::read_dataset_with_ts - /// [`read_dataset_with_ts_cs`]: InMemDicomObject::read_dataset_with_ts_cs - #[inline] - pub fn read_dataset(decoder: S) -> Result - where - S: StatefulDecode, - { - Self::read_dataset_with_dict(decoder, StandardDataDictionary) - } - - /// Read an object from a source, - /// using the given transfer syntax and default character set. - /// - /// If the attribute _Specific Character Set_ is found in the encoded data, - /// this will override the given character set. - #[inline] - pub fn read_dataset_with_ts_cs( - from: S, - ts: &TransferSyntax, - cs: SpecificCharacterSet, - ) -> Result - where - S: Read, - { - Self::read_dataset_with_dict_ts_cs(from, StandardDataDictionary, ts, cs) - } - - /// Read an object from a source, - /// using the given transfer syntax. - /// - /// The default character set is assumed - /// until _Specific Character Set_ is found in the encoded data, - /// after which the text decoder will be overridden accordingly. - #[inline] - pub fn read_dataset_with_ts(from: S, ts: &TransferSyntax) -> Result - where - S: Read, - { - Self::read_dataset_with_dict_ts_cs( - from, - StandardDataDictionary, - ts, - SpecificCharacterSet::default(), - ) - } -} - -impl FileDicomObject> -where - D: DataDictionary, - D: Clone, -{ - /// Create a new empty object, using the given dictionary and - /// file meta table. - pub fn new_empty_with_dict_and_meta(dict: D, meta: FileMetaTable) -> Self { - FileDicomObject { - meta, - obj: InMemDicomObject { - entries: BTreeMap::new(), - dict, - len: Length::UNDEFINED, - charset_changed: false, - }, - } - } - - /// Create a DICOM object by reading from a file. - /// - /// This function assumes the standard file encoding structure: - /// first it automatically detects whether the 128-byte preamble is present, - /// skipping it when found. - /// Then it reads the file meta group, - /// followed by the rest of the data set. - pub fn open_file_with_dict>(path: P, dict: D) -> Result { - Self::open_file_with(path, dict, TransferSyntaxRegistry) - } - - /// Create a DICOM object by reading from a file. - /// - /// This function assumes the standard file encoding structure: - /// first it automatically detects whether the 128-byte preamble is present, - /// skipping it when found. - /// Then it reads the file meta group, - /// followed by the rest of the data set. - /// - /// This function allows you to choose a different transfer syntax index, - /// but its use is only advised when the built-in transfer syntax registry - /// is insufficient. Otherwise, please use [`open_file_with_dict`] instead. - /// - /// [`open_file_with_dict`]: #method.open_file_with_dict - pub fn open_file_with(path: P, dict: D, ts_index: R) -> Result - where - P: AsRef, - R: TransferSyntaxIndex, - { - Self::open_file_with_all_options( - path, - dict, - ts_index, - None, - ReadPreamble::Auto, - Default::default(), - Default::default(), - ) - } - - pub(crate) fn open_file_with_all_options( - path: P, - dict: D, - ts_index: R, - read_until: Option, - mut read_preamble: ReadPreamble, - odd_length: OddLengthStrategy, - charset_override: CharacterSetOverride, - ) -> Result - where - P: AsRef, - R: TransferSyntaxIndex, - { - let path = path.as_ref(); - let mut file = - BufReader::new(File::open(path).with_context(|_| OpenFileSnafu { filename: path })?); - - if read_preamble == ReadPreamble::Auto { - read_preamble = Self::detect_preamble(&mut file) - .with_context(|_| ReadFileSnafu { filename: path })?; - } - - if read_preamble == ReadPreamble::Auto || read_preamble == ReadPreamble::Always { - let mut buf = [0u8; 128]; - // skip the preamble - file.read_exact(&mut buf) - .with_context(|_| ReadFileSnafu { filename: path })?; - } - - Self::read_parts_with_all_options_impl( - file, - dict, - ts_index, - read_until, - odd_length, - charset_override, - ) - } - - /// Create a DICOM object by reading from a byte source. - /// - /// This function assumes the standard file encoding structure: - /// first it automatically detects whether the 128-byte preamble is present, - /// skipping it when found. - /// Then it reads the file meta group, - /// followed by the rest of the data set. - pub fn from_reader_with_dict(src: S, dict: D) -> Result - where - S: Read, - { - Self::from_reader_with(src, dict, TransferSyntaxRegistry) - } - - /// Create a DICOM object by reading from a byte source. - /// - /// This function assumes the standard file encoding structure: - /// first it automatically detects whether the preamble is present, - /// skipping it when found. - /// Then it reads the file meta group, - /// followed by the rest of the data set. - /// - /// This function allows you to choose a different transfer syntax index, - /// but its use is only advised when the built-in transfer syntax registry - /// is insufficient. Otherwise, please use [`from_reader_with_dict`] instead. - /// - /// [`from_reader_with_dict`]: #method.from_reader_with_dict - pub fn from_reader_with(src: S, dict: D, ts_index: R) -> Result - where - S: Read, - R: TransferSyntaxIndex, - { - Self::from_reader_with_all_options( - src, - dict, - ts_index, - None, - ReadPreamble::Auto, - Default::default(), - Default::default(), - ) - } - - pub(crate) fn from_reader_with_all_options( - src: S, - dict: D, - ts_index: R, - read_until: Option, - mut read_preamble: ReadPreamble, - odd_length: OddLengthStrategy, - charset_override: CharacterSetOverride, - ) -> Result - where - S: Read, - R: TransferSyntaxIndex, - { - let mut file = BufReader::new(src); - - if read_preamble == ReadPreamble::Auto { - read_preamble = Self::detect_preamble(&mut file).context(ReadPreambleBytesSnafu)?; - } - - if read_preamble == ReadPreamble::Always { - // skip preamble - let mut buf = [0u8; 128]; - // skip the preamble - file.read_exact(&mut buf).context(ReadPreambleBytesSnafu)?; - } - - Self::read_parts_with_all_options_impl( - file, - dict, - ts_index, - read_until, - odd_length, - charset_override, - ) - } - - // detect the presence of a preamble - // and provide a better `ReadPreamble` option accordingly - fn detect_preamble(reader: &mut BufReader) -> std::io::Result - where - S: Read, - { - let buf = reader.fill_buf()?; - let buflen = buf.len(); - - if buflen < 4 { - return Err(std::io::ErrorKind::UnexpectedEof.into()); - } - - if buflen >= 132 && &buf[128..132] == b"DICM" { - return Ok(ReadPreamble::Always); - } - - if &buf[0..4] == b"DICM" { - return Ok(ReadPreamble::Never); - } - - // could not detect - Ok(ReadPreamble::Auto) - } - - /// Common implementation for reading the file meta group - /// and the main data set (expects no preamble and no magic code), - /// according to the file's transfer syntax and the given options. - /// - /// If Media Storage SOP Class UID or Media Storage SOP Instance UID - /// are missing in the file meta group, - /// this function will attempt to populate them from the main data set. - fn read_parts_with_all_options_impl( - mut src: BufReader, - dict: D, - ts_index: R, - read_until: Option, - odd_length: OddLengthStrategy, - charset_override: CharacterSetOverride, - ) -> Result - where - S: Read, - R: TransferSyntaxIndex, - { - // read metadata header - let mut meta = FileMetaTable::from_reader(&mut src).context(ParseMetaDataSetSnafu)?; - - let ts_uid = meta.transfer_syntax(); - // read rest of data according to metadata, feed it to object - if let Some(ts) = ts_index.get(ts_uid) { - let mut options = DataSetReaderOptions::default(); - options.odd_length = odd_length; - options.charset_override = charset_override; - - let obj = match ts.codec() { - Codec::Dataset(Some(adapter)) => { - let adapter = adapter.adapt_reader(Box::new(src)); - let mut dataset = DataSetReader::new_with_ts_options(adapter, ts, options) - .context(CreateParserSnafu)?; - InMemDicomObject::build_object( - &mut dataset, - dict, - false, - Length::UNDEFINED, - read_until, - )? - } - Codec::Dataset(None) => { - if ts_uid == uids::DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN - || ts_uid == uids::JPIP_REFERENCED_DEFLATE - || ts_uid == uids::JPIPHTJ2K_REFERENCED_DEFLATE - { - return ReadUnsupportedTransferSyntaxWithSuggestionSnafu { - uid: ts.uid(), - name: ts.name(), - feature_name: "dicom-transfer-syntax-registry/deflate", - } - .fail(); - } - - return ReadUnsupportedTransferSyntaxSnafu { - uid: ts.uid(), - name: ts.name(), - } - .fail(); - } - Codec::None | Codec::EncapsulatedPixelData(..) => { - let mut dataset = DataSetReader::new_with_ts_options(src, ts, options) - .context(CreateParserSnafu)?; - InMemDicomObject::build_object( - &mut dataset, - dict, - false, - Length::UNDEFINED, - read_until, - )? - } - }; - - // if Media Storage SOP Class UID is empty attempt to infer from SOP Class UID - if meta.media_storage_sop_class_uid().is_empty() { - if let Some(elem) = obj.get(tags::SOP_CLASS_UID) { - meta.media_storage_sop_class_uid = elem - .value() - .to_str() - .context(ParseSopAttributeSnafu)? - .to_string(); - } - } - - // if Media Storage SOP Instance UID is empty attempt to infer from SOP Instance UID - if meta.media_storage_sop_instance_uid().is_empty() { - if let Some(elem) = obj.get(tags::SOP_INSTANCE_UID) { - meta.media_storage_sop_instance_uid = elem - .value() - .to_str() - .context(ParseSopAttributeSnafu)? - .to_string(); - } - } - - Ok(FileDicomObject { meta, obj }) - } else { - ReadUnrecognizedTransferSyntaxSnafu { - uid: ts_uid.to_string(), - } - .fail() - } - } -} - -impl FileDicomObject> { - /// Create a new empty object, using the given file meta table. - pub fn new_empty_with_meta(meta: FileMetaTable) -> Self { - FileDicomObject { - meta, - obj: InMemDicomObject { - entries: BTreeMap::new(), - dict: StandardDataDictionary, - len: Length::UNDEFINED, - charset_changed: false, - }, - } - } -} - -impl InMemDicomObject -where - D: DataDictionary, - D: Clone, -{ - /// Create a new empty object, using the given dictionary for name lookup. - pub fn new_empty_with_dict(dict: D) -> Self { - InMemDicomObject { - entries: BTreeMap::new(), - dict, - len: Length::UNDEFINED, - charset_changed: false, - } - } - - /// Construct a DICOM object from an iterator of structured elements. - pub fn from_element_source_with_dict(iter: I, dict: D) -> Result - where - I: IntoIterator>>, - { - let entries: Result<_> = iter.into_iter().map_ok(|e| (e.tag(), e)).collect(); - Ok(InMemDicomObject { - entries: entries?, - dict, - len: Length::UNDEFINED, - charset_changed: false, - }) - } - - /// Construct a DICOM object from a non-fallible iterator of structured elements. - pub fn from_iter_with_dict(iter: I, dict: D) -> Self - where - I: IntoIterator>, - { - let entries = iter.into_iter().map(|e| (e.tag(), e)).collect(); - InMemDicomObject { - entries, - dict, - len: Length::UNDEFINED, - charset_changed: false, - } - } - - /// Construct a DICOM object representing a command set, - /// from a non-fallible iterator of structured elements. - /// - /// This method will automatically insert - /// a _Command Group Length_ (0000,0000) element - /// based on the command elements found in the sequence. - pub fn command_from_iter_with_dict(iter: I, dict: D) -> Self - where - I: IntoIterator>, - { - let mut calculated_length: u32 = 0; - let mut entries: BTreeMap<_, _> = iter - .into_iter() - .map(|e| { - // count the length of command set elements - if e.tag().0 == 0x0000 && e.tag().1 != 0x0000 { - let l = e.value().length(); - calculated_length += if l.is_defined() { even_len(l.0) } else { 0 } + 8; - } - - (e.tag(), e) - }) - .collect(); - - entries.insert( - Tag(0, 0), - InMemElement::new(Tag(0, 0), VR::UL, PrimitiveValue::from(calculated_length)), - ); - - InMemDicomObject { - entries, - dict, - len: Length::UNDEFINED, - charset_changed: false, - } - } - - /// Read an object from a source, - /// using the given decoder - /// and the given dictionary for name lookup. - pub fn read_dataset_with_dict(decoder: S, dict: D) -> Result - where - S: StatefulDecode, - D: DataDictionary, - { - let mut dataset = DataSetReader::new(decoder, Default::default()); - InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) - } - - /// Read an object from a source, - /// using the given data dictionary and transfer syntax. - #[inline] - pub fn read_dataset_with_dict_ts( - from: S, - dict: D, - ts: &TransferSyntax, - ) -> Result - where - S: Read, - D: DataDictionary, - { - Self::read_dataset_with_dict_ts_cs(from, dict, ts, SpecificCharacterSet::default()) - } - - /// Read an object from a source, - /// using the given data dictionary, - /// transfer syntax, - /// and the given character set to assume by default. - /// - /// If the attribute _Specific Character Set_ is found in the encoded data, - /// this will override the given character set. - pub fn read_dataset_with_dict_ts_cs( - from: S, - dict: D, - ts: &TransferSyntax, - cs: SpecificCharacterSet, - ) -> Result - where - S: Read, - D: DataDictionary, - { - let from = BufReader::new(from); - - match ts.codec() { - Codec::Dataset(Some(adapter)) => { - let adapter = adapter.adapt_reader(Box::new(from)); - let mut dataset = - DataSetReader::new_with_ts_cs(adapter, ts, cs).context(CreateParserSnafu)?; - InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) - } - Codec::Dataset(None) => { - let uid = ts.uid(); - if uid == uids::DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN - || uid == uids::JPIP_REFERENCED_DEFLATE - || uid == uids::JPIPHTJ2K_REFERENCED_DEFLATE - { - return ReadUnsupportedTransferSyntaxWithSuggestionSnafu { - uid, - name: ts.name(), - feature_name: "dicom-transfer-syntax-registry/deflate", - } - .fail(); - } - - ReadUnsupportedTransferSyntaxSnafu { - uid, - name: ts.name(), - } - .fail() - } - Codec::None | Codec::EncapsulatedPixelData(..) => { - let mut dataset = - DataSetReader::new_with_ts_cs(from, ts, cs).context(CreateParserSnafu)?; - InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) - } - } - } - - // Standard methods follow. They are not placed as a trait implementation - // because they may require outputs to reference the lifetime of self, - // which is not possible without GATs. - - /// Retrieve a particular DICOM element by its tag. - /// - /// An error is returned if the element does not exist. - /// For an alternative to this behavior, - /// see [`element_opt`](InMemDicomObject::element_opt). - pub fn element(&self, tag: Tag) -> Result<&InMemElement> { - self.entries - .get(&tag) - .context(NoSuchDataElementTagSnafu { tag }) - } - - /// Retrieve a particular DICOM element by its name. - /// - /// This method translates the given attribute name into its tag - /// before retrieving the element. - /// If the attribute is known in advance, - /// using [`element`](InMemDicomObject::element) - /// with a tag constant is preferred. - /// - /// An error is returned if the element does not exist. - /// For an alternative to this behavior, - /// see [`element_by_name_opt`](InMemDicomObject::element_by_name_opt). - pub fn element_by_name(&self, name: &str) -> Result<&InMemElement, AccessByNameError> { - let tag = self.lookup_name(name)?; - self.entries - .get(&tag) - .with_context(|| NoSuchDataElementAliasSnafu { - tag, - alias: name.to_string(), - }) - } - - /// Retrieve a particular DICOM element that might not exist by its tag. - /// - /// If the element does not exist, - /// `None` is returned. - pub fn element_opt(&self, tag: Tag) -> Result>, AccessError> { - match self.element(tag) { - Ok(e) => Ok(Some(e)), - Err(super::AccessError::NoSuchDataElementTag { .. }) => Ok(None), - } - } - - /// Get a particular DICOM attribute from this object by tag. - /// - /// If the element does not exist, - /// `None` is returned. - pub fn get(&self, tag: Tag) -> Option<&InMemElement> { - self.entries.get(&tag) - } - - // Get a mutable reference to a particular DICOM attribute from this object by tag. - // - // Should be private as it would allow a user to change the tag of an - // element and diverge from the dictionary - fn get_mut(&mut self, tag: Tag) -> Option<&mut InMemElement> { - self.entries.get_mut(&tag) - } - - /// Retrieve a particular DICOM element that might not exist by its name. - /// - /// If the element does not exist, - /// `None` is returned. - /// - /// This method translates the given attribute name into its tag - /// before retrieving the element. - /// If the attribute is known in advance, - /// using [`element_opt`](InMemDicomObject::element_opt) - /// with a tag constant is preferred. - pub fn element_by_name_opt( - &self, - name: &str, - ) -> Result>, AccessByNameError> { - match self.element_by_name(name) { - Ok(e) => Ok(Some(e)), - Err(AccessByNameError::NoSuchDataElementAlias { .. }) => Ok(None), - Err(e) => Err(e), - } - } - - 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/medical/dicom/2024a/output/chtml/part05/sect_7.8.html - if elem.header().vr() == VR::LO && elem.to_str().unwrap_or_default() == creator { - return Some(tag); - } - } - None - } - - /// Get a private element from the dataset using the group number, creator and element number. - /// - /// An error is raised when the group number is not odd, - /// the private creator is not found in the group, - /// or the private element is not found. - /// - /// For more info, see the [DICOM standard section on private elements][1]. - /// - /// [1]: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part05/sect_7.8.html - /// - /// ## Example - /// - /// ``` - /// # use dicom_core::{VR, PrimitiveValue, Tag, DataElement}; - /// # use dicom_object::{InMemDicomObject, PrivateElementError}; - /// # use std::error::Error; - /// let mut ds = InMemDicomObject::from_element_iter([ - /// DataElement::new( - /// Tag(0x0009, 0x0010), - /// VR::LO, - /// PrimitiveValue::from("CREATOR 1"), - /// ), - /// DataElement::new(Tag(0x0009, 0x01001), VR::DS, "1.0"), - /// ]); - /// assert_eq!( - /// ds.private_element(0x0009, "CREATOR 1", 0x01)? - /// .value() - /// .to_str()?, - /// "1.0" - /// ); - /// # Ok::<(), Box>(()) - /// ``` - pub fn private_element( - &self, - group: GroupNumber, - creator: &str, - element: u8, - ) -> Result<&InMemElement, 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 - /// element changes. - pub fn put(&mut self, elt: InMemElement) -> Option> { - self.put_element(elt) - } - - /// 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 - /// element changes. - pub fn put_element(&mut self, elt: InMemElement) -> Option> { - self.len = Length::UNDEFINED; - self.invalidate_if_charset_changed(elt.tag()); - self.entries.insert(elt.tag(), elt) - } - - /// Insert a private element into the dataset, replacing (and returning) any - /// previous element of the same attribute. - /// - /// This function will find the next available private element block in the given - /// group. If the creator already exists, the element will be added to the block - /// already reserved for that creator. If it does not exist, then a new block - /// will be reserved for the creator in the specified group. - /// An error is returned if there is no space left in the group. - /// - /// For more info, see the [DICOM standard section on private elements][1]. - /// - /// [1]: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part05/sect_7.8.html - /// - /// ## Example - /// ``` - /// # use dicom_core::{VR, PrimitiveValue, Tag, DataElement, header::Header}; - /// # use dicom_object::InMemDicomObject; - /// # use std::error::Error; - /// let mut ds = InMemDicomObject::new_empty(); - /// ds.put_private_element( - /// 0x0009, - /// "CREATOR 1", - /// 0x02, - /// VR::DS, - /// PrimitiveValue::from("1.0"), - /// )?; - /// assert_eq!( - /// ds.private_element(0x0009, "CREATOR 1", 0x02)? - /// .value() - /// .to_str()?, - /// "1.0" - /// ); - /// assert_eq!( - /// ds.private_element(0x0009, "CREATOR 1", 0x02)? - /// .header() - /// .tag(), - /// Tag(0x0009, 0x0102) - /// ); - /// # Ok::<(), Box>(()) - /// ``` - pub fn put_private_element( - &mut self, - group: GroupNumber, - creator: &str, - element: u8, - vr: VR, - value: PrimitiveValue, - ) -> Result>, 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); - Ok(self.put_element(DataElement::new(tag, vr, value))) - } else { - // Find last reserved block of tags. - let range = Tag(group, 0)..Tag(group, 0xFF); - let last_entry = self.entries.range(range).next_back(); - 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); - Ok(self.put_element(DataElement::new(tag, vr, value))) - } else { - NoSpaceSnafu { group }.fail() - } - } - } - - /// 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( - &mut self, - tag: Tag, - vr: VR, - string: impl Into, - ) -> Option> { - self.put_element(DataElement::new(tag, vr, string.into())) - } - - /// Remove a DICOM element by its tag, - /// reporting whether it was present. - pub fn remove_element(&mut self, tag: Tag) -> bool { - if self.entries.remove(&tag).is_some() { - self.len = Length::UNDEFINED; - true - } else { - false - } - } - - /// Remove a DICOM element by its keyword, - /// reporting whether it was present. - pub fn remove_element_by_name(&mut self, name: &str) -> Result { - let tag = self.lookup_name(name)?; - Ok(self.entries.remove(&tag).is_some()).map(|removed| { - if removed { - self.len = Length::UNDEFINED; - } - removed - }) - } - - /// Remove and return a particular DICOM element by its tag. - pub fn take_element(&mut self, tag: Tag) -> Result> { - self.entries - .remove(&tag) - .map(|e| { - self.len = Length::UNDEFINED; - e - }) - .context(NoSuchDataElementTagSnafu { tag }) - } - - /// Remove and return a particular DICOM element by its tag, - /// if it is present, - /// returns `None` otherwise. - pub fn take(&mut self, tag: Tag) -> Option> { - self.entries.remove(&tag).map(|e| { - self.len = Length::UNDEFINED; - e - }) - } - - /// Remove and return a particular DICOM element by its name. - pub fn take_element_by_name( - &mut self, - name: &str, - ) -> Result, AccessByNameError> { - let tag = self.lookup_name(name)?; - self.entries - .remove(&tag) - .map(|e| { - self.len = Length::UNDEFINED; - e - }) - .with_context(|| NoSuchDataElementAliasSnafu { - tag, - alias: name.to_string(), - }) - } - - /// Modify the object by - /// retaining only the DICOM data elements specified by the predicate. - /// - /// The elements are visited in ascending tag order, - /// and those for which `f(&element)` returns `false` are removed. - pub fn retain(&mut self, mut f: impl FnMut(&InMemElement) -> bool) { - self.entries.retain(|_, elem| f(elem)); - self.len = Length::UNDEFINED; - } - - /// Obtain a temporary mutable reference to a DICOM value by tag, - /// so that mutations can be applied within. - /// - /// If found, this method resets all related lengths recorded - /// and returns `true`. - /// Returns `false` otherwise. - /// - /// # Example - /// - /// ``` - /// # use dicom_core::{DataElement, VR, dicom_value}; - /// # use dicom_dictionary_std::tags; - /// # use dicom_object::InMemDicomObject; - /// let mut obj = InMemDicomObject::from_element_iter([ - /// DataElement::new(tags::LOSSY_IMAGE_COMPRESSION_RATIO, VR::DS, dicom_value!(Strs, ["25"])), - /// ]); - /// - /// // update lossy image compression ratio - /// obj.update_value(tags::LOSSY_IMAGE_COMPRESSION_RATIO, |e| { - /// e.primitive_mut().unwrap().extend_str(["2.56"]); - /// }); - /// - /// assert_eq!( - /// obj.get(tags::LOSSY_IMAGE_COMPRESSION_RATIO).unwrap().value().to_str().unwrap(), - /// "25\\2.56" - /// ); - /// ``` - pub fn update_value( - &mut self, - tag: Tag, - f: impl FnMut(&mut Value, InMemFragment>), - ) -> bool { - self.invalidate_if_charset_changed(tag); - if let Some(e) = self.entries.get_mut(&tag) { - e.update_value(f); - self.len = Length::UNDEFINED; - true - } else { - false - } - } - - /// Obtain a temporary mutable reference to a DICOM value by AttributeSelector, - /// so that mutations can be applied within. - /// - /// If found, this method resets all related lengths recorded - /// and returns `true`. - /// Returns `false` otherwise. - /// - /// See the documentation of [`AttributeSelector`] for more information - /// on how to write attribute selectors. - /// - /// Note: Consider using [`apply`](ApplyOp::apply) when possible. - /// - /// # Example - /// - /// ``` - /// # use dicom_core::{DataElement, VR, dicom_value, value::DataSetSequence}; - /// # use dicom_dictionary_std::tags; - /// # use dicom_object::InMemDicomObject; - /// # use dicom_core::ops::{AttributeAction, AttributeOp, ApplyOp}; - /// let mut dcm = InMemDicomObject::from_element_iter([ - /// DataElement::new( - /// tags::OTHER_PATIENT_I_DS_SEQUENCE, - /// VR::SQ, - /// DataSetSequence::from(vec![InMemDicomObject::from_element_iter([ - /// DataElement::new( - /// tags::PATIENT_ID, - /// VR::LO, - /// dicom_value!(Str, "1234") - /// )]) - /// ]) - /// ), - /// ]); - /// let selector = ( - /// tags::OTHER_PATIENT_I_DS_SEQUENCE, - /// 0, - /// tags::PATIENT_ID - /// ); - /// - /// // update referenced SOP instance UID for deidentification potentially - /// dcm.update_value_at(*&selector, |e| { - /// let mut v = e.primitive_mut().unwrap(); - /// *v = dicom_value!(Str, "abcd"); - /// }); - /// - /// assert_eq!( - /// dcm.entry_at(*&selector).unwrap().value().to_str().unwrap(), - /// "abcd" - /// ); - /// ``` - pub fn update_value_at( - &mut self, - selector: impl Into, - f: impl FnMut(&mut Value, InMemFragment>), - ) -> Result<(), AtAccessError> { - self.entry_at_mut(selector) - .map(|e| e.update_value(f)) - .map(|_| { - self.len = Length::UNDEFINED; - }) - } - - /// Obtain the DICOM value by finding the element - /// that matches the given selector. - /// - /// Returns an error if the respective element or any of its parents - /// cannot be found. - /// - /// See the documentation of [`AttributeSelector`] for more information - /// on how to write attribute selectors. - /// - /// # Example - /// - /// ```no_run - /// # use dicom_core::prelude::*; - /// # use dicom_core::ops::AttributeSelector; - /// # use dicom_dictionary_std::tags; - /// # use dicom_object::InMemDicomObject; - /// # let obj: InMemDicomObject = unimplemented!(); - /// let referenced_sop_instance_iod = obj.value_at( - /// ( - /// tags::SHARED_FUNCTIONAL_GROUPS_SEQUENCE, - /// tags::REFERENCED_IMAGE_SEQUENCE, - /// tags::REFERENCED_SOP_INSTANCE_UID, - /// ))? - /// .to_str()?; - /// # Ok::<_, Box>(()) - /// ``` - pub fn value_at( - &self, - selector: impl Into, - ) -> Result<&Value, InMemFragment>, AtAccessError> { - let selector: AttributeSelector = selector.into(); - - let mut obj = self; - for (i, step) in selector.iter().enumerate() { - match step { - // reached the leaf - AttributeSelectorStep::Tag(tag) => { - return obj.get(*tag).map(|e| e.value()).with_context(|| { - MissingLeafElementSnafu { - selector: selector.clone(), - } - }); - } - // navigate further down - AttributeSelectorStep::Nested { tag, item } => { - let e = obj - .entries - .get(tag) - .with_context(|| crate::MissingSequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - - // get items - let items = e.items().with_context(|| NotASequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - - // if item.length == i and action is a constructive action, append new item - obj = - items - .get(*item as usize) - .with_context(|| crate::MissingSequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - } - } - } - - unreachable!() - } - - /// Change the 'specific_character_set' tag to ISO_IR 192, marking the dataset as UTF-8 - pub fn convert_to_utf8(&mut self) { - self.put(DataElement::new( - tags::SPECIFIC_CHARACTER_SET, - VR::CS, - "ISO_IR 192", - )); - } - - /// Get a DataElement by AttributeSelector - /// - /// If the element or other intermediate elements do not exist, the method will return an error. - /// - /// See the documentation of [`AttributeSelector`] for more information - /// on how to write attribute selectors. - /// - /// If you only need the value, use [`value_at`](Self::value_at). - pub fn entry_at( - &self, - selector: impl Into, - ) -> Result<&InMemElement, AtAccessError> { - let selector: AttributeSelector = selector.into(); - - let mut obj = self; - for (i, step) in selector.iter().enumerate() { - match step { - // reached the leaf - AttributeSelectorStep::Tag(tag) => { - return obj.get(*tag).with_context(|| MissingLeafElementSnafu { - selector: selector.clone(), - }) - } - // navigate further down - AttributeSelectorStep::Nested { tag, item } => { - let e = obj - .entries - .get(tag) - .with_context(|| crate::MissingSequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - - // get items - let items = e.items().with_context(|| NotASequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - - // if item.length == i and action is a constructive action, append new item - obj = - items - .get(*item as usize) - .with_context(|| crate::MissingSequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - } - } - } - - unreachable!() - } - - // Get a mutable reference to a particular entry by AttributeSelector - // - // Should be private for the same reason as `self.get_mut` - fn entry_at_mut( - &mut self, - selector: impl Into, - ) -> Result<&mut InMemElement, AtAccessError> { - let selector: AttributeSelector = selector.into(); - - let mut obj = self; - for (i, step) in selector.iter().enumerate() { - match step { - // reached the leaf - AttributeSelectorStep::Tag(tag) => { - return obj.get_mut(*tag).with_context(|| MissingLeafElementSnafu { - selector: selector.clone(), - }) - } - // navigate further down - AttributeSelectorStep::Nested { tag, item } => { - let e = - obj.entries - .get_mut(tag) - .with_context(|| crate::MissingSequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - - // get items - let items = e.items_mut().with_context(|| NotASequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - })?; - - // if item.length == i and action is a constructive action, append new item - obj = items.get_mut(*item as usize).with_context(|| { - crate::MissingSequenceSnafu { - selector: selector.clone(), - step_index: i as u32, - } - })?; - } - } - } - - unreachable!() - } - - /// Apply the given attribute operation on this object. - /// - /// For more complex updates, see [`update_value_at`]. - /// - /// See the [`dicom_core::ops`] module - /// for more information. - /// - /// # Examples - /// - /// ```rust - /// # use dicom_core::header::{DataElement, VR}; - /// # use dicom_core::value::PrimitiveValue; - /// # use dicom_dictionary_std::tags; - /// # use dicom_object::mem::*; - /// # use dicom_object::ops::ApplyResult; - /// use dicom_core::ops::{ApplyOp, AttributeAction, AttributeOp}; - /// # fn main() -> Result<(), Box> { - /// // given an in-memory DICOM object - /// let mut obj = InMemDicomObject::from_element_iter([ - /// DataElement::new( - /// tags::PATIENT_NAME, - /// VR::PN, - /// PrimitiveValue::from("Rosling^Hans") - /// ), - /// ]); - /// - /// // apply patient name change - /// obj.apply(AttributeOp::new( - /// tags::PATIENT_NAME, - /// AttributeAction::SetStr("Patient^Anonymous".into()), - /// ))?; - /// - /// assert_eq!( - /// obj.element(tags::PATIENT_NAME)?.to_str()?, - /// "Patient^Anonymous", - /// ); - /// # Ok(()) - /// # } - /// ``` - fn apply(&mut self, op: AttributeOp) -> ApplyResult { - let AttributeOp { selector, action } = op; - let dict = self.dict.clone(); - - let mut obj = self; - for (i, step) in selector.iter().enumerate() { - match step { - // reached the leaf - AttributeSelectorStep::Tag(tag) => return obj.apply_leaf(*tag, action), - // navigate further down - AttributeSelectorStep::Nested { tag, item } => { - if !obj.entries.contains_key(tag) { - // missing sequence, create it if action is constructive - if action.is_constructive() { - let vr = dict - .by_tag(*tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::UN); - - if vr != VR::SQ && vr != VR::UN { - return Err(ApplyError::NotASequence { - selector: selector.clone(), - step_index: i as u32, - }); - } - - obj.put(DataElement::new(*tag, vr, DataSetSequence::empty())); - } else { - return Err(ApplyError::MissingSequence { - selector: selector.clone(), - step_index: i as u32, - }); - } - }; - - // get items - let items = obj - .entries - .get_mut(tag) - .expect("sequence element should exist at this point") - .items_mut() - .ok_or_else(|| ApplyError::NotASequence { - selector: selector.clone(), - step_index: i as u32, - })?; - - // if item.length == i and action is a constructive action, append new item - obj = if items.len() == *item as usize && action.is_constructive() { - items.push(InMemDicomObject::new_empty_with_dict(dict.clone())); - items.last_mut().unwrap() - } else { - items.get_mut(*item as usize).ok_or_else(|| { - ApplyError::MissingSequence { - selector: selector.clone(), - step_index: i as u32, - } - })? - }; - } - } - } - unreachable!() - } - - fn apply_leaf(&mut self, tag: Tag, action: AttributeAction) -> ApplyResult { - self.invalidate_if_charset_changed(tag); - match action { - AttributeAction::Remove => { - self.remove_element(tag); - Ok(()) - } - AttributeAction::Empty => { - if let Some(e) = self.entries.get_mut(&tag) { - let vr = e.vr(); - // replace element - *e = DataElement::empty(tag, vr); - self.len = Length::UNDEFINED; - } - Ok(()) - } - AttributeAction::SetVr(new_vr) => { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - let e = DataElement::new(header.tag, new_vr, value); - self.put(e); - } else { - self.put(DataElement::empty(tag, new_vr)); - } - Ok(()) - } - AttributeAction::Set(new_value) => { - self.apply_change_value_impl(tag, new_value); - Ok(()) - } - AttributeAction::SetStr(string) => { - let new_value = PrimitiveValue::from(&*string); - self.apply_change_value_impl(tag, new_value); - Ok(()) - } - AttributeAction::SetIfMissing(new_value) => { - if self.get(tag).is_none() { - self.apply_change_value_impl(tag, new_value); - } - Ok(()) - } - AttributeAction::SetStrIfMissing(string) => { - if self.get(tag).is_none() { - let new_value = PrimitiveValue::from(&*string); - self.apply_change_value_impl(tag, new_value); - } - Ok(()) - } - AttributeAction::Replace(new_value) => { - if self.get(tag).is_some() { - self.apply_change_value_impl(tag, new_value); - } - Ok(()) - } - AttributeAction::ReplaceStr(string) => { - if self.get(tag).is_some() { - let new_value = PrimitiveValue::from(&*string); - self.apply_change_value_impl(tag, new_value); - } - Ok(()) - } - AttributeAction::PushStr(string) => self.apply_push_str_impl(tag, string), - AttributeAction::PushI32(integer) => self.apply_push_i32_impl(tag, integer), - AttributeAction::PushU32(integer) => self.apply_push_u32_impl(tag, integer), - AttributeAction::PushI16(integer) => self.apply_push_i16_impl(tag, integer), - AttributeAction::PushU16(integer) => self.apply_push_u16_impl(tag, integer), - AttributeAction::PushF32(number) => self.apply_push_f32_impl(tag, number), - AttributeAction::PushF64(number) => self.apply_push_f64_impl(tag, number), - AttributeAction::Truncate(limit) => { - self.update_value(tag, |value| value.truncate(limit)); - Ok(()) - } - _ => UnsupportedActionSnafu.fail(), - } - } - - fn apply_change_value_impl(&mut self, tag: Tag, new_value: PrimitiveValue) { - self.invalidate_if_charset_changed(tag); - - if let Some(e) = self.entries.get_mut(&tag) { - let vr = e.vr(); - // handle edge case: if VR is SQ and suggested value is empty, - // then create an empty data set sequence - let new_value = if vr == VR::SQ && new_value.is_empty() { - DataSetSequence::empty().into() - } else { - Value::from(new_value) - }; - *e = DataElement::new(tag, vr, new_value); - self.len = Length::UNDEFINED; - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::UN); - // insert element - - // handle edge case: if VR is SQ and suggested value is empty, - // then create an empty data set sequence - let new_value = if vr == VR::SQ && new_value.is_empty() { - DataSetSequence::empty().into() - } else { - Value::from(new_value) - }; - - self.put(DataElement::new(tag, vr, new_value)); - } - } - - fn invalidate_if_charset_changed(&mut self, tag: Tag) { - if tag == tags::SPECIFIC_CHARACTER_SET { - self.charset_changed = true; - } - } - - fn apply_push_str_impl(&mut self, tag: Tag, string: Cow<'static, str>) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - self.invalidate_if_charset_changed(tag); - // extend value - v.extend_str([string]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::UN); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(&*string))); - Ok(()) - } - } - - fn apply_push_i32_impl(&mut self, tag: Tag, integer: i32) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - // extend value - v.extend_i32([integer]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::SL); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); - Ok(()) - } - } - - fn apply_push_u32_impl(&mut self, tag: Tag, integer: u32) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - // extend value - v.extend_u32([integer]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::UL); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); - Ok(()) - } - } - - fn apply_push_i16_impl(&mut self, tag: Tag, integer: i16) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - // extend value - v.extend_i16([integer]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::SS); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); - Ok(()) - } - } - - fn apply_push_u16_impl(&mut self, tag: Tag, integer: u16) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - // extend value - v.extend_u16([integer]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::US); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); - Ok(()) - } - } - - fn apply_push_f32_impl(&mut self, tag: Tag, number: f32) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - // extend value - v.extend_f32([number]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::FL); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(number))); - Ok(()) - } - } - - fn apply_push_f64_impl(&mut self, tag: Tag, number: f64) -> ApplyResult { - if let Some(e) = self.entries.remove(&tag) { - let (header, value) = e.into_parts(); - match value { - Value::Primitive(mut v) => { - // extend value - v.extend_f64([number]).context(ModifySnafu)?; - // reinsert element - self.put(DataElement::new(tag, header.vr, v)); - Ok(()) - } - - Value::PixelSequence(..) => IncompatibleTypesSnafu { - kind: ValueType::PixelSequence, - } - .fail(), - Value::Sequence(..) => IncompatibleTypesSnafu { - kind: ValueType::DataSetSequence, - } - .fail(), - } - } else { - // infer VR from tag - let vr = dicom_dictionary_std::StandardDataDictionary - .by_tag(tag) - .and_then(|entry| entry.vr().exact()) - .unwrap_or(VR::FD); - // insert element - self.put(DataElement::new(tag, vr, PrimitiveValue::from(number))); - Ok(()) - } - } - - /// Write this object's data set into the given writer, - /// with the given encoder specifications, - /// without preamble, magic code, nor file meta group. - /// - /// The text encoding to use will be the default character set - /// until _Specific Character Set_ is found in the data set, - /// in which then that character set will be used. - /// - /// Uses the default [DataSetWriterOptions] for the writer. - /// - /// Note: [`write_dataset_with_ts`] and [`write_dataset_with_ts_cs`] - /// may be easier to use and _will_ apply a dataset adapter (such as - /// DeflatedExplicitVRLittleEndian (1.2.840.10008.1.2.99)) whereas this - /// method will _not_ - /// - /// [`write_dataset_with_ts`]: #method.write_dataset_with_ts - /// [`write_dataset_with_ts_cs`]: #method.write_dataset_with_ts_cs - pub fn write_dataset(&self, to: W, encoder: E) -> Result<(), WriteError> - where - W: Write, - E: EncodeTo, - { - // prepare data set writer - let mut dset_writer = DataSetWriter::new(to, encoder); - let required_options = IntoTokensOptions::new(self.charset_changed); - // write object - dset_writer - .write_sequence(self.into_tokens_with_options(required_options)) - .context(PrintDataSetSnafu)?; - - Ok(()) - } - - /// Write this object's data set into the given printer, - /// with the specified transfer syntax and character set, - /// without preamble, magic code, nor file meta group. - /// - /// The default [DataSetWriterOptions] is used for the writer. To change - /// that, use [`write_dataset_with_ts_cs_options`](Self::write_dataset_with_ts_cs_options). - /// - /// If the attribute _Specific Character Set_ is found in the data set, - /// the last parameter is overridden accordingly. - /// See also [`write_dataset_with_ts`](Self::write_dataset_with_ts). - pub fn write_dataset_with_ts_cs( - &self, - to: W, - ts: &TransferSyntax, - cs: SpecificCharacterSet, - ) -> Result<(), WriteError> - where - W: Write, - { - if let Codec::Dataset(Some(adapter)) = ts.codec() { - let adapter = adapter.adapt_writer(Box::new(to)); - // prepare data set writer - let mut dset_writer = - DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?; - - // write object - dset_writer - .write_sequence(self.into_tokens()) - .context(PrintDataSetSnafu)?; - - Ok(()) - } else { - // prepare data set writer - let mut dset_writer = - DataSetWriter::with_ts_cs(to, ts, cs).context(CreatePrinterSnafu)?; - - // write object - dset_writer - .write_sequence(self.into_tokens()) - .context(PrintDataSetSnafu)?; - - Ok(()) - } - } - - /// Write this object's data set into the given printer, - /// with the specified transfer syntax and character set, - /// without preamble, magic code, nor file meta group. - /// - /// If the attribute _Specific Character Set_ is found in the data set, - /// the last parameter is overridden accordingly. - /// See also [`write_dataset_with_ts`](Self::write_dataset_with_ts). - pub fn write_dataset_with_ts_cs_options( - &self, - to: W, - ts: &TransferSyntax, - cs: SpecificCharacterSet, - options: DataSetWriterOptions, - ) -> Result<(), WriteError> - where - W: Write, - { - // prepare data set writer - let mut dset_writer = - DataSetWriter::with_ts_cs_options(to, ts, cs, options).context(CreatePrinterSnafu)?; - let required_options = IntoTokensOptions::new(self.charset_changed); - - // write object - dset_writer - .write_sequence(self.into_tokens_with_options(required_options)) - .context(PrintDataSetSnafu)?; - - Ok(()) - } - - /// Write this object's data set into the given writer, - /// with the specified transfer syntax, - /// without preamble, magic code, nor file meta group. - /// - /// The default [DataSetWriterOptions] is used for the writer. To change - /// that, use [`write_dataset_with_ts_options`](Self::write_dataset_with_ts_options). - /// - /// The default character set is assumed - /// until the _Specific Character Set_ is found in the data set, - /// after which the text encoder is overridden accordingly. - pub fn write_dataset_with_ts(&self, to: W, ts: &TransferSyntax) -> Result<(), WriteError> - where - W: Write, - { - self.write_dataset_with_ts_cs(to, ts, SpecificCharacterSet::default()) - } - - /// Write this object's data set into the given writer, - /// with the specified transfer syntax, - /// without preamble, magic code, nor file meta group. - /// - /// The default character set is assumed - /// until the _Specific Character Set_ is found in the data set, - /// after which the text encoder is overridden accordingly. - pub fn write_dataset_with_ts_options( - &self, - to: W, - ts: &TransferSyntax, - options: DataSetWriterOptions, - ) -> Result<(), WriteError> - where - W: Write, - { - self.write_dataset_with_ts_cs_options(to, ts, SpecificCharacterSet::default(), options) - } - - /// Encapsulate this object to contain a file meta group - /// as described exactly by the given table. - /// - /// **Note:** this method will not adjust the file meta group - /// to be semantically valid for the object. - /// Namely, the _Media Storage SOP Instance UID_ - /// and _Media Storage SOP Class UID_ - /// are not updated based on the receiving data set. - pub fn with_exact_meta(self, meta: FileMetaTable) -> FileDicomObject { - FileDicomObject { meta, obj: self } - } - - /// Encapsulate this object to contain a file meta group, - /// created through the given file meta table builder. - /// - /// A complete file meta group should provide - /// the _Transfer Syntax UID_, - /// the _Media Storage SOP Instance UID_, - /// and the _Media Storage SOP Class UID_. - /// The last two will be filled with the values of - /// _SOP Instance UID_ and _SOP Class UID_ - /// if they are present in this object. - /// - /// # Example - /// - /// ```no_run - /// # use dicom_core::{DataElement, VR}; - /// # use dicom_dictionary_std::tags; - /// # use dicom_dictionary_std::uids; - /// use dicom_object::{InMemDicomObject, meta::FileMetaTableBuilder}; - /// - /// let obj = InMemDicomObject::from_element_iter([ - /// DataElement::new(tags::SOP_CLASS_UID, VR::UI, uids::COMPUTED_RADIOGRAPHY_IMAGE_STORAGE), - /// DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.60156688944589400766024286894543900794"), - /// // ... - /// ]); - /// - /// let obj = obj.with_meta(FileMetaTableBuilder::new() - /// .transfer_syntax(uids::EXPLICIT_VR_LITTLE_ENDIAN))?; - /// - /// // can now save everything to a file - /// let meta = obj.write_to_file("out.dcm")?; - /// # Result::<_, Box>::Ok(()) - /// ``` - pub fn with_meta( - self, - mut meta: FileMetaTableBuilder, - ) -> Result, WithMetaError> { - if let Some(elem) = self.get(tags::SOP_INSTANCE_UID) { - meta = meta.media_storage_sop_instance_uid( - elem.value().to_str().context(PrepareMetaTableSnafu)?, - ); - } - if let Some(elem) = self.get(tags::SOP_CLASS_UID) { - meta = meta - .media_storage_sop_class_uid(elem.value().to_str().context(PrepareMetaTableSnafu)?); - } - Ok(FileDicomObject { - meta: meta.build().context(BuildMetaTableSnafu)?, - obj: self, - }) - } - - /// Obtain an iterator over the elements of this object. - pub fn iter(&self) -> impl Iterator> + '_ { - self.into_iter() - } - - /// Obtain an iterator over the tags of the object's elements. - pub fn tags(&self) -> impl Iterator + '_ { - self.entries.keys().copied() - } - - // private methods - - /// Build an object by consuming a data set parser. - fn build_object( - dataset: &mut I, - dict: D, - in_item: bool, - len: Length, - read_until: Option, - ) -> Result - where - I: ?Sized + Iterator>, - { - let mut entries: BTreeMap> = BTreeMap::new(); - // perform a structured parsing of incoming tokens - while let Some(token) = dataset.next() { - let elem = match token.context(ReadTokenSnafu)? { - DataToken::PixelSequenceStart => { - // stop reading if reached `read_until` tag - if read_until - .map(|t| t <= Tag(0x7fe0, 0x0010)) - .unwrap_or(false) - { - break; - } - let value = InMemDicomObject::build_encapsulated_data(&mut *dataset)?; - DataElement::new(Tag(0x7fe0, 0x0010), VR::OB, value) - } - DataToken::ElementHeader(header) => { - // stop reading if reached `read_until` tag - if read_until.map(|t| t <= header.tag).unwrap_or(false) { - break; - } - - // fetch respective value, place it in the entries - let next_token = dataset.next().context(MissingElementValueSnafu)?; - match next_token.context(ReadTokenSnafu)? { - DataToken::PrimitiveValue(v) => InMemElement::new_with_len( - header.tag, - header.vr, - header.len, - Value::Primitive(v), - ), - token => { - return UnexpectedTokenSnafu { token }.fail(); - } - } - } - DataToken::SequenceStart { tag, len } => { - // stop reading if reached `read_until` tag - if read_until.map(|t| t <= tag).unwrap_or(false) { - break; - } - - // delegate sequence building to another function - let items = Self::build_sequence(tag, len, &mut *dataset, &dict)?; - DataElement::new_with_len( - tag, - VR::SQ, - len, - Value::Sequence(DataSetSequence::new(items, len)), - ) - } - DataToken::ItemEnd if in_item => { - // end of item, leave now - return Ok(InMemDicomObject { - entries, - dict, - len, - charset_changed: false, - }); - } - token => return UnexpectedTokenSnafu { token }.fail(), - }; - entries.insert(elem.tag(), elem); - } - - Ok(InMemDicomObject { - entries, - dict, - len, - charset_changed: false, - }) - } - - /// Build an encapsulated pixel data by collecting all fragments into an - /// in-memory DICOM value. - fn build_encapsulated_data( - dataset: I, - ) -> Result, InMemFragment>, ReadError> - where - I: Iterator>, - { - // continue fetching tokens to retrieve: - // - the offset table - // - the various compressed fragments - - let mut offset_table = None; - - let mut fragments = C::new(); - - for token in dataset { - match token.context(ReadTokenSnafu)? { - DataToken::OffsetTable(table) => { - offset_table = Some(table); - } - DataToken::ItemValue(data) => { - fragments.push(data); - } - DataToken::ItemEnd => { - // at the end of the first item ensure the presence of - // an empty offset_table here, so that the next items - // are seen as compressed fragments - if offset_table.is_none() { - offset_table = Some(Vec::new()) - } - } - DataToken::ItemStart { len: _ } => { /* no-op */ } - DataToken::SequenceEnd => { - // end of pixel data - break; - } - // the following variants are unexpected - token @ DataToken::ElementHeader(_) - | token @ DataToken::PixelSequenceStart - | token @ DataToken::SequenceStart { .. } - | token @ DataToken::PrimitiveValue(_) => { - return UnexpectedTokenSnafu { token }.fail(); - } - } - } - - Ok(Value::PixelSequence(PixelFragmentSequence::new( - offset_table.unwrap_or_default(), - fragments, - ))) - } - - /// Build a DICOM sequence by consuming a data set parser. - fn build_sequence( - _tag: Tag, - _len: Length, - dataset: &mut I, - dict: &D, - ) -> Result>, ReadError> - where - I: ?Sized + Iterator>, - { - let mut items: C<_> = SmallVec::new(); - while let Some(token) = dataset.next() { - match token.context(ReadTokenSnafu)? { - DataToken::ItemStart { len } => { - items.push(Self::build_object( - &mut *dataset, - dict.clone(), - true, - len, - None, - )?); - } - DataToken::SequenceEnd => { - return Ok(items); - } - token => return UnexpectedTokenSnafu { token }.fail(), - }; - } - - // iterator fully consumed without a sequence delimiter - PrematureEndSnafu.fail() - } - - fn lookup_name(&self, name: &str) -> Result { - self.dict - .by_name(name) - .context(NoSuchAttributeNameSnafu { name }) - .map(|e| e.tag()) - } -} - -impl ApplyOp for InMemDicomObject -where - D: DataDictionary, - D: Clone, -{ - type Err = ApplyError; - - #[inline] - fn apply(&mut self, op: AttributeOp) -> ApplyResult { - self.apply(op) - } -} - -impl<'a, D> IntoIterator for &'a InMemDicomObject { - type Item = &'a InMemElement; - type IntoIter = ::std::collections::btree_map::Values<'a, Tag, InMemElement>; - - fn into_iter(self) -> Self::IntoIter { - self.entries.values() - } -} - -impl IntoIterator for InMemDicomObject { - type Item = InMemElement; - type IntoIter = Iter; - - fn into_iter(self) -> Self::IntoIter { - Iter { - inner: self.entries.into_iter(), - } - } -} - -/// Base iterator type for an in-memory DICOM object. -#[derive(Debug)] -pub struct Iter { - inner: ::std::collections::btree_map::IntoIter>, -} - -impl Iterator for Iter { - type Item = InMemElement; - - fn next(&mut self) -> Option { - self.inner.next().map(|x| x.1) - } - - fn size_hint(&self) -> (usize, Option) { - self.inner.size_hint() - } - - fn count(self) -> usize { - self.inner.count() - } -} - -impl Extend> for InMemDicomObject { - fn extend(&mut self, iter: I) - where - I: IntoIterator>, - { - self.len = Length::UNDEFINED; - self.entries.extend(iter.into_iter().map(|e| (e.tag(), e))) - } -} - -fn even_len(l: u32) -> u32 { - (l + 1) & !1 -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{open_file, DicomAttribute as _}; - use byteordered::Endianness; - use dicom_core::chrono::FixedOffset; - use dicom_core::value::{DicomDate, DicomDateTime, DicomTime}; - use dicom_core::{dicom_value, header::DataElementHeader}; - use dicom_encoding::{ - decode::{basic::BasicDecoder, implicit_le::ImplicitVRLittleEndianDecoder}, - encode::{implicit_le::ImplicitVRLittleEndianEncoder, EncoderFor}, - }; - use dicom_parser::StatefulDecoder; - - fn assert_obj_eq(obj1: &InMemDicomObject, obj2: &InMemDicomObject) - where - D: std::fmt::Debug, - { - // debug representation because it makes a stricter comparison and - // assumes that Undefined lengths are equal. - assert_eq!(format!("{obj1:?}"), format!("{:?}", obj2)) - } - - #[test] - fn inmem_object_compare() { - let mut obj1 = InMemDicomObject::new_empty(); - let mut obj2 = InMemDicomObject::new_empty(); - assert_eq!(obj1, obj2); - let empty_patient_name = DataElement::empty(Tag(0x0010, 0x0010), VR::PN); - obj1.put(empty_patient_name.clone()); - assert_ne!(obj1, obj2); - obj2.put(empty_patient_name.clone()); - assert_obj_eq(&obj1, &obj2); - } - - #[test] - fn inmem_object_read_dataset() { - let data_in = [ - 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) - 0x08, 0x00, 0x00, 0x00, // Length: 8 - b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', - ]; - - let decoder = ImplicitVRLittleEndianDecoder::default(); - let text = SpecificCharacterSet::default(); - let mut cursor = &data_in[..]; - let parser = StatefulDecoder::new( - &mut cursor, - decoder, - BasicDecoder::new(Endianness::Little), - text, - ); - - let obj = InMemDicomObject::read_dataset(parser).unwrap(); - - let mut gt = InMemDicomObject::new_empty(); - - let patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - dicom_value!(Strs, ["Doe^John"]), - ); - gt.put(patient_name); - - assert_eq!(obj, gt); - } - - #[test] - fn inmem_object_read_dataset_with_ts_cs() { - let data_in = [ - 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) - 0x08, 0x00, 0x00, 0x00, // Length: 8 - b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', - ]; - - let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2").unwrap(); - let cs = SpecificCharacterSet::default(); - let mut cursor = &data_in[..]; - - let obj = InMemDicomObject::read_dataset_with_dict_ts_cs( - &mut cursor, - StandardDataDictionary, - ts, - cs, - ) - .unwrap(); - - let mut gt = InMemDicomObject::new_empty(); - - let patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - dicom_value!(Strs, ["Doe^John"]), - ); - gt.put(patient_name); - - assert_eq!(obj, gt); - } - - /// Reading a data set - /// saves the original length of a text element. - #[test] - fn inmem_object_read_dataset_saves_len() { - let data_in = [ - // SpecificCharacterSet (0008,0005) - 0x08, 0x00, 0x05, 0x00, // - // Length: 10 - 0x0a, 0x00, 0x00, 0x00, // - b'I', b'S', b'O', b'_', b'I', b'R', b' ', b'1', b'0', b'0', - // ReferringPhysicianName (0008,0090) - 0x08, 0x00, 0x90, 0x00, // - // Length: 12 - 0x0c, 0x00, 0x00, 0x00, b'S', b'i', b'm', 0xF5, b'e', b's', b'^', b'J', b'o', 0xE3, - b'o', b' ', - ]; - - let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2").unwrap(); - let mut cursor = &data_in[..]; - - let obj = - InMemDicomObject::read_dataset_with_dict_ts(&mut cursor, StandardDataDictionary, ts) - .unwrap(); - - let physician_name = obj.element(Tag(0x0008, 0x0090)).unwrap(); - assert_eq!(physician_name.header().len, Length(12)); - assert_eq!(physician_name.value().to_str().unwrap(), "Simões^João"); - } - - #[test] - fn inmem_object_write_dataset() { - let mut obj = InMemDicomObject::new_empty(); - - let patient_name = - DataElement::new(Tag(0x0010, 0x0010), VR::PN, dicom_value!(Str, "Doe^John")); - obj.put(patient_name); - - let mut out = Vec::new(); - - let printer = EncoderFor::new(ImplicitVRLittleEndianEncoder::default()); - - obj.write_dataset(&mut out, printer).unwrap(); - - assert_eq!( - out, - &[ - 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) - 0x08, 0x00, 0x00, 0x00, // Length: 8 - b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', - ][..], - ); - } - - #[test] - fn inmem_object_write_dataset_with_ts() { - let mut obj = InMemDicomObject::new_empty(); - - let patient_name = - DataElement::new(Tag(0x0010, 0x0010), VR::PN, dicom_value!(Str, "Doe^John")); - obj.put(patient_name); - - let mut out = Vec::new(); - - let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.1").unwrap(); - - obj.write_dataset_with_ts(&mut out, ts).unwrap(); - - assert_eq!( - out, - &[ - 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) - b'P', b'N', // VR: PN - 0x08, 0x00, // Length: 8 - b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', - ][..], - ); - } - - #[test] - fn inmem_object_write_dataset_encapsulated_pixel_data() { - let mut obj = InMemDicomObject::new_empty(); - - let sop_instance_uid = - DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.44399302050596340528032699331187776010"); - obj.put(sop_instance_uid); - - obj.put(DataElement::new( - tags::PIXEL_DATA, - VR::OB, - PixelFragmentSequence::new_fragments([ - vec![0x01, 0x02, 0x03, 0x04], - vec![0x05, 0x06, 0x07, 0x08], - ]) - )); - - let mut out = Vec::new(); - - let ts = TransferSyntaxRegistry.get(uids::ENCAPSULATED_UNCOMPRESSED_EXPLICIT_VR_LITTLE_ENDIAN).unwrap(); - - obj.write_dataset_with_ts(&mut out, ts).unwrap(); - - assert_eq!( - out, - &[ - 0x08, 0x00, 0x18, 0x00, // Tag(0x0008, 0x0018) - b'U', b'I', // VR: UI - 0x2c, 0x00, // Length: 44 - // 2.25.44399302050596340528032699331187776010 - b'2', b'.', b'2', b'5', b'.', b'4', b'4', b'3', b'9', b'9', b'3', - b'0', b'2', b'0', b'5', b'0', b'5', b'9', b'6', b'3', b'4', b'0', - b'5', b'2', b'8', b'0', b'3', b'2', b'6', b'9', b'9', b'3', b'3', - b'1', b'1', b'8', b'7', b'7', b'7', b'6', b'0', b'1', b'0', b'\0', - // pixel data - 0xe0, 0x7f, 0x10, 0x00, // Tag(0x7fe0, 0x0010) - b'O', b'B', // VR: OB - 0x00, 0x00, // reserved - 0xff, 0xff, 0xff, 0xff, // Length: undefined - // first fragment (offset table) - 0xfe, 0xff, 0x00, 0xe0, - 0x00, 0x00, 0x00, 0x00, // Length: 0 - // second fragment - 0xfe, 0xff, 0x00, 0xe0, - 0x04, 0x00, 0x00, 0x00, // Length: 4 - 0x01, 0x02, 0x03, 0x04, - // third fragment - 0xfe, 0xff, 0x00, 0xe0, - 0x04, 0x00, 0x00, 0x00, // Length: 4 - 0x05, 0x06, 0x07, 0x08, - // sequence delimitation item - 0xfe, 0xff, 0xdd, 0xe0, - 0x00, 0x00, 0x00, 0x00, // Length: 0 - ][..], - ); - } - - #[test] - fn inmem_object_write_dataset_with_ts_cs() { - let mut obj = InMemDicomObject::new_empty(); - - let patient_name = - DataElement::new(Tag(0x0010, 0x0010), VR::PN, dicom_value!(Str, "Doe^John")); - obj.put(patient_name); - - let mut out = Vec::new(); - - let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2").unwrap(); - let cs = SpecificCharacterSet::default(); - - obj.write_dataset_with_ts_cs(&mut out, ts, cs).unwrap(); - - assert_eq!( - out, - &[ - 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) - 0x08, 0x00, 0x00, 0x00, // Length: 8 - b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', - ][..], - ); - } - - /// writing a DICOM date time into an object - /// should include value padding - #[test] - fn inmem_object_write_datetime_odd() { - let mut obj = InMemDicomObject::new_empty(); - - // add a number that will be encoded in text - let instance_number = - DataElement::new(Tag(0x0020, 0x0013), VR::IS, PrimitiveValue::from(1_i32)); - obj.put(instance_number); - - // add a date time - let dt = DicomDateTime::from_date_and_time_with_time_zone( - DicomDate::from_ymd(2022, 11, 22).unwrap(), - DicomTime::from_hms(18, 9, 35).unwrap(), - FixedOffset::east_opt(3600).unwrap(), - ) - .unwrap(); - let instance_coercion_date_time = - DataElement::new(Tag(0x0008, 0x0015), VR::DT, dicom_value!(DateTime, dt)); - obj.put(instance_coercion_date_time); - - // explicit VR Little Endian - let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.1").unwrap(); - - let mut out = Vec::new(); - obj.write_dataset_with_ts(&mut out, ts) - .expect("should write DICOM data without errors"); - - assert_eq!( - out, - &[ - // instance coercion date time - 0x08, 0x00, 0x15, 0x00, // Tag(0x0008, 0x0015) - b'D', b'T', // VR: DT - 0x14, 0x00, // Length: 20 bytes - b'2', b'0', b'2', b'2', b'1', b'1', b'2', b'2', // date - b'1', b'8', b'0', b'9', b'3', b'5', // time - b'+', b'0', b'1', b'0', b'0', // offset - b' ', // padding to even length - // instance number - 0x20, 0x00, 0x13, 0x00, // Tag(0x0020, 0x0013) - b'I', b'S', // VR: IS - 0x02, 0x00, // Length: 2 bytes - b'1', b' ' // 1, with padding - ][..], - ); - } - - /// Writes a file from scratch - /// and opens it to check that the data is equivalent. - #[test] - fn inmem_write_to_file_with_meta() { - let sop_uid = "1.4.645.212121"; - let mut obj = InMemDicomObject::new_empty(); - - obj.put(DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - dicom_value!(Strs, ["Doe^John"]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0060), - VR::CS, - dicom_value!(Strs, ["CR"]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0018), - VR::UI, - dicom_value!(Strs, [sop_uid]), - )); - - let file_object = obj - .with_meta( - FileMetaTableBuilder::default() - // Explicit VR Little Endian - .transfer_syntax("1.2.840.10008.1.2.1") - // Computed Radiography image storage - .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1") - .media_storage_sop_instance_uid(sop_uid), - ) - .unwrap(); - - // create temporary file path and write object to that file - let dir = tempfile::tempdir().unwrap(); - let mut file_path = dir.keep(); - file_path.push(format!("{sop_uid}.dcm")); - - file_object.write_to_file(&file_path).unwrap(); - - // read the file back to validate the outcome - let saved_object = open_file(file_path).unwrap(); - assert_eq!(file_object, saved_object); - } - - /// Creating a file DICOM object from an in-mem DICOM object - /// infers the SOP instance UID. - #[test] - fn inmem_with_meta_infers_sop_instance_uid() { - let sop_uid = "1.4.645.252521"; - let mut obj = InMemDicomObject::new_empty(); - - obj.put(DataElement::new( - tags::SOP_INSTANCE_UID, - VR::UI, - PrimitiveValue::from(sop_uid), - )); - - let file_object = obj - .with_meta( - // Media Storage SOP Instance UID deliberately not set - FileMetaTableBuilder::default() - // Explicit VR Little Endian - .transfer_syntax("1.2.840.10008.1.2.1") - // Computed Radiography image storage - .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1"), - ) - .unwrap(); - - let meta = file_object.meta(); - - assert_eq!( - meta.media_storage_sop_instance_uid.trim_end_matches('\0'), - sop_uid.trim_end_matches('\0'), - ); - } - - /// Write a file from scratch, with exact file meta table. - #[test] - fn inmem_write_to_file_with_exact_meta() { - let sop_uid = "1.4.645.212121"; - let mut obj = InMemDicomObject::new_empty(); - - obj.put(DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - dicom_value!(Strs, ["Doe^John"]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0060), - VR::CS, - dicom_value!(Strs, ["CR"]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0018), - VR::UI, - dicom_value!(Strs, [sop_uid]), - )); - - let file_object = obj.with_exact_meta( - FileMetaTableBuilder::default() - // Explicit VR Little Endian - .transfer_syntax("1.2.840.10008.1.2.1") - // Computed Radiography image storage - .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1") - .media_storage_sop_instance_uid(sop_uid) - .build() - .unwrap(), - ); - - // create temporary file path and write object to that file - let dir = tempfile::tempdir().unwrap(); - let mut file_path = dir.keep(); - file_path.push(format!("{sop_uid}.dcm")); - - file_object.write_to_file(&file_path).unwrap(); - - // read the file back to validate the outcome - let saved_object = open_file(file_path).unwrap(); - assert_eq!(file_object, saved_object); - } - - #[test] - fn inmem_object_get() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - let elem1 = obj.element(Tag(0x0010, 0x0010)).unwrap(); - assert_eq!(elem1, &another_patient_name); - } - - #[test] - fn infer_media_sop_from_dataset_sop_elements() { - let sop_instance_uid = "1.4.645.313131"; - let sop_class_uid = "1.2.840.10008.5.1.4.1.1.2"; - let mut obj = InMemDicomObject::new_empty(); - - obj.put(DataElement::new( - Tag(0x0008, 0x0018), - VR::UI, - dicom_value!(Strs, [sop_instance_uid]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0016), - VR::UI, - dicom_value!(Strs, [sop_class_uid]), - )); - - let file_object = obj.with_exact_meta( - FileMetaTableBuilder::default() - .transfer_syntax("1.2.840.10008.1.2.1") - // Media Storage SOP Class and Instance UIDs are missing and set to an empty string - .media_storage_sop_class_uid("") - .media_storage_sop_instance_uid("") - .build() - .unwrap(), - ); - - // create temporary file path and write object to that file - let dir = tempfile::tempdir().unwrap(); - let mut file_path = dir.keep(); - file_path.push(format!("{sop_instance_uid}.dcm")); - - file_object.write_to_file(&file_path).unwrap(); - - // read the file back to validate the outcome - let saved_object = open_file(file_path).unwrap(); - - // verify that the empty string media storage sop instance and class UIDs have been inferred from the sop instance and class UID - assert_eq!( - saved_object.meta().media_storage_sop_instance_uid(), - sop_instance_uid - ); - assert_eq!( - saved_object.meta().media_storage_sop_class_uid(), - sop_class_uid - ); - } - - #[test] - fn inmem_object_get_opt() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - let elem1 = obj.element_opt(Tag(0x0010, 0x0010)).unwrap(); - assert_eq!(elem1, Some(&another_patient_name)); - - // try a missing element, should return None - assert_eq!(obj.element_opt(Tag(0x0010, 0x0020)).unwrap(), None); - } - - #[test] - fn inmem_object_get_by_name() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - let elem1 = obj.element_by_name("PatientName").unwrap(); - assert_eq!(elem1, &another_patient_name); - } - - #[test] - fn inmem_object_get_by_name_opt() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - let elem1 = obj.element_by_name_opt("PatientName").unwrap(); - assert_eq!(elem1, Some(&another_patient_name)); - - // try a missing element, should return None - assert_eq!(obj.element_by_name_opt("PatientID").unwrap(), None); - } - - #[test] - fn inmem_object_take_element() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - let elem1 = obj.take_element(Tag(0x0010, 0x0010)).unwrap(); - assert_eq!(elem1, another_patient_name); - assert!(matches!( - obj.take_element(Tag(0x0010, 0x0010)), - Err(AccessError::NoSuchDataElementTag { - tag: Tag(0x0010, 0x0010), - .. - }) - )); - } - - #[test] - fn inmem_object_take_element_by_name() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - let elem1 = obj.take_element_by_name("PatientName").unwrap(); - assert_eq!(elem1, another_patient_name); - assert!(matches!( - obj.take_element_by_name("PatientName"), - Err(AccessByNameError::NoSuchDataElementAlias { - tag: Tag(0x0010, 0x0010), - alias, - .. - }) if alias == "PatientName")); - } - - #[test] - fn inmem_object_remove_element() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - assert!(obj.remove_element(Tag(0x0010, 0x0010))); - assert!(!obj.remove_element(Tag(0x0010, 0x0010))); - } - - #[test] - fn inmem_object_remove_element_by_name() { - let another_patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(another_patient_name.clone()); - assert!(obj.remove_element_by_name("PatientName").unwrap()); - assert!(!obj.remove_element_by_name("PatientName").unwrap()); - } - - /// Elements are traversed in tag order. - #[test] - fn inmem_traverse_elements() { - let sop_uid = "1.4.645.212121"; - let mut obj = InMemDicomObject::new_empty(); - - obj.put(DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - dicom_value!(Strs, ["Doe^John"]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0060), - VR::CS, - dicom_value!(Strs, ["CR"]), - )); - obj.put(DataElement::new( - Tag(0x0008, 0x0018), - VR::UI, - dicom_value!(Strs, [sop_uid]), - )); - - { - let mut iter = obj.iter(); - assert_eq!( - *iter.next().unwrap().header(), - DataElementHeader::new(Tag(0x0008, 0x0018), VR::UI, Length(sop_uid.len() as u32)), - ); - assert_eq!( - *iter.next().unwrap().header(), - DataElementHeader::new(Tag(0x0008, 0x0060), VR::CS, Length(2)), - ); - assert_eq!( - *iter.next().unwrap().header(), - DataElementHeader::new(Tag(0x0010, 0x0010), VR::PN, Length(8)), - ); - } - - // .tags() - let tags: Vec<_> = obj.tags().collect(); - assert_eq!( - tags, - vec![ - Tag(0x0008, 0x0018), - Tag(0x0008, 0x0060), - Tag(0x0010, 0x0010), - ] - ); - - // .into_iter() - let mut iter = obj.into_iter(); - assert_eq!( - iter.next(), - Some(DataElement::new( - Tag(0x0008, 0x0018), - VR::UI, - dicom_value!(Strs, [sop_uid]), - )), - ); - assert_eq!( - iter.next(), - Some(DataElement::new( - Tag(0x0008, 0x0060), - VR::CS, - dicom_value!(Strs, ["CR"]), - )), - ); - assert_eq!( - iter.next(), - Some(DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::from("Doe^John"), - )), - ); - } - - #[test] - fn inmem_empty_object_into_tokens() { - let obj = InMemDicomObject::new_empty(); - let tokens = obj.into_tokens(); - assert_eq!(tokens.count(), 0); - } - - #[test] - fn inmem_shallow_object_from_tokens() { - let tokens = vec![ - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x0060), - vr: VR::CS, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::Str("MG".to_owned())), - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0010, 0x0010), - vr: VR::PN, - len: Length(8), - }), - DataToken::PrimitiveValue(PrimitiveValue::Str("Doe^John".to_owned())), - ]; - - let gt_obj = InMemDicomObject::from_element_iter(vec![ - DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ), - DataElement::new( - Tag(0x0008, 0x0060), - VR::CS, - PrimitiveValue::Str("MG".to_string()), - ), - ]); - - let obj = InMemDicomObject::build_object( - &mut tokens.into_iter().map(Result::Ok), - StandardDataDictionary, - false, - Length::UNDEFINED, - None, - ) - .unwrap(); - - assert_obj_eq(&obj, >_obj); - } - - #[test] - fn inmem_shallow_object_into_tokens() { - let patient_name = DataElement::new( - Tag(0x0010, 0x0010), - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - ); - let modality = DataElement::new( - Tag(0x0008, 0x0060), - VR::CS, - PrimitiveValue::Str("MG".to_string()), - ); - let mut obj = InMemDicomObject::new_empty(); - obj.put(patient_name); - obj.put(modality); - - let tokens: Vec<_> = obj.into_tokens().collect(); - - assert_eq!( - tokens, - vec![ - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x0060), - vr: VR::CS, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::Str("MG".to_owned())), - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0010, 0x0010), - vr: VR::PN, - len: Length(8), - }), - DataToken::PrimitiveValue(PrimitiveValue::Str("Doe^John".to_owned())), - ] - ); - } - - #[test] - fn inmem_deep_object_from_tokens() { - use smallvec::smallvec; - - let obj_1 = InMemDicomObject::from_element_iter(vec![ - DataElement::new(Tag(0x0018, 0x6012), VR::US, Value::Primitive(1_u16.into())), - DataElement::new(Tag(0x0018, 0x6014), VR::US, Value::Primitive(2_u16.into())), - ]); - - let obj_2 = InMemDicomObject::from_element_iter(vec![DataElement::new( - Tag(0x0018, 0x6012), - VR::US, - Value::Primitive(4_u16.into()), - )]); - - let gt_obj = InMemDicomObject::from_element_iter(vec![ - DataElement::new( - Tag(0x0018, 0x6011), - VR::SQ, - Value::from(DataSetSequence::new( - smallvec![obj_1, obj_2], - Length::UNDEFINED, - )), - ), - DataElement::new(Tag(0x0020, 0x4000), VR::LT, Value::Primitive("TEST".into())), - ]); - - let tokens: Vec<_> = vec![ - DataToken::SequenceStart { - tag: Tag(0x0018, 0x6011), - len: Length::UNDEFINED, - }, - DataToken::ItemStart { - len: Length::UNDEFINED, - }, - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0018, 0x6012), - vr: VR::US, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::U16([1].as_ref().into())), - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0018, 0x6014), - vr: VR::US, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::U16([2].as_ref().into())), - DataToken::ItemEnd, - DataToken::ItemStart { - len: Length::UNDEFINED, - }, - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0018, 0x6012), - vr: VR::US, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::U16([4].as_ref().into())), - DataToken::ItemEnd, - DataToken::SequenceEnd, - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0020, 0x4000), - vr: VR::LT, - len: Length(4), - }), - DataToken::PrimitiveValue(PrimitiveValue::Str("TEST".into())), - ]; - - let obj = InMemDicomObject::build_object( - &mut tokens.into_iter().map(Result::Ok), - StandardDataDictionary, - false, - Length::UNDEFINED, - None, - ) - .unwrap(); - - assert_obj_eq(&obj, >_obj); - } - - #[test] - fn inmem_deep_object_into_tokens() { - use smallvec::smallvec; - - let obj_1 = InMemDicomObject::from_element_iter(vec![ - DataElement::new(Tag(0x0018, 0x6012), VR::US, Value::Primitive(1_u16.into())), - DataElement::new(Tag(0x0018, 0x6014), VR::US, Value::Primitive(2_u16.into())), - ]); - - let obj_2 = InMemDicomObject::from_element_iter(vec![DataElement::new( - Tag(0x0018, 0x6012), - VR::US, - Value::Primitive(4_u16.into()), - )]); - - let main_obj = InMemDicomObject::from_element_iter(vec![ - DataElement::new( - Tag(0x0018, 0x6011), - VR::SQ, - Value::from(DataSetSequence::new( - smallvec![obj_1, obj_2], - Length::UNDEFINED, - )), - ), - DataElement::new(Tag(0x0020, 0x4000), VR::LT, Value::Primitive("TEST".into())), - ]); - - let tokens: Vec<_> = main_obj.into_tokens().collect(); - - assert_eq!( - tokens, - vec![ - DataToken::SequenceStart { - tag: Tag(0x0018, 0x6011), - len: Length::UNDEFINED, - }, - DataToken::ItemStart { - len: Length::UNDEFINED, - }, - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0018, 0x6012), - vr: VR::US, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::U16([1].as_ref().into())), - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0018, 0x6014), - vr: VR::US, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::U16([2].as_ref().into())), - DataToken::ItemEnd, - DataToken::ItemStart { - len: Length::UNDEFINED, - }, - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0018, 0x6012), - vr: VR::US, - len: Length(2), - }), - DataToken::PrimitiveValue(PrimitiveValue::U16([4].as_ref().into())), - DataToken::ItemEnd, - DataToken::SequenceEnd, - DataToken::ElementHeader(DataElementHeader { - tag: Tag(0x0020, 0x4000), - vr: VR::LT, - len: Length(4), - }), - DataToken::PrimitiveValue(PrimitiveValue::Str("TEST".into())), - ] - ); - } - - #[test] - fn inmem_encapsulated_pixel_data_from_tokens() { - use smallvec::smallvec; - - let gt_obj = InMemDicomObject::from_element_iter(vec![DataElement::new( - Tag(0x7fe0, 0x0010), - VR::OB, - Value::from(PixelFragmentSequence::new_fragments(smallvec![vec![ - 0x33; - 32 - ]])), - )]); - - let tokens: Vec<_> = vec![ - DataToken::PixelSequenceStart, - DataToken::ItemStart { len: Length(0) }, - DataToken::ItemEnd, - DataToken::ItemStart { len: Length(32) }, - DataToken::ItemValue(vec![0x33; 32]), - DataToken::ItemEnd, - DataToken::SequenceEnd, - ]; - - let obj = InMemDicomObject::build_object( - &mut tokens.into_iter().map(Result::Ok), - StandardDataDictionary, - false, - Length::UNDEFINED, - None, - ) - .unwrap(); - - assert_obj_eq(&obj, >_obj); - } - - #[test] - fn inmem_encapsulated_pixel_data_into_tokens() { - use smallvec::smallvec; - - let main_obj = InMemDicomObject::from_element_iter(vec![DataElement::new( - Tag(0x7fe0, 0x0010), - VR::OB, - Value::from(PixelFragmentSequence::new_fragments(smallvec![vec![ - 0x33; - 32 - ]])), - )]); - - let tokens: Vec<_> = main_obj.into_tokens().collect(); - - assert_eq!( - tokens, - vec![ - DataToken::PixelSequenceStart, - DataToken::ItemStart { len: Length(0) }, - DataToken::ItemEnd, - DataToken::ItemStart { len: Length(32) }, - DataToken::ItemValue(vec![0x33; 32]), - DataToken::ItemEnd, - DataToken::SequenceEnd, - ] - ); - } - - /// Test that a DICOM object can be reliably used - /// behind the `DicomObject` trait. - #[test] - fn can_use_behind_trait() { - fn dicom_dataset() -> impl DicomObject { - InMemDicomObject::from_element_iter([DataElement::new( - tags::PATIENT_NAME, - VR::PN, - PrimitiveValue::Str("Doe^John".to_string()), - )]) - } - - let obj = dicom_dataset(); - let elem1 = obj - .attr_by_name_opt("PatientName") - .unwrap() - .expect("PatientName should be present"); - assert_eq!( - &elem1 - .to_str() - .expect("should be able to retrieve patient name as string"), - "Doe^John" - ); - - // try a missing element, should return None - assert!(obj.attr_opt(tags::PATIENT_ID).unwrap().is_none()); - } - - /// Test attribute operations on in-memory DICOM objects. - #[test] - fn inmem_ops() { - // create a base DICOM object - let base_obj = InMemDicomObject::from_element_iter([ - DataElement::new( - tags::SERIES_INSTANCE_UID, - VR::UI, - PrimitiveValue::from("2.25.137041794342168732369025909031346220736.1"), - ), - DataElement::new( - tags::SERIES_INSTANCE_UID, - VR::UI, - PrimitiveValue::from("2.25.137041794342168732369025909031346220736.1"), - ), - DataElement::new( - tags::SOP_INSTANCE_UID, - VR::UI, - PrimitiveValue::from("2.25.137041794342168732369025909031346220736.1.1"), - ), - DataElement::new( - tags::STUDY_DESCRIPTION, - VR::LO, - PrimitiveValue::from("Test study"), - ), - DataElement::new( - tags::INSTITUTION_NAME, - VR::LO, - PrimitiveValue::from("Test Hospital"), - ), - DataElement::new(tags::ROWS, VR::US, PrimitiveValue::from(768_u16)), - DataElement::new(tags::COLUMNS, VR::US, PrimitiveValue::from(1024_u16)), - DataElement::new( - tags::LOSSY_IMAGE_COMPRESSION, - VR::CS, - PrimitiveValue::from("01"), - ), - DataElement::new( - tags::LOSSY_IMAGE_COMPRESSION_RATIO, - VR::DS, - PrimitiveValue::from("5"), - ), - DataElement::new( - tags::LOSSY_IMAGE_COMPRESSION_METHOD, - VR::DS, - PrimitiveValue::from("ISO_10918_1"), - ), - ]); - - { - // remove - let mut obj = base_obj.clone(); - let op = AttributeOp { - selector: AttributeSelector::from(tags::STUDY_DESCRIPTION), - action: AttributeAction::Remove, - }; - - obj.apply(op).unwrap(); - - assert_eq!(obj.get(tags::STUDY_DESCRIPTION), None); - } - { - let mut obj = base_obj.clone(); - - // set if missing does nothing - // on an existing string - let op = AttributeOp { - selector: tags::INSTITUTION_NAME.into(), - action: AttributeAction::SetIfMissing("Nope Hospital".into()), - }; - - obj.apply(op).unwrap(); - - assert_eq!( - obj.get(tags::INSTITUTION_NAME), - Some(&DataElement::new( - tags::INSTITUTION_NAME, - VR::LO, - PrimitiveValue::from("Test Hospital"), - )) - ); - - // replace string - let op = AttributeOp::new( - tags::INSTITUTION_NAME, - AttributeAction::ReplaceStr("REMOVED".into()), - ); - - obj.apply(op).unwrap(); - - assert_eq!( - obj.get(tags::INSTITUTION_NAME), - Some(&DataElement::new( - tags::INSTITUTION_NAME, - VR::LO, - PrimitiveValue::from("REMOVED"), - )) - ); - - // replacing a non-existing attribute - // does nothing - let op = AttributeOp::new( - tags::REQUESTING_PHYSICIAN, - AttributeAction::ReplaceStr("Doctor^Anonymous".into()), - ); - - obj.apply(op).unwrap(); - - assert_eq!(obj.get(tags::REQUESTING_PHYSICIAN), None); - - // but DetIfMissing works - let op = AttributeOp::new( - tags::REQUESTING_PHYSICIAN, - AttributeAction::SetStrIfMissing("Doctor^Anonymous".into()), - ); - - obj.apply(op).unwrap(); - - assert_eq!( - obj.get(tags::REQUESTING_PHYSICIAN), - Some(&DataElement::new( - tags::REQUESTING_PHYSICIAN, - VR::PN, - PrimitiveValue::from("Doctor^Anonymous"), - )) - ); - } - { - // reset string - let mut obj = base_obj.clone(); - let op = AttributeOp::new( - tags::REQUESTING_PHYSICIAN, - AttributeAction::SetStr("Doctor^Anonymous".into()), - ); - - obj.apply(op).unwrap(); - - assert_eq!( - obj.get(tags::REQUESTING_PHYSICIAN), - Some(&DataElement::new( - tags::REQUESTING_PHYSICIAN, - VR::PN, - PrimitiveValue::from("Doctor^Anonymous"), - )) - ); - } - - { - // extend with number - let mut obj = base_obj.clone(); - let op = AttributeOp::new( - tags::LOSSY_IMAGE_COMPRESSION_RATIO, - AttributeAction::PushF64(1.25), - ); - - obj.apply(op).unwrap(); - - assert_eq!( - obj.get(tags::LOSSY_IMAGE_COMPRESSION_RATIO), - Some(&DataElement::new( - tags::LOSSY_IMAGE_COMPRESSION_RATIO, - VR::DS, - dicom_value!(Strs, ["5", "1.25"]), - )) - ); - } - } - - /// Test attribute operations on nested data sets. - #[test] - fn nested_inmem_ops() { - let obj_1 = InMemDicomObject::from_element_iter([ - DataElement::new(Tag(0x0018, 0x6012), VR::US, PrimitiveValue::from(1_u16)), - DataElement::new(Tag(0x0018, 0x6014), VR::US, PrimitiveValue::from(2_u16)), - ]); - - let obj_2 = InMemDicomObject::from_element_iter([DataElement::new( - Tag(0x0018, 0x6012), - VR::US, - PrimitiveValue::from(4_u16), - )]); - - let mut main_obj = InMemDicomObject::from_element_iter(vec![ - DataElement::new( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - VR::SQ, - DataSetSequence::from(vec![obj_1, obj_2]), - ), - DataElement::new(Tag(0x0020, 0x4000), VR::LT, Value::Primitive("TEST".into())), - ]); - - let selector: AttributeSelector = - (tags::SEQUENCE_OF_ULTRASOUND_REGIONS, 0, Tag(0x0018, 0x6014)).into(); - - main_obj - .apply(AttributeOp::new(selector, AttributeAction::Set(3.into()))) - .unwrap(); - - assert_eq!( - main_obj - .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .unwrap() - .items() - .unwrap()[0] - .get(Tag(0x0018, 0x6014)) - .unwrap() - .value(), - &PrimitiveValue::from(3).into(), - ); - - let selector: AttributeSelector = - (tags::SEQUENCE_OF_ULTRASOUND_REGIONS, 1, Tag(0x0018, 0x6012)).into(); - - main_obj - .apply(AttributeOp::new(selector, AttributeAction::Remove)) - .unwrap(); - - // item should be empty - assert_eq!( - main_obj - .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .unwrap() - .items() - .unwrap()[1] - .tags() - .collect::>(), - Vec::::new(), - ); - - // trying to access the removed element returns an error - assert!(matches!( - main_obj.value_at((tags::SEQUENCE_OF_ULTRASOUND_REGIONS, 1, Tag(0x0018, 0x6012),)), - Err(AtAccessError::MissingLeafElement { .. }) - )) - } - - /// Test that constructive operations create items if necessary. - #[test] - fn constructive_op() { - let mut obj = InMemDicomObject::from_element_iter([DataElement::new( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - VR::SQ, - DataSetSequence::empty(), - )]); - - let op = AttributeOp::new( - ( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - 0, - tags::REGION_SPATIAL_FORMAT, - ), - AttributeAction::Set(5_u16.into()), - ); - - obj.apply(op).unwrap(); - - // should have an item - assert_eq!( - obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .unwrap() - .items() - .unwrap() - .len(), - 1, - ); - - // item should have 1 element - assert_eq!( - &obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .unwrap() - .items() - .unwrap()[0], - &InMemDicomObject::from_element_iter([DataElement::new( - tags::REGION_SPATIAL_FORMAT, - VR::US, - PrimitiveValue::from(5_u16) - )]), - ); - - // new value can be accessed using value_at - assert_eq!( - obj.value_at(( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - 0, - tags::REGION_SPATIAL_FORMAT - )) - .unwrap(), - &Value::from(PrimitiveValue::from(5_u16)), - ) - } - - /// Test that operations on in-memory DICOM objects - /// can create sequences from scratch. - #[test] - fn inmem_ops_can_create_seq() { - let mut obj = InMemDicomObject::new_empty(); - - obj.apply(AttributeOp::new( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - AttributeAction::SetIfMissing(PrimitiveValue::Empty), - )) - .unwrap(); - - { - // should create an empty sequence - let sequence_ultrasound = obj - .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .expect("should have sequence element"); - - assert_eq!(sequence_ultrasound.vr(), VR::SQ); - - assert_eq!(sequence_ultrasound.items(), Some(&[][..]),); - } - - obj.apply(AttributeOp::new( - ( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - tags::REGION_SPATIAL_FORMAT, - ), - AttributeAction::Set(1_u16.into()), - )) - .unwrap(); - - { - // sequence should now have an item - assert_eq!( - obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .unwrap() - .items() - .map(|items| items.len()), - Some(1), - ); - } - } - - /// Test that operations on in-memory DICOM objects - /// can create deeply nested attributes from scratch. - #[test] - fn inmem_ops_can_create_nested_attribute() { - let mut obj = InMemDicomObject::new_empty(); - - obj.apply(AttributeOp::new( - ( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - tags::REGION_SPATIAL_FORMAT, - ), - AttributeAction::Set(1_u16.into()), - )) - .unwrap(); - - { - // should create a sequence with a single item - assert_eq!( - obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .unwrap() - .items() - .map(|items| items.len()), - Some(1), - ); - - // item should have Region Spatial Format - assert_eq!( - obj.value_at(( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - tags::REGION_SPATIAL_FORMAT - )) - .unwrap(), - &PrimitiveValue::from(1_u16).into(), - ); - - // same result when using `DicomObject::at` - assert_eq!( - DicomObject::at(&obj, ( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - tags::REGION_SPATIAL_FORMAT - )) - .unwrap(), - &PrimitiveValue::from(1_u16).into(), - ); - } - } - - /// Test that operations on in-memory DICOM objects - /// can truncate sequences. - #[test] - fn inmem_ops_can_truncate_seq() { - let mut obj = InMemDicomObject::from_element_iter([ - DataElement::new( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - VR::SQ, - DataSetSequence::from(vec![InMemDicomObject::new_empty()]), - ), - DataElement::new_with_len( - tags::PIXEL_DATA, - VR::OB, - Length::UNDEFINED, - PixelFragmentSequence::new(vec![], vec![vec![0xcc; 8192], vec![0x55; 1024]]), - ), - ]); - - // removes the single item in the sequences - obj.apply(AttributeOp::new( - tags::SEQUENCE_OF_ULTRASOUND_REGIONS, - AttributeAction::Truncate(0), - )) - .unwrap(); - - { - let sequence_ultrasound = obj - .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) - .expect("should have sequence element"); - assert_eq!(sequence_ultrasound.items(), Some(&[][..]),); - } - - // remove one of the fragments - obj.apply(AttributeOp::new( - tags::PIXEL_DATA, - AttributeAction::Truncate(1), - )) - .unwrap(); - - { - // pixel data should now have a single fragment - assert_eq!( - obj.get(tags::PIXEL_DATA) - .unwrap() - .fragments() - .map(|fragments| fragments.len()), - Some(1), - ); - } - } - - #[test] - fn inmem_obj_reset_defined_length() { - let mut entries: BTreeMap> = BTreeMap::new(); - - let patient_name = - DataElement::new(tags::PATIENT_NAME, VR::CS, PrimitiveValue::from("Doe^John")); - - let study_description = DataElement::new( - tags::STUDY_DESCRIPTION, - VR::LO, - PrimitiveValue::from("Test study"), - ); - - entries.insert(tags::PATIENT_NAME, patient_name.clone()); - - // create object and force an arbitrary defined Length value - let obj = InMemDicomObject:: { - entries, - dict: StandardDataDictionary, - len: Length(1), - charset_changed: false, - }; - - assert!(obj.length().is_defined()); - - let mut o = obj.clone(); - o.put_element(study_description); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.remove_element(tags::PATIENT_NAME); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.remove_element_by_name("PatientName").unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.take_element(tags::PATIENT_NAME).unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.take_element_by_name("PatientName").unwrap(); - assert!(o.length().is_undefined()); - - // resets Length even when retain does not make any changes - let mut o = obj.clone(); - o.retain(|e| e.tag() == tags::PATIENT_NAME); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::Remove, - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new(tags::PATIENT_NAME, AttributeAction::Empty)) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::SetVr(VR::IS), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::Set(dicom_value!(Str, "Unknown")), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::SetStr("Patient^Anonymous".into()), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_AGE, - AttributeAction::SetIfMissing(dicom_value!(75)), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_ADDRESS, - AttributeAction::SetStrIfMissing("Chicago".into()), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::Replace(dicom_value!(Str, "Unknown")), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::ReplaceStr("Unknown".into()), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushStr("^Prof".into()), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushI32(-16), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushU32(16), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushI16(-16), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushU16(16), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushF32(16.16), - )) - .unwrap(); - assert!(o.length().is_undefined()); - - let mut o = obj.clone(); - o.apply(AttributeOp::new( - tags::PATIENT_NAME, - AttributeAction::PushF64(16.1616), - )) - .unwrap(); - assert!(o.length().is_undefined()); - } - - #[test] - fn create_commands() { - // empty - let obj = InMemDicomObject::command_from_element_iter([]); - assert_eq!( - obj.get(tags::COMMAND_GROUP_LENGTH) - .map(|e| e.value().to_int::().unwrap()), - Some(0) - ); - - // C-FIND-RQ - let obj = InMemDicomObject::command_from_element_iter([ - // affected SOP class UID: 8 + 28 = 36 - DataElement::new( - tags::AFFECTED_SOP_CLASS_UID, - VR::UI, - PrimitiveValue::from("1.2.840.10008.5.1.4.1.2.1.1"), - ), - // command field: 36 + 8 + 2 = 46 - DataElement::new( - tags::COMMAND_FIELD, - VR::US, - // 0020H: C-FIND-RQ message - dicom_value!(U16, [0x0020]), - ), - // message ID: 46 + 8 + 2 = 56 - DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [0])), - //priority: 56 + 8 + 2 = 66 - DataElement::new( - tags::PRIORITY, - VR::US, - // medium - dicom_value!(U16, [0x0000]), - ), - // data set type: 66 + 8 + 2 = 76 - DataElement::new( - tags::COMMAND_DATA_SET_TYPE, - VR::US, - dicom_value!(U16, [0x0001]), - ), - ]); - assert_eq!( - obj.get(tags::COMMAND_GROUP_LENGTH) - .map(|e| e.value().to_int::().unwrap()), - Some(76) - ); - - let storage_sop_class_uid = "1.2.840.10008.5.1.4.1.1.4"; - let storage_sop_instance_uid = "2.25.221314879990624101283043547144116927116"; - - // C-STORE-RQ - let obj = InMemDicomObject::command_from_element_iter([ - // group length (should be ignored in calculations and overridden) - DataElement::new( - tags::COMMAND_GROUP_LENGTH, - VR::UL, - PrimitiveValue::from(9999_u32), - ), - // SOP Class UID: 8 + 26 = 34 - DataElement::new( - tags::AFFECTED_SOP_CLASS_UID, - VR::UI, - dicom_value!(Str, storage_sop_class_uid), - ), - // command field: 34 + 8 + 2 = 44 - DataElement::new(tags::COMMAND_FIELD, VR::US, dicom_value!(U16, [0x0001])), - // message ID: 44 + 8 + 2 = 54 - DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [1])), - //priority: 54 + 8 + 2 = 64 - DataElement::new(tags::PRIORITY, VR::US, dicom_value!(U16, [0x0000])), - // data set type: 64 + 8 + 2 = 74 - DataElement::new( - tags::COMMAND_DATA_SET_TYPE, - VR::US, - dicom_value!(U16, [0x0000]), - ), - // affected SOP Instance UID: 74 + 8 + 44 = 126 - DataElement::new( - tags::AFFECTED_SOP_INSTANCE_UID, - VR::UI, - dicom_value!(Str, storage_sop_instance_uid), - ), - ]); - - assert_eq!( - obj.get(tags::COMMAND_GROUP_LENGTH) - .map(|e| e.value().to_int::().unwrap()), - Some(126) - ); - } - - #[test] - fn test_even_len() { - assert_eq!(even_len(0), 0); - assert_eq!(even_len(1), 2); - assert_eq!(even_len(2), 2); - assert_eq!(even_len(3), 4); - assert_eq!(even_len(4), 4); - assert_eq!(even_len(5), 6); - } - - #[test] - fn can_update_value() { - let mut obj = InMemDicomObject::from_element_iter([DataElement::new( - tags::ANATOMIC_REGION_SEQUENCE, - VR::SQ, - DataSetSequence::empty(), - )]); - assert_eq!( - obj.get(tags::ANATOMIC_REGION_SEQUENCE).map(|e| e.length()), - Some(Length(0)), - ); - - assert!(!obj.update_value(tags::BURNED_IN_ANNOTATION, |_value| { - panic!("should not be called") - }),); - - let o = obj.update_value(tags::ANATOMIC_REGION_SEQUENCE, |value| { - // add an item - let items = value.items_mut().unwrap(); - items.push(InMemDicomObject::from_element_iter([DataElement::new( - tags::INSTANCE_NUMBER, - VR::IS, - PrimitiveValue::from(1), - )])); - }); - assert!(o); - - assert!(obj - .get(tags::ANATOMIC_REGION_SEQUENCE) - .unwrap() - .length() - .is_undefined()); - } - - #[test] - fn deep_sequence_change_encoding_writes_undefined_sequence_length() { - use smallvec::smallvec; - - let obj_1 = InMemDicomObject::from_element_iter(vec![ - //The length of this string is 20 bytes in ISO_IR 100 but should be 22 bytes in ISO_IR 192 (UTF-8) - DataElement::new( - tags::STUDY_DESCRIPTION, - VR::SL, - Value::Primitive("MORFOLOGÍA Y FUNCIÓN".into()), - ), - //ISO_IR 100 and ISO_IR 192 length are the same - DataElement::new( - tags::SERIES_DESCRIPTION, - VR::SL, - Value::Primitive("0123456789".into()), - ), - ]); - - let some_tag = Tag(0x0018, 0x6011); - - let inner_sequence = InMemDicomObject::from_element_iter(vec![DataElement::new( - some_tag, - VR::SQ, - Value::from(DataSetSequence::new( - smallvec![obj_1], - Length(30), //20 bytes from study, 10 from series - )), - )]); - let outer_sequence = DataElement::new( - some_tag, - VR::SQ, - Value::from(DataSetSequence::new( - smallvec![inner_sequence.clone(), inner_sequence], - Length(60), //20 bytes from study, 10 from series - )), - ); - - let original_object = InMemDicomObject::from_element_iter(vec![ - DataElement::new(tags::SPECIFIC_CHARACTER_SET, VR::CS, "ISO_IR 100"), - outer_sequence, - ]); - - assert_eq!( - original_object - .get(some_tag) - .expect("object should be present") - .length(), - Length(60) - ); - - let mut changed_charset = original_object.clone(); - changed_charset.convert_to_utf8(); - assert!(changed_charset.charset_changed); - - use dicom_parser::dataset::DataToken as token; - let options = IntoTokensOptions::new(true); - let converted_tokens: Vec<_> = changed_charset.into_tokens_with_options(options).collect(); - - assert_eq!( - vec![ - token::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x0005), - vr: VR::CS, - len: Length(10), - }), - token::PrimitiveValue("ISO_IR 192".into()), - token::SequenceStart { - tag: Tag(0x0018, 0x6011), - len: Length::UNDEFINED, - }, - token::ItemStart { - len: Length::UNDEFINED - }, - token::SequenceStart { - tag: Tag(0x0018, 0x6011), - len: Length::UNDEFINED, - }, - token::ItemStart { - len: Length::UNDEFINED - }, - token::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x1030), - vr: VR::SL, - len: Length(22), - }), - token::PrimitiveValue("MORFOLOGÍA Y FUNCIÓN".into()), - token::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x103E), - vr: VR::SL, - len: Length(10), - }), - token::PrimitiveValue("0123456789".into()), - token::ItemEnd, - token::SequenceEnd, - token::ItemEnd, - token::ItemStart { - len: Length::UNDEFINED - }, - token::SequenceStart { - tag: Tag(0x0018, 0x6011), - len: Length::UNDEFINED, - }, - token::ItemStart { - len: Length::UNDEFINED - }, - token::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x1030), - vr: VR::SL, - len: Length(22), - }), - token::PrimitiveValue("MORFOLOGÍA Y FUNCIÓN".into()), - token::ElementHeader(DataElementHeader { - tag: Tag(0x0008, 0x103E), - vr: VR::SL, - len: Length(10), - }), - token::PrimitiveValue("0123456789".into()), - token::ItemEnd, - token::SequenceEnd, - token::ItemEnd, - token::SequenceEnd, - ], - 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(); - - let res = ds.put_private_element( - 0x0012, - "CREATOR 4", - 0x02, - VR::DS, - PrimitiveValue::Str("1.0".to_string()), - ); - assert_eq!( - &res.err().unwrap().to_string(), - "Group number must be odd, found 0x0012" - ); - - 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) - ); - } - - #[test] - fn private_element_group_full() { - let mut ds = InMemDicomObject::from_element_iter( - (0..=0x00FFu16) - .map(|i| { - DataElement::new(Tag(0x0009, i), VR::LO, PrimitiveValue::from("CREATOR 1")) - }) - .collect::>>(), - ); - let res = ds.put_private_element(0x0009, "TEST", 0x01, VR::DS, PrimitiveValue::from("1.0")); - assert_eq!( - res.err().unwrap().to_string(), - "No space available in group 0x0009" - ); - } -} diff --git a/object/src/meta.rs b/object/src/meta.rs deleted file mode 100644 index a4dbaa6b..00000000 --- a/object/src/meta.rs +++ /dev/null @@ -1,1802 +0,0 @@ -//! Module containing data structures and readers of DICOM file meta information tables. -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, AttributeSelector, AttributeSelectorStep, -}; -use dicom_core::value::{ - ConvertValueError, DicomValueType, InMemFragment, 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::borrow::Cow; -use std::io::{Read, Write}; - -use crate::ops::{ - ApplyError, ApplyResult, IllegalExtendSnafu, IncompatibleTypesSnafu, MandatorySnafu, - UnsupportedActionSnafu, UnsupportedAttributeSnafu, -}; -use crate::{ - AtAccessError, AttributeError, DicomAttribute, DicomObject, 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 { - /// The file meta group parser could not read - /// the magic code `DICM` from its source. - #[snafu(display("Could not start reading DICOM data"))] - ReadMagicCode { - backtrace: Backtrace, - source: std::io::Error, - }, - - /// The file meta group parser could not fetch - /// the value of a data element from its source. - #[snafu(display("Could not read data value"))] - ReadValueData { - backtrace: Backtrace, - source: std::io::Error, - }, - - /// The parser could not allocate memory for the - /// given length of a data element. - #[snafu(display("Could not allocate memory"))] - AllocationSize { - backtrace: Backtrace, - source: std::collections::TryReserveError, - }, - - /// The file meta group parser could not decode - /// the text in one of its data elements. - #[snafu(display("Could not decode text in {}", name))] - DecodeText { - name: std::borrow::Cow<'static, str>, - #[snafu(backtrace)] - source: dicom_encoding::text::DecodeTextError, - }, - - /// Invalid DICOM data, detected by checking the `DICM` code. - #[snafu(display("Invalid DICOM file (magic code check failed)"))] - NotDicom { backtrace: Backtrace }, - - /// An issue occurred while decoding the next data element - /// in the file meta data set. - #[snafu(display("Could not decode data element"))] - DecodeElement { - #[snafu(backtrace)] - source: dicom_encoding::decode::Error, - }, - - /// A data element with an unexpected tag was retrieved: - /// the parser was expecting another tag first, - /// or at least one that is part of the the file meta group. - #[snafu(display("Unexpected data element tagged {}", tag))] - UnexpectedTag { tag: Tag, backtrace: Backtrace }, - - /// A required file meta data element is missing. - #[snafu(display("Missing data element `{}`", alias))] - MissingElement { - alias: &'static str, - backtrace: Backtrace, - }, - - /// The value length of a data elements in the file meta group - /// was unexpected. - #[snafu(display("Unexpected length {} for data element tagged {}", length, tag))] - UnexpectedDataValueLength { - tag: Tag, - length: Length, - backtrace: Backtrace, - }, - - /// The value length of a data element is undefined, - /// but knowing the length is required in its context. - #[snafu(display("Undefined value length for data element tagged {}", tag))] - UndefinedValueLength { tag: Tag, backtrace: Backtrace }, - - /// The file meta group data set could not be written. - #[snafu(display("Could not write file meta group data set"))] - WriteSet { - #[snafu(backtrace)] - source: dicom_parser::dataset::write::Error, - }, -} - -type Result = std::result::Result; - -/// DICOM File Meta Information Table. -/// -/// This data type contains the relevant parts of the file meta information table, -/// as specified in [part 6, chapter 7][1] of the standard. -/// -/// Creating a new file meta table from scratch -/// is more easily done using a [`FileMetaTableBuilder`]. -/// When modifying the struct's public fields, -/// it is possible to update the information group length -/// through method [`update_information_group_length`][2]. -/// -/// [1]: http://dicom.nema.org/medical/dicom/current/output/chtml/part06/chapter_7.html -/// [2]: FileMetaTable::update_information_group_length -#[derive(Debug, Clone, PartialEq)] -pub struct FileMetaTable { - /// File Meta Information Group Length - pub information_group_length: u32, - /// File Meta Information Version - pub information_version: [u8; 2], - /// Media Storage SOP Class UID - pub media_storage_sop_class_uid: String, - /// Media Storage SOP Instance UID - pub media_storage_sop_instance_uid: String, - /// Transfer Syntax UID - pub transfer_syntax: String, - /// Implementation Class UID - pub implementation_class_uid: String, - - /// Implementation Version Name - pub implementation_version_name: Option, - /// Source Application Entity Title - pub source_application_entity_title: Option, - /// Sending Application Entity Title - pub sending_application_entity_title: Option, - /// Receiving Application Entity Title - pub receiving_application_entity_title: Option, - /// Private Information Creator UID - pub private_information_creator_uid: Option, - /// Private Information - pub private_information: Option>, - /* - Missing attributes: - - (0002,0026) Source Presentation Address Source​Presentation​Address UR 1 - (0002,0027) Sending Presentation Address Sending​Presentation​Address UR 1 - (0002,0028) Receiving Presentation Address Receiving​Presentation​Address UR 1 - (0002,0031) RTV Meta Information Version RTV​Meta​Information​Version OB 1 - (0002,0032) RTV Communication SOP Class UID RTV​Communication​SOP​Class​UID UI 1 - (0002,0033) RTV Communication SOP Instance UID RTV​Communication​SOP​Instance​UID UI 1 - (0002,0035) RTV Source Identifier RTV​Source​Identifier OB 1 - (0002,0036) RTV Flow Identifier RTV​Flow​Identifier OB 1 - (0002,0037) RTV Flow RTP Sampling Rate RTV​Flow​RTP​Sampling​Rate UL 1 - (0002,0038) RTV Flow Actual Frame Duration RTV​Flow​Actual​Frame​Duration FD 1 - */ -} - -/// Utility function for reading the body of the DICOM element as a UID. -fn read_str_body<'s, S, T>(source: &'s mut S, text: &T, len: u32) -> Result -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 { - /// Construct a file meta group table - /// by parsing a DICOM data set from a reader. - /// - /// This method fails if the first four bytes - /// are not the DICOM magic code `DICM`. - pub fn from_reader(file: R) -> Result { - FileMetaTable::read_from(file) - } - - /// Getter for the transfer syntax UID, - /// with trailing characters already excluded. - pub fn transfer_syntax(&self) -> &str { - self.transfer_syntax - .trim_end_matches(|c: char| c.is_whitespace() || c == '\0') - } - - /// Getter for the media storage SOP instance UID, - /// with trailing characters already excluded. - 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') - } - - /// Getter for the media storage SOP class UID, - /// with trailing characters already excluded. - 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') - } - - /// Getter for the implementation class UID, - /// with trailing characters already excluded. - pub fn implementation_class_uid(&self) -> &str { - self.implementation_class_uid - .trim_end_matches(|c: char| c.is_whitespace() || c == '\0') - } - - /// Getter for the private information creator UID, - /// with trailing characters already excluded. - 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')) - } - - /// Set the file meta table's transfer syntax - /// according to the given transfer syntax descriptor. - /// - /// This replaces the table's transfer syntax UID - /// to the given transfer syntax, without padding to even length. - /// The information group length field is automatically recalculated. - pub fn set_transfer_syntax(&mut self, ts: &TransferSyntax) { - self.transfer_syntax = ts - .uid() - .trim_end_matches(|c: char| c.is_whitespace() || c == '\0') - .to_string(); - self.update_information_group_length(); - } - - /// Calculate the expected file meta group length - /// according to the file meta attributes currently set, - /// and assign it to the field `information_group_length`. - pub fn update_information_group_length(&mut self) { - self.information_group_length = self.calculate_information_group_length(); - } - - /// Apply the given attribute operation on this file meta information table. - /// - /// See the [`dicom_core::ops`] module - /// for more information. - 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(_) - ) => - { - // any other attribute is not supported - // (ignore Remove, Empty, 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(_) => { - // ignore - Ok(()) - } - AttributeAction::Set(value) | AttributeAction::Replace(value) => { - // require value to be textual - 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(_) => { - // no-op - 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, - ) -> 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(_) => { - // ignore - Ok(()) - } - AttributeAction::Set(value) => { - // require value to be textual - 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(()); - } - - // require value to be textual - 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(()); - } - - // require value to be textual - 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(), - } - } - - /// Calculate the expected file meta group length, - /// ignoring `information_group_length`. - fn calculate_information_group_length(&self) -> u32 { - // determine the expected meta group size based on the given fields. - // attribute FileMetaInformationGroupLength is not included - // in the calculations intentionally - 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) - } - - /// Read the DICOM magic code (`b"DICM"`) - /// and the whole file meta group from the given reader. - fn read_from(mut file: S) -> Result { - let mut buff: [u8; 4] = [0; 4]; - { - // check magic code - 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); - - // Fetch optional data elements - 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) => { - // Implementation Version - 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) - } - // Media Storage SOP Class UID - Tag(0x0002, 0x0002) => { - builder.media_storage_sop_class_uid(read_str_body(&mut file, &text, elem_len)?) - } - // Media Storage SOP Instance UID - Tag(0x0002, 0x0003) => builder - .media_storage_sop_instance_uid(read_str_body(&mut file, &text, elem_len)?), - // Transfer Syntax - Tag(0x0002, 0x0010) => { - builder.transfer_syntax(read_str_body(&mut file, &text, elem_len)?) - } - // Implementation Class UID - Tag(0x0002, 0x0012) => { - builder.implementation_class_uid(read_str_body(&mut file, &text, elem_len)?) - } - Tag(0x0002, 0x0013) => { - // Implementation Version Name - 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) => { - // Source Application Entity Title - 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) => { - // Sending Application Entity Title - 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) => { - // Receiving Application Entity Title - 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) => { - // Private Information Creator UID - 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) => { - // Private Information - 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, _) => { - // unknown tag, do nothing - // could be an unsupported or non-standard attribute - tracing::info!("Unknown tag {}", tag); - // consume value without saving it - 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 { - // reported element length longer than actual stream - return UnexpectedDataValueLengthSnafu { - tag: elem.tag(), - length: elem_len, - } - .fail(); - } - builder - } - tag => { - // unexpected tag from another group! do nothing for now, - // but this could pose an issue up ahead (see #50) - tracing::warn!("Unexpected off-group tag {}", tag); - // consume value without saving it - 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 { - // reported element length longer than actual stream - 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() - } - - /// Create an iterator over the defined data elements - /// of the file meta group, - /// consuming the file meta table. - /// - /// See [`to_element_iter`](FileMetaTable::to_element_iter) - /// for a version which copies the element from the table. - pub fn into_element_iter(self) -> impl Iterator> { - let mut elems = vec![ - // file information group length - 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() - } - - /// Create an iterator of data elements copied from the file meta group. - /// - /// See [`into_element_iter`](FileMetaTable::into_element_iter) - /// for a version which consumes the table. - pub fn to_element_iter(&self) -> impl Iterator> + '_ { - self.clone().into_element_iter() - } - - pub fn write(&self, writer: W) -> Result<()> { - let mut dset = DataSetWriter::new( - writer, - EncoderFor::new(ExplicitVRLittleEndianEncoder::default()), - ); - //There are no sequences in the `FileMetaTable`, so the value of `invalidate_sq_len` is - //not important - dset.write_sequence( - self.clone() - .into_element_iter() - .flat_map(IntoTokens::into_tokens), - ) - .context(WriteSetSnafu)?; - - dset.flush().context(WriteSetSnafu) - } -} - -/// An attribute selector for a file meta information table. -#[derive(Debug)] -pub struct FileMetaAttribute<'a> { - meta: &'a FileMetaTable, - tag_e: u16, -} - -impl HasLength for FileMetaAttribute<'_> { - fn length(&self) -> Length { - match Tag(0x0002, self.tag_e) { - tags::FILE_META_INFORMATION_GROUP_LENGTH => Length(4), - tags::MEDIA_STORAGE_SOP_CLASS_UID => { - Length(self.meta.media_storage_sop_class_uid.len() as u32) - } - tags::MEDIA_STORAGE_SOP_INSTANCE_UID => { - Length(self.meta.media_storage_sop_instance_uid.len() as u32) - } - tags::IMPLEMENTATION_CLASS_UID => { - Length(self.meta.implementation_class_uid.len() as u32) - } - tags::IMPLEMENTATION_VERSION_NAME => Length( - self.meta - .implementation_version_name - .as_ref() - .map(|s| s.len() as u32) - .unwrap_or(0), - ), - tags::SOURCE_APPLICATION_ENTITY_TITLE => Length( - self.meta - .source_application_entity_title - .as_ref() - .map(|s| s.len() as u32) - .unwrap_or(0), - ), - tags::SENDING_APPLICATION_ENTITY_TITLE => Length( - self.meta - .sending_application_entity_title - .as_ref() - .map(|s| s.len() as u32) - .unwrap_or(0), - ), - tags::TRANSFER_SYNTAX_UID => Length(self.meta.transfer_syntax.len() as u32), - tags::PRIVATE_INFORMATION_CREATOR_UID => Length( - self.meta - .private_information_creator_uid - .as_ref() - .map(|s| s.len() as u32) - .unwrap_or(0), - ), - _ => unreachable!(), - } - } -} - -impl DicomValueType for FileMetaAttribute<'_> { - fn value_type(&self) -> ValueType { - match Tag(0x0002, self.tag_e) { - tags::MEDIA_STORAGE_SOP_CLASS_UID - | tags::MEDIA_STORAGE_SOP_INSTANCE_UID - | tags::TRANSFER_SYNTAX_UID - | tags::IMPLEMENTATION_CLASS_UID - | tags::IMPLEMENTATION_VERSION_NAME - | tags::SOURCE_APPLICATION_ENTITY_TITLE - | tags::SENDING_APPLICATION_ENTITY_TITLE - | tags::RECEIVING_APPLICATION_ENTITY_TITLE - | tags::PRIVATE_INFORMATION_CREATOR_UID => ValueType::Str, - tags::FILE_META_INFORMATION_GROUP_LENGTH => ValueType::U32, - tags::FILE_META_INFORMATION_VERSION => ValueType::U8, - tags::PRIVATE_INFORMATION => ValueType::U8, - _ => unreachable!(), - } - } - - fn cardinality(&self) -> usize { - match Tag(0x0002, self.tag_e) { - tags::MEDIA_STORAGE_SOP_CLASS_UID - | tags::MEDIA_STORAGE_SOP_INSTANCE_UID - | tags::SOURCE_APPLICATION_ENTITY_TITLE - | tags::SENDING_APPLICATION_ENTITY_TITLE - | tags::RECEIVING_APPLICATION_ENTITY_TITLE - | tags::TRANSFER_SYNTAX_UID - | tags::IMPLEMENTATION_CLASS_UID - | tags::IMPLEMENTATION_VERSION_NAME - | tags::PRIVATE_INFORMATION_CREATOR_UID => 1, - tags::FILE_META_INFORMATION_GROUP_LENGTH => 1, - tags::PRIVATE_INFORMATION => 1, - tags::FILE_META_INFORMATION_VERSION => 2, - _ => 1, - } - } -} - -impl DicomAttribute for FileMetaAttribute<'_> { - type Item<'b> - = EmptyObject - where - Self: 'b; - type PixelData<'b> - = InMemFragment - where - Self: 'b; - - fn to_primitive_value(&self) -> Result { - Ok(match Tag(0x0002, self.tag_e) { - tags::FILE_META_INFORMATION_GROUP_LENGTH => { - PrimitiveValue::from(self.meta.information_group_length) - } - tags::FILE_META_INFORMATION_VERSION => { - PrimitiveValue::from(self.meta.information_version) - } - tags::MEDIA_STORAGE_SOP_CLASS_UID => { - PrimitiveValue::from(self.meta.media_storage_sop_class_uid.clone()) - } - tags::MEDIA_STORAGE_SOP_INSTANCE_UID => { - PrimitiveValue::from(self.meta.media_storage_sop_instance_uid.clone()) - } - tags::SOURCE_APPLICATION_ENTITY_TITLE => { - PrimitiveValue::from(self.meta.source_application_entity_title.clone().unwrap()) - } - tags::SENDING_APPLICATION_ENTITY_TITLE => { - PrimitiveValue::from(self.meta.sending_application_entity_title.clone().unwrap()) - } - tags::RECEIVING_APPLICATION_ENTITY_TITLE => PrimitiveValue::from( - self.meta - .receiving_application_entity_title - .clone() - .unwrap(), - ), - tags::TRANSFER_SYNTAX_UID => PrimitiveValue::from(self.meta.transfer_syntax.clone()), - tags::IMPLEMENTATION_CLASS_UID => { - PrimitiveValue::from(self.meta.implementation_class_uid.clone()) - } - tags::IMPLEMENTATION_VERSION_NAME => { - PrimitiveValue::from(self.meta.implementation_version_name.clone().unwrap()) - } - tags::PRIVATE_INFORMATION_CREATOR_UID => { - PrimitiveValue::from(self.meta.private_information_creator_uid.clone().unwrap()) - } - tags::PRIVATE_INFORMATION => { - PrimitiveValue::from(self.meta.private_information.clone().unwrap()) - } - _ => unreachable!(), - }) - } - - fn to_str(&self) -> std::result::Result, AttributeError> { - match Tag(0x0002, self.tag_e) { - tags::FILE_META_INFORMATION_GROUP_LENGTH => { - Ok(self.meta.information_group_length.to_string().into()) - } - tags::FILE_META_INFORMATION_VERSION => Ok(format!( - "{:02X}{:02X}", - self.meta.information_version[0], self.meta.information_version[1] - ) - .into()), - tags::MEDIA_STORAGE_SOP_CLASS_UID => { - Ok(Cow::Borrowed(self.meta.media_storage_sop_class_uid())) - } - tags::MEDIA_STORAGE_SOP_INSTANCE_UID => { - Ok(Cow::Borrowed(self.meta.media_storage_sop_instance_uid())) - } - tags::TRANSFER_SYNTAX_UID => Ok(Cow::Borrowed(self.meta.transfer_syntax())), - tags::IMPLEMENTATION_CLASS_UID => { - Ok(Cow::Borrowed(self.meta.implementation_class_uid())) - } - tags::IMPLEMENTATION_VERSION_NAME => Ok(self - .meta - .implementation_version_name - .as_deref() - .map(Cow::Borrowed) - .unwrap_or_default()), - tags::SOURCE_APPLICATION_ENTITY_TITLE => Ok(self - .meta - .source_application_entity_title - .as_deref() - .map(Cow::Borrowed) - .unwrap_or_default()), - tags::SENDING_APPLICATION_ENTITY_TITLE => Ok(self - .meta - .sending_application_entity_title - .as_deref() - .map(Cow::Borrowed) - .unwrap_or_default()), - tags::RECEIVING_APPLICATION_ENTITY_TITLE => Ok(self - .meta - .receiving_application_entity_title - .as_deref() - .map(Cow::Borrowed) - .unwrap_or_default()), - tags::PRIVATE_INFORMATION_CREATOR_UID => Ok(self - .meta - .private_information_creator_uid - .as_deref() - .map(|v| { - Cow::Borrowed(v.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')) - }) - .unwrap_or_default()), - tags::PRIVATE_INFORMATION => Err(AttributeError::ConvertValue { - source: ConvertValueError { - cause: None, - original: ValueType::U8, - requested: "str", - }, - }), - _ => unreachable!(), - } - } - - fn item(&self, _index: u32) -> Result, AttributeError> { - Err(AttributeError::NotDataSet) - } - - fn num_items(&self) -> Option { - None - } - - fn fragment(&self, _index: u32) -> Result, AttributeError> { - Err(AttributeError::NotPixelData) - } - - fn num_fragments(&self) -> Option { - None - } -} - -impl DicomObject for FileMetaTable { - type Attribute<'a> - = FileMetaAttribute<'a> - where - Self: 'a; - - type LeafAttribute<'a> - = FileMetaAttribute<'a> - where - Self: 'a; - - fn attr_opt( - &self, - tag: Tag, - ) -> std::result::Result>, crate::AccessError> { - // check that the attribute value is in the table, - // then return a suitable `FileMetaAttribute` - - if match tag { - // mandatory attributes - tags::FILE_META_INFORMATION_GROUP_LENGTH - | tags::FILE_META_INFORMATION_VERSION - | tags::MEDIA_STORAGE_SOP_CLASS_UID - | tags::MEDIA_STORAGE_SOP_INSTANCE_UID - | tags::TRANSFER_SYNTAX_UID - | tags::IMPLEMENTATION_CLASS_UID - | tags::IMPLEMENTATION_VERSION_NAME => true, - // optional attributes - tags::SOURCE_APPLICATION_ENTITY_TITLE - if self.source_application_entity_title.is_some() => - { - true - } - tags::SENDING_APPLICATION_ENTITY_TITLE - if self.sending_application_entity_title.is_some() => - { - true - } - tags::RECEIVING_APPLICATION_ENTITY_TITLE - if self.receiving_application_entity_title.is_some() => - { - true - } - tags::PRIVATE_INFORMATION_CREATOR_UID - if self.private_information_creator_uid.is_some() => - { - true - } - tags::PRIVATE_INFORMATION if self.private_information.is_some() => true, - _ => false, - } { - Ok(Some(FileMetaAttribute { - meta: self, - tag_e: tag.element(), - })) - } else { - Ok(None) - } - } - - fn attr_by_name_opt<'a>( - &'a self, - name: &str, - ) -> std::result::Result>, crate::AccessByNameError> { - let tag = match name { - "FileMetaInformationGroupLength" => tags::FILE_META_INFORMATION_GROUP_LENGTH, - "FileMetaInformationVersion" => tags::FILE_META_INFORMATION_VERSION, - "MediaStorageSOPClassUID" => tags::MEDIA_STORAGE_SOP_CLASS_UID, - "MediaStorageSOPInstanceUID" => tags::MEDIA_STORAGE_SOP_INSTANCE_UID, - "TransferSyntaxUID" => tags::TRANSFER_SYNTAX_UID, - "ImplementationClassUID" => tags::IMPLEMENTATION_CLASS_UID, - "ImplementationVersionName" => tags::IMPLEMENTATION_VERSION_NAME, - "SourceApplicationEntityTitle" => tags::SOURCE_APPLICATION_ENTITY_TITLE, - "SendingApplicationEntityTitle" => tags::SENDING_APPLICATION_ENTITY_TITLE, - "ReceivingApplicationEntityTitle" => tags::RECEIVING_APPLICATION_ENTITY_TITLE, - "PrivateInformationCreatorUID" => tags::PRIVATE_INFORMATION_CREATOR_UID, - "PrivateInformation" => tags::PRIVATE_INFORMATION, - _ => return Ok(None), - }; - self.attr_opt(tag) - .map_err(|_| crate::NoSuchAttributeNameSnafu { name }.build()) - } - - fn at( - &self, - selector: impl Into, - ) -> Result, crate::AtAccessError> { - let selector: AttributeSelector = selector.into(); - match selector.split_first() { - (AttributeSelectorStep::Tag(tag), None) => self - .attr(tag) - .map_err(|_| AtAccessError::MissingLeafElement { selector }), - (_, Some(_)) => crate::NotASequenceSnafu { - selector, - step_index: 0_u32, - } - .fail(), - (_, None) => unreachable!("broken invariant: nested step at end of selector"), - } - } -} - -impl ApplyOp for FileMetaTable { - type Err = ApplyError; - - /// Apply the given attribute operation on this file meta information table. - /// - /// See the [`dicom_core::ops`] module - /// for more information. - fn apply(&mut self, op: AttributeOp) -> ApplyResult { - self.apply(op) - } -} - -/// A builder for DICOM meta information tables. -#[derive(Debug, Default, Clone)] -pub struct FileMetaTableBuilder { - /// File Meta Information Group Length (UL) - information_group_length: Option, - /// File Meta Information Version (OB) - information_version: Option<[u8; 2]>, - /// Media Storage SOP Class UID (UI) - media_storage_sop_class_uid: Option, - /// Media Storage SOP Instance UID (UI) - media_storage_sop_instance_uid: Option, - /// Transfer Syntax UID (UI) - transfer_syntax: Option, - /// Implementation Class UID (UI) - implementation_class_uid: Option, - - /// Implementation Version Name (SH) - implementation_version_name: Option, - /// Source Application Entity Title (AE) - source_application_entity_title: Option, - /// Sending Application Entity Title (AE) - sending_application_entity_title: Option, - /// Receiving Application Entity Title (AE) - receiving_application_entity_title: Option, - /// Private Information Creator UID (UI) - private_information_creator_uid: Option, - /// Private Information (OB) - private_information: Option>, -} - -/// Ensure that the string is even lengthed, by adding a trailing character -/// if not. -#[inline] -fn padded(s: T, pad: char) -> String -where - T: Into, -{ - let mut s = s.into(); - if s.len() % 2 == 1 { - s.push(pad); - } - s -} - -/// Ensure that the string is even lengthed with trailing '\0's. -fn ui_padded(s: T) -> String -where - T: Into, -{ - padded(s, '\0') -} - -/// Ensure that the string is even lengthed with trailing spaces. -fn txt_padded(s: T) -> String -where - T: Into, -{ - padded(s, ' ') -} - -impl FileMetaTableBuilder { - /// Create a new, empty builder. - pub fn new() -> FileMetaTableBuilder { - FileMetaTableBuilder::default() - } - - /// Define the meta information group length. - pub fn group_length(mut self, value: u32) -> FileMetaTableBuilder { - self.information_group_length = Some(value); - self - } - - /// Define the meta information version. - pub fn information_version(mut self, value: [u8; 2]) -> FileMetaTableBuilder { - self.information_version = Some(value); - self - } - - /// Define the media storage SOP class UID. - pub fn media_storage_sop_class_uid(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.media_storage_sop_class_uid = Some(ui_padded(value)); - self - } - - /// Define the media storage SOP instance UID. - pub fn media_storage_sop_instance_uid(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.media_storage_sop_instance_uid = Some(ui_padded(value)); - self - } - - /// Define the transfer syntax UID. - pub fn transfer_syntax(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.transfer_syntax = Some(ui_padded(value)); - self - } - - /// Define the implementation class UID. - pub fn implementation_class_uid(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.implementation_class_uid = Some(ui_padded(value)); - self - } - - /// Define the implementation version name. - pub fn implementation_version_name(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.implementation_version_name = Some(txt_padded(value)); - self - } - - /// Define the source application entity title. - pub fn source_application_entity_title(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.source_application_entity_title = Some(txt_padded(value)); - self - } - - /// Define the sending application entity title. - pub fn sending_application_entity_title(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.sending_application_entity_title = Some(txt_padded(value)); - self - } - - /// Define the receiving application entity title. - pub fn receiving_application_entity_title(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.receiving_application_entity_title = Some(txt_padded(value)); - self - } - - /// Define the private information creator UID. - pub fn private_information_creator_uid(mut self, value: T) -> FileMetaTableBuilder - where - T: Into, - { - self.private_information_creator_uid = Some(ui_padded(value)); - self - } - - /// Define the private information as a vector of bytes. - pub fn private_information(mut self, value: T) -> FileMetaTableBuilder - where - T: Into>, - { - self.private_information = Some(value.into()); - self - } - - /// Build the table. - pub fn build(self) -> Result { - let information_version = self.information_version.unwrap_or( - // Missing information version, will assume (00H, 01H). See #28 - [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(|| { - // override implementation version name - implementation_version_name = Some(IMPLEMENTATION_VERSION_NAME.to_string()); - - IMPLEMENTATION_CLASS_UID.to_string() - }); - - let mut table = FileMetaTable { - // placeholder value which will be replaced on update - 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>(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: &[u8] = &[ - // magic code - b'D', b'I', b'C', b'M', - // File Meta Information Group Length: (0000,0002) ; UL ; 4 ; 200 - 0x02, 0x00, 0x00, 0x00, b'U', b'L', 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, - // File Meta Information Version: (0002, 0001) ; OB ; 2 ; [0x00, 0x01] - 0x02, 0x00, 0x01, 0x00, b'O', b'B', 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, - // Media Storage SOP Class UID (0002, 0002) ; UI ; 26 ; "1.2.840.10008.5.1.4.1.1.1\0" (ComputedRadiographyImageStorage) - 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, - // Media Storage SOP Instance UID (0002, 0003) ; UI ; 56 ; "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0" - 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, - // Transfer Syntax UID (0002, 0010) ; UI ; 20 ; "1.2.840.10008.1.2.1\0" (LittleEndianExplicit) - 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, - // Implementation Class UID (0002, 0012) ; UI ; 20 ; "1.2.345.6.7890.1.234" - 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, - // optional elements: - - // Implementation Version Name (0002,0013) ; SH ; "RUSTY_DICOM_269" - 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, - // Source Application Entity Title (0002, 0016) ; AE ; 0 (no data) - 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); - } - - /// Build a file meta table with the minimum set of parameters. - #[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); - } - - /// Changing the transfer syntax updates the file meta group length. - #[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![ - // Information Group Length - DataElement::new(Tag(0x0002, 0x0000), VR::UL, dicom_value!(U32, 200)), - // Information Version - DataElement::new(Tag(0x0002, 0x0001), VR::OB, dicom_value!(U8, [0, 1])), - // Media Storage SOP Class UID - DataElement::new( - Tag(0x0002, 0x0002), - VR::UI, - Value::Primitive("1.2.840.10008.5.1.4.1.1.1\0".into()), - ), - // Media Storage SOP Instance UID - DataElement::new( - Tag(0x0002, 0x0003), - VR::UI, - Value::Primitive( - "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".into(), - ), - ), - // Transfer Syntax - DataElement::new( - Tag(0x0002, 0x0010), - VR::UI, - Value::Primitive("1.2.840.10008.1.2.1\0".into()), - ), - // Implementation Class UID - DataElement::new( - Tag(0x0002, 0x0012), - VR::UI, - Value::Primitive("1.2.345.6.7890.1.234".into()), - ), - // Implementation Version Name - DataElement::new( - Tag(0x0002, 0x0013), - VR::SH, - Value::Primitive("RUSTY_DICOM_269 ".into()), - ), - // Source Application Entity Title - 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, // dummy value - 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, - }; - - // replace does not set missing attributes - table - .apply(AttributeOp::new( - tags::IMPLEMENTATION_VERSION_NAME, - AttributeAction::ReplaceStr("MY_DICOM_1.1".into()), - )) - .unwrap(); - - assert_eq!(table.implementation_version_name, None); - - // but SetStr does - 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"), - ); - - // Set (primitive) also works - 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"), - ); - - // set if missing works only if value isn't set yet - 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"), - ); - - // replacing mandatory field - 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", - ); - } - - /// writing file meta information and reading it back - /// should not fail and the the group length should be the same - #[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 - ); - } - - /// Can access file meta properties via the DicomObject trait - #[test] - fn dicom_object_api() { - use crate::{DicomAttribute as _, DicomObject as _}; - use dicom_dictionary_std::uids; - - let meta = FileMetaTableBuilder::new() - .transfer_syntax(uids::RLE_LOSSLESS) - .media_storage_sop_class_uid(uids::ENHANCED_MR_IMAGE_STORAGE) - .media_storage_sop_instance_uid("2.25.94766187067244888884745908966163363746") - .implementation_version_name("RUSTY_DICOM_269") - .build() - .unwrap(); - - assert_eq!( - meta.attr(tags::TRANSFER_SYNTAX_UID) - .unwrap() - .to_str() - .unwrap(), - uids::RLE_LOSSLESS - ); - - let sop_class_uid = meta.attr_opt(tags::MEDIA_STORAGE_SOP_CLASS_UID).unwrap(); - let sop_class_uid = sop_class_uid.as_ref().map(|v| v.to_str().unwrap()); - assert_eq!( - sop_class_uid.as_deref(), - Some(uids::ENHANCED_MR_IMAGE_STORAGE) - ); - - assert_eq!( - meta.attr_by_name("MediaStorageSOPInstanceUID") - .unwrap() - .to_str() - .unwrap(), - "2.25.94766187067244888884745908966163363746" - ); - - assert!(meta.attr_opt(tags::PRIVATE_INFORMATION).unwrap().is_none()); - } -} From 610af285d66a591e91793ac631c5473751630873 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 12:54:45 +0200 Subject: [PATCH 02/12] Add now methods to DicomDate and DicomTime --- core/Cargo.toml | 3 +++ core/src/value/partial.rs | 16 +++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 3b5a672e..2b365852 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,3 +18,6 @@ num-traits = "0.2.12" safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.8" + +[features] +now = [] diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index bfdf355b..e0ea86f0 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1,7 +1,9 @@ //! Handling of partial precision of Date, Time and DateTime values. use crate::value::AsRange; -use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; +use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +#[cfg(feature = "now")] +use chrono::{Local, Utc}; use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; use std::fmt; @@ -308,12 +310,14 @@ impl DicomDate { } // Constructs a new `DicomDate` now from the local timezone + #[cfg(feature = "now")] pub fn now_local() -> Result { - return DicomDate::try_from(&Local::now()); + return DicomDate::try_from(&Local::now().date_naive()); } // Constructs a new `DicomDate` now from the utc timezone + #[cfg(feature = "now")] pub fn now_utc() -> Result { - return DicomDate::try_from(&Utc::now()); + return DicomDate::try_from(&Utc::now().date_naive()); } /// Retrieves the year from a date as a reference @@ -461,12 +465,14 @@ impl DicomTime { } // Constructs a new `DicomDate` now from the local timezone + #[cfg(feature = "now")] pub fn now_local() -> Result { - return DicomTime::try_from(&Local::now()); + return DicomTime::try_from(&Local::now().naive_local().time()); } // Constructs a new `DicomDate` now from the utc timezone + #[cfg(feature = "now")] pub fn now_utc() -> Result { - return DicomTime::try_from(&Utc::now()); + return DicomTime::try_from(&Utc::now().naive_utc().time()); } /** Retrieves the hour from a time as a reference */ From c19fbada36aa585cdc1a1901c5f89388706f6f05 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 12:59:37 +0200 Subject: [PATCH 03/12] Clean files --- object/src/mem.rs | 4333 ++++++++++++++++++++++++++++++++++++++++++++ object/src/meta.rs | 1802 ++++++++++++++++++ 2 files changed, 6135 insertions(+) create mode 100644 object/src/mem.rs create mode 100644 object/src/meta.rs diff --git a/object/src/mem.rs b/object/src/mem.rs new file mode 100644 index 00000000..376f72b1 --- /dev/null +++ b/object/src/mem.rs @@ -0,0 +1,4333 @@ +//! This module contains the implementation for an in-memory DICOM object. +//! +//! Use [`InMemDicomObject`] for your DICOM data set construction needs. +//! Values of this type support infallible insertion, removal, and retrieval +//! of elements by DICOM tag, +//! or name (keyword) with a data element dictionary look-up. +//! +//! If you wish to build a complete DICOM file, +//! you can start from an `InMemDicomObject` +//! and complement it with a [file meta group table](crate::meta) +//! (see [`with_meta`](InMemDicomObject::with_meta) +//! and [`with_exact_meta`](InMemDicomObject::with_exact_meta)). +//! +//! # Example +//! +//! A new DICOM data set can be built by providing a sequence of data elements. +//! Insertion and removal methods are also available. +//! +//! ``` +//! # use dicom_core::{DataElement, VR, dicom_value}; +//! # use dicom_dictionary_std::tags; +//! # use dicom_dictionary_std::uids; +//! # use dicom_object::InMemDicomObject; +//! let mut obj = InMemDicomObject::from_element_iter([ +//! DataElement::new(tags::SOP_CLASS_UID, VR::UI, uids::COMPUTED_RADIOGRAPHY_IMAGE_STORAGE), +//! DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.60156688944589400766024286894543900794"), +//! // ... +//! ]); +//! +//! // continue adding elements +//! obj.put(DataElement::new(tags::MODALITY, VR::CS, "CR")); +//! ``` +//! +//! In-memory DICOM objects may have a byte length recorded, +//! if it was part of a data set sequence with explicit length. +//! If necessary, this number can be obtained via the [`HasLength`] trait. +//! However, any modifications made to the object will reset this length +//! to [_undefined_](dicom_core::Length::UNDEFINED). +use dicom_core::ops::{ + ApplyOp, AttributeAction, AttributeOp, AttributeSelector, AttributeSelectorStep, +}; +use dicom_encoding::Codec; +use dicom_parser::dataset::read::{DataSetReaderOptions, OddLengthStrategy}; +use dicom_parser::dataset::write::DataSetWriterOptions; +use dicom_parser::stateful::decode::CharacterSetOverride; +use itertools::Itertools; +use smallvec::SmallVec; +use snafu::{ensure, OptionExt, ResultExt}; +use std::borrow::Cow; +use std::fs::File; +use std::io::{BufRead, BufReader, Read}; +use std::path::Path; +use std::{collections::BTreeMap, io::Write}; + +use crate::file::ReadPreamble; +use crate::ops::{ + ApplyError, ApplyResult, IncompatibleTypesSnafu, ModifySnafu, UnsupportedActionSnafu, +}; +use crate::{meta::FileMetaTable, FileMetaTableBuilder}; +use crate::{ + AccessByNameError, AccessError, AtAccessError, BuildMetaTableSnafu, CreateParserSnafu, + CreatePrinterSnafu, DicomObject, ElementNotFoundSnafu, FileDicomObject, InvalidGroupSnafu, + MissingElementValueSnafu, MissingLeafElementSnafu, NoSpaceSnafu, NoSuchAttributeNameSnafu, + NoSuchDataElementAliasSnafu, NoSuchDataElementTagSnafu, NotASequenceSnafu, OpenFileSnafu, + ParseMetaDataSetSnafu, ParseSopAttributeSnafu, PrematureEndSnafu, PrepareMetaTableSnafu, + PrintDataSetSnafu, PrivateCreatorNotFoundSnafu, PrivateElementError, ReadError, ReadFileSnafu, + ReadPreambleBytesSnafu, ReadTokenSnafu, ReadUnrecognizedTransferSyntaxSnafu, + ReadUnsupportedTransferSyntaxSnafu, ReadUnsupportedTransferSyntaxWithSuggestionSnafu, + UnexpectedTokenSnafu, WithMetaError, WriteError, +}; +use dicom_core::dictionary::{DataDictionary, DataDictionaryEntry}; +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, uids, StandardDataDictionary}; +use dicom_encoding::transfer_syntax::TransferSyntaxIndex; +use dicom_encoding::{encode::EncodeTo, text::SpecificCharacterSet, TransferSyntax}; +use dicom_parser::dataset::{DataSetReader, DataToken, IntoTokensOptions}; +use dicom_parser::{ + dataset::{read::Error as ParserError, DataSetWriter, IntoTokens}, + StatefulDecode, +}; +use dicom_transfer_syntax_registry::TransferSyntaxRegistry; + +/// A full in-memory DICOM data element. +pub type InMemElement = DataElement, InMemFragment>; + +/// The type of a pixel data fragment. +pub type InMemFragment = dicom_core::value::InMemFragment; + +type Result = std::result::Result; + +type ParserResult = std::result::Result; + +/// A DICOM object that is fully contained in memory. +/// +/// See the [module-level documentation](self) +/// for more details. +#[derive(Debug, Clone)] +pub struct InMemDicomObject { + /// the element map + entries: BTreeMap>, + /// the data dictionary + dict: D, + /// The length of the DICOM object in bytes. + /// It is usually undefined, unless it is part of an item + /// in a sequence with a specified length in its item header. + len: Length, + /// In case the SpecificCharSet changes we need to mark the object as dirty, + /// because changing the character set may change the length in bytes of + /// stored text. It has to be public for now because we need + pub(crate) charset_changed: bool, +} + +impl PartialEq for InMemDicomObject { + // This implementation ignores the data dictionary. + fn eq(&self, other: &Self) -> bool { + self.entries == other.entries + } +} + +impl HasLength for InMemDicomObject { + fn length(&self) -> Length { + self.len + } +} + +impl HasLength for &InMemDicomObject { + fn length(&self) -> Length { + self.len + } +} + +impl DicomObject for InMemDicomObject +where + D: DataDictionary, + D: Clone, +{ + type Attribute<'a> = &'a Value, InMemFragment> + where Self: 'a; + + type LeafAttribute<'a> = &'a Value, InMemFragment> + where Self: 'a; + + #[inline] + fn attr_opt(&self, tag: Tag) -> Result>> { + let elem = InMemDicomObject::element_opt(self, tag)?; + Ok(elem.map(|e| e.value())) + } + + #[inline] + fn attr_by_name_opt( + &self, + name: &str, + ) -> Result>, AccessByNameError> { + let elem = InMemDicomObject::element_by_name_opt(self, name)?; + Ok(elem.map(|e| e.value())) + } + + #[inline] + fn attr(&self, tag: Tag) -> Result> { + let elem = InMemDicomObject::element(self, tag)?; + Ok(elem.value()) + } + + #[inline] + fn attr_by_name(&self, name: &str) -> Result, AccessByNameError> { + let elem = InMemDicomObject::element_by_name(self, name)?; + Ok(elem.value()) + } + + #[inline] + fn at(&self, selector: impl Into) -> Result, AtAccessError> { + self.value_at(selector) + } +} + +impl<'s, D: 's> DicomObject for &'s InMemDicomObject +where + D: DataDictionary, + D: Clone, +{ + type Attribute<'a> = &'a Value, InMemFragment> + where Self: 'a, + 's: 'a; + + type LeafAttribute<'a> = &'a Value, InMemFragment> + where Self: 'a, + 's: 'a; + + #[inline] + fn attr_opt(&self, tag: Tag) -> Result>> { + let elem = InMemDicomObject::element_opt(*self, tag)?; + Ok(elem.map(|e| e.value())) + } + + #[inline] + fn attr_by_name_opt( + &self, + name: &str, + ) -> Result>, AccessByNameError> { + let elem = InMemDicomObject::element_by_name_opt(*self, name)?; + Ok(elem.map(|e| e.value())) + } + + #[inline] + fn attr(&self, tag: Tag) -> Result> { + let elem = InMemDicomObject::element(*self, tag)?; + Ok(elem.value()) + } + + #[inline] + fn attr_by_name(&self, name: &str) -> Result, AccessByNameError> { + let elem = InMemDicomObject::element_by_name(*self, name)?; + Ok(elem.value()) + } + + #[inline] + fn at(&self, selector: impl Into) -> Result, AtAccessError> { + self.value_at(selector) + } +} + +impl FileDicomObject> { + /// Create a DICOM object by reading from a file. + /// + /// This function assumes the standard file encoding structure: + /// first it automatically detects whether the 128-byte preamble is present, + /// skipping it if found. + /// Then it reads the file meta group, + /// followed by the rest of the data set. + pub fn open_file>(path: P) -> Result { + Self::open_file_with_dict(path, StandardDataDictionary) + } + + /// Create a DICOM object by reading from a byte source. + /// + /// This function assumes the standard file encoding structure: + /// first it automatically detects whether the 128-byte preamble is present, + /// skipping it if found. + /// Then it reads the file meta group, + /// followed by the rest of the data set. + pub fn from_reader(src: S) -> Result + where + S: Read, + { + Self::from_reader_with_dict(src, StandardDataDictionary) + } +} + +impl InMemDicomObject { + /// Create a new empty DICOM object. + pub fn new_empty() -> Self { + InMemDicomObject { + entries: BTreeMap::new(), + dict: StandardDataDictionary, + len: Length::UNDEFINED, + charset_changed: false, + } + } + + /// Construct a DICOM object from a fallible source of structured elements. + #[inline] + pub fn from_element_source(iter: I) -> Result + where + I: IntoIterator>>, + { + Self::from_element_source_with_dict(iter, StandardDataDictionary) + } + + /// Construct a DICOM object from a non-fallible source of structured elements. + #[inline] + pub fn from_element_iter(iter: I) -> Self + where + I: IntoIterator>, + { + Self::from_iter_with_dict(iter, StandardDataDictionary) + } + + /// Construct a DICOM object representing a command set, + /// from a non-fallible iterator of structured elements. + /// + /// This method will automatically insert + /// a _Command Group Length_ (0000,0000) element + /// based on the command elements found in the sequence. + #[inline] + pub fn command_from_element_iter(iter: I) -> Self + where + I: IntoIterator>, + { + Self::command_from_iter_with_dict(iter, StandardDataDictionary) + } + + /// Read an object from a source using the given decoder. + /// + /// Note: [`read_dataset_with_ts`] and [`read_dataset_with_ts_cs`] + /// may be easier to use. + /// + /// [`read_dataset_with_ts`]: InMemDicomObject::read_dataset_with_ts + /// [`read_dataset_with_ts_cs`]: InMemDicomObject::read_dataset_with_ts_cs + #[inline] + pub fn read_dataset(decoder: S) -> Result + where + S: StatefulDecode, + { + Self::read_dataset_with_dict(decoder, StandardDataDictionary) + } + + /// Read an object from a source, + /// using the given transfer syntax and default character set. + /// + /// If the attribute _Specific Character Set_ is found in the encoded data, + /// this will override the given character set. + #[inline] + pub fn read_dataset_with_ts_cs( + from: S, + ts: &TransferSyntax, + cs: SpecificCharacterSet, + ) -> Result + where + S: Read, + { + Self::read_dataset_with_dict_ts_cs(from, StandardDataDictionary, ts, cs) + } + + /// Read an object from a source, + /// using the given transfer syntax. + /// + /// The default character set is assumed + /// until _Specific Character Set_ is found in the encoded data, + /// after which the text decoder will be overridden accordingly. + #[inline] + pub fn read_dataset_with_ts(from: S, ts: &TransferSyntax) -> Result + where + S: Read, + { + Self::read_dataset_with_dict_ts_cs( + from, + StandardDataDictionary, + ts, + SpecificCharacterSet::default(), + ) + } +} + +impl FileDicomObject> +where + D: DataDictionary, + D: Clone, +{ + /// Create a new empty object, using the given dictionary and + /// file meta table. + pub fn new_empty_with_dict_and_meta(dict: D, meta: FileMetaTable) -> Self { + FileDicomObject { + meta, + obj: InMemDicomObject { + entries: BTreeMap::new(), + dict, + len: Length::UNDEFINED, + charset_changed: false, + }, + } + } + + /// Create a DICOM object by reading from a file. + /// + /// This function assumes the standard file encoding structure: + /// first it automatically detects whether the 128-byte preamble is present, + /// skipping it when found. + /// Then it reads the file meta group, + /// followed by the rest of the data set. + pub fn open_file_with_dict>(path: P, dict: D) -> Result { + Self::open_file_with(path, dict, TransferSyntaxRegistry) + } + + /// Create a DICOM object by reading from a file. + /// + /// This function assumes the standard file encoding structure: + /// first it automatically detects whether the 128-byte preamble is present, + /// skipping it when found. + /// Then it reads the file meta group, + /// followed by the rest of the data set. + /// + /// This function allows you to choose a different transfer syntax index, + /// but its use is only advised when the built-in transfer syntax registry + /// is insufficient. Otherwise, please use [`open_file_with_dict`] instead. + /// + /// [`open_file_with_dict`]: #method.open_file_with_dict + pub fn open_file_with(path: P, dict: D, ts_index: R) -> Result + where + P: AsRef, + R: TransferSyntaxIndex, + { + Self::open_file_with_all_options( + path, + dict, + ts_index, + None, + ReadPreamble::Auto, + Default::default(), + Default::default(), + ) + } + + pub(crate) fn open_file_with_all_options( + path: P, + dict: D, + ts_index: R, + read_until: Option, + mut read_preamble: ReadPreamble, + odd_length: OddLengthStrategy, + charset_override: CharacterSetOverride, + ) -> Result + where + P: AsRef, + R: TransferSyntaxIndex, + { + let path = path.as_ref(); + let mut file = + BufReader::new(File::open(path).with_context(|_| OpenFileSnafu { filename: path })?); + + if read_preamble == ReadPreamble::Auto { + read_preamble = Self::detect_preamble(&mut file) + .with_context(|_| ReadFileSnafu { filename: path })?; + } + + if read_preamble == ReadPreamble::Auto || read_preamble == ReadPreamble::Always { + let mut buf = [0u8; 128]; + // skip the preamble + file.read_exact(&mut buf) + .with_context(|_| ReadFileSnafu { filename: path })?; + } + + Self::read_parts_with_all_options_impl( + file, + dict, + ts_index, + read_until, + odd_length, + charset_override, + ) + } + + /// Create a DICOM object by reading from a byte source. + /// + /// This function assumes the standard file encoding structure: + /// first it automatically detects whether the 128-byte preamble is present, + /// skipping it when found. + /// Then it reads the file meta group, + /// followed by the rest of the data set. + pub fn from_reader_with_dict(src: S, dict: D) -> Result + where + S: Read, + { + Self::from_reader_with(src, dict, TransferSyntaxRegistry) + } + + /// Create a DICOM object by reading from a byte source. + /// + /// This function assumes the standard file encoding structure: + /// first it automatically detects whether the preamble is present, + /// skipping it when found. + /// Then it reads the file meta group, + /// followed by the rest of the data set. + /// + /// This function allows you to choose a different transfer syntax index, + /// but its use is only advised when the built-in transfer syntax registry + /// is insufficient. Otherwise, please use [`from_reader_with_dict`] instead. + /// + /// [`from_reader_with_dict`]: #method.from_reader_with_dict + pub fn from_reader_with(src: S, dict: D, ts_index: R) -> Result + where + S: Read, + R: TransferSyntaxIndex, + { + Self::from_reader_with_all_options( + src, + dict, + ts_index, + None, + ReadPreamble::Auto, + Default::default(), + Default::default(), + ) + } + + pub(crate) fn from_reader_with_all_options( + src: S, + dict: D, + ts_index: R, + read_until: Option, + mut read_preamble: ReadPreamble, + odd_length: OddLengthStrategy, + charset_override: CharacterSetOverride, + ) -> Result + where + S: Read, + R: TransferSyntaxIndex, + { + let mut file = BufReader::new(src); + + if read_preamble == ReadPreamble::Auto { + read_preamble = Self::detect_preamble(&mut file).context(ReadPreambleBytesSnafu)?; + } + + if read_preamble == ReadPreamble::Always { + // skip preamble + let mut buf = [0u8; 128]; + // skip the preamble + file.read_exact(&mut buf).context(ReadPreambleBytesSnafu)?; + } + + Self::read_parts_with_all_options_impl( + file, + dict, + ts_index, + read_until, + odd_length, + charset_override, + ) + } + + // detect the presence of a preamble + // and provide a better `ReadPreamble` option accordingly + fn detect_preamble(reader: &mut BufReader) -> std::io::Result + where + S: Read, + { + let buf = reader.fill_buf()?; + let buflen = buf.len(); + + if buflen < 4 { + return Err(std::io::ErrorKind::UnexpectedEof.into()); + } + + if buflen >= 132 && &buf[128..132] == b"DICM" { + return Ok(ReadPreamble::Always); + } + + if &buf[0..4] == b"DICM" { + return Ok(ReadPreamble::Never); + } + + // could not detect + Ok(ReadPreamble::Auto) + } + + /// Common implementation for reading the file meta group + /// and the main data set (expects no preamble and no magic code), + /// according to the file's transfer syntax and the given options. + /// + /// If Media Storage SOP Class UID or Media Storage SOP Instance UID + /// are missing in the file meta group, + /// this function will attempt to populate them from the main data set. + fn read_parts_with_all_options_impl( + mut src: BufReader, + dict: D, + ts_index: R, + read_until: Option, + odd_length: OddLengthStrategy, + charset_override: CharacterSetOverride, + ) -> Result + where + S: Read, + R: TransferSyntaxIndex, + { + // read metadata header + let mut meta = FileMetaTable::from_reader(&mut src).context(ParseMetaDataSetSnafu)?; + + let ts_uid = meta.transfer_syntax(); + // read rest of data according to metadata, feed it to object + if let Some(ts) = ts_index.get(ts_uid) { + let mut options = DataSetReaderOptions::default(); + options.odd_length = odd_length; + options.charset_override = charset_override; + + let obj = match ts.codec() { + Codec::Dataset(Some(adapter)) => { + let adapter = adapter.adapt_reader(Box::new(src)); + let mut dataset = DataSetReader::new_with_ts_options(adapter, ts, options) + .context(CreateParserSnafu)?; + InMemDicomObject::build_object( + &mut dataset, + dict, + false, + Length::UNDEFINED, + read_until, + )? + } + Codec::Dataset(None) => { + if ts_uid == uids::DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN + || ts_uid == uids::JPIP_REFERENCED_DEFLATE + || ts_uid == uids::JPIPHTJ2K_REFERENCED_DEFLATE + { + return ReadUnsupportedTransferSyntaxWithSuggestionSnafu { + uid: ts.uid(), + name: ts.name(), + feature_name: "dicom-transfer-syntax-registry/deflate", + } + .fail(); + } + + return ReadUnsupportedTransferSyntaxSnafu { + uid: ts.uid(), + name: ts.name(), + } + .fail(); + } + Codec::None | Codec::EncapsulatedPixelData(..) => { + let mut dataset = DataSetReader::new_with_ts_options(src, ts, options) + .context(CreateParserSnafu)?; + InMemDicomObject::build_object( + &mut dataset, + dict, + false, + Length::UNDEFINED, + read_until, + )? + } + }; + + // if Media Storage SOP Class UID is empty attempt to infer from SOP Class UID + if meta.media_storage_sop_class_uid().is_empty() { + if let Some(elem) = obj.get(tags::SOP_CLASS_UID) { + meta.media_storage_sop_class_uid = elem + .value() + .to_str() + .context(ParseSopAttributeSnafu)? + .to_string(); + } + } + + // if Media Storage SOP Instance UID is empty attempt to infer from SOP Instance UID + if meta.media_storage_sop_instance_uid().is_empty() { + if let Some(elem) = obj.get(tags::SOP_INSTANCE_UID) { + meta.media_storage_sop_instance_uid = elem + .value() + .to_str() + .context(ParseSopAttributeSnafu)? + .to_string(); + } + } + + Ok(FileDicomObject { meta, obj }) + } else { + ReadUnrecognizedTransferSyntaxSnafu { + uid: ts_uid.to_string(), + } + .fail() + } + } +} + +impl FileDicomObject> { + /// Create a new empty object, using the given file meta table. + pub fn new_empty_with_meta(meta: FileMetaTable) -> Self { + FileDicomObject { + meta, + obj: InMemDicomObject { + entries: BTreeMap::new(), + dict: StandardDataDictionary, + len: Length::UNDEFINED, + charset_changed: false, + }, + } + } +} + +impl InMemDicomObject +where + D: DataDictionary, + D: Clone, +{ + /// Create a new empty object, using the given dictionary for name lookup. + pub fn new_empty_with_dict(dict: D) -> Self { + InMemDicomObject { + entries: BTreeMap::new(), + dict, + len: Length::UNDEFINED, + charset_changed: false, + } + } + + /// Construct a DICOM object from an iterator of structured elements. + pub fn from_element_source_with_dict(iter: I, dict: D) -> Result + where + I: IntoIterator>>, + { + let entries: Result<_> = iter.into_iter().map_ok(|e| (e.tag(), e)).collect(); + Ok(InMemDicomObject { + entries: entries?, + dict, + len: Length::UNDEFINED, + charset_changed: false, + }) + } + + /// Construct a DICOM object from a non-fallible iterator of structured elements. + pub fn from_iter_with_dict(iter: I, dict: D) -> Self + where + I: IntoIterator>, + { + let entries = iter.into_iter().map(|e| (e.tag(), e)).collect(); + InMemDicomObject { + entries, + dict, + len: Length::UNDEFINED, + charset_changed: false, + } + } + + /// Construct a DICOM object representing a command set, + /// from a non-fallible iterator of structured elements. + /// + /// This method will automatically insert + /// a _Command Group Length_ (0000,0000) element + /// based on the command elements found in the sequence. + pub fn command_from_iter_with_dict(iter: I, dict: D) -> Self + where + I: IntoIterator>, + { + let mut calculated_length: u32 = 0; + let mut entries: BTreeMap<_, _> = iter + .into_iter() + .map(|e| { + // count the length of command set elements + if e.tag().0 == 0x0000 && e.tag().1 != 0x0000 { + let l = e.value().length(); + calculated_length += if l.is_defined() { even_len(l.0) } else { 0 } + 8; + } + + (e.tag(), e) + }) + .collect(); + + entries.insert( + Tag(0, 0), + InMemElement::new(Tag(0, 0), VR::UL, PrimitiveValue::from(calculated_length)), + ); + + InMemDicomObject { + entries, + dict, + len: Length::UNDEFINED, + charset_changed: false, + } + } + + /// Read an object from a source, + /// using the given decoder + /// and the given dictionary for name lookup. + pub fn read_dataset_with_dict(decoder: S, dict: D) -> Result + where + S: StatefulDecode, + D: DataDictionary, + { + let mut dataset = DataSetReader::new(decoder, Default::default()); + InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) + } + + /// Read an object from a source, + /// using the given data dictionary and transfer syntax. + #[inline] + pub fn read_dataset_with_dict_ts( + from: S, + dict: D, + ts: &TransferSyntax, + ) -> Result + where + S: Read, + D: DataDictionary, + { + Self::read_dataset_with_dict_ts_cs(from, dict, ts, SpecificCharacterSet::default()) + } + + /// Read an object from a source, + /// using the given data dictionary, + /// transfer syntax, + /// and the given character set to assume by default. + /// + /// If the attribute _Specific Character Set_ is found in the encoded data, + /// this will override the given character set. + pub fn read_dataset_with_dict_ts_cs( + from: S, + dict: D, + ts: &TransferSyntax, + cs: SpecificCharacterSet, + ) -> Result + where + S: Read, + D: DataDictionary, + { + let from = BufReader::new(from); + + match ts.codec() { + Codec::Dataset(Some(adapter)) => { + let adapter = adapter.adapt_reader(Box::new(from)); + let mut dataset = + DataSetReader::new_with_ts_cs(adapter, ts, cs).context(CreateParserSnafu)?; + InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) + } + Codec::Dataset(None) => { + let uid = ts.uid(); + if uid == uids::DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN + || uid == uids::JPIP_REFERENCED_DEFLATE + || uid == uids::JPIPHTJ2K_REFERENCED_DEFLATE + { + return ReadUnsupportedTransferSyntaxWithSuggestionSnafu { + uid, + name: ts.name(), + feature_name: "dicom-transfer-syntax-registry/deflate", + } + .fail(); + } + + ReadUnsupportedTransferSyntaxSnafu { + uid, + name: ts.name(), + } + .fail() + } + Codec::None | Codec::EncapsulatedPixelData(..) => { + let mut dataset = + DataSetReader::new_with_ts_cs(from, ts, cs).context(CreateParserSnafu)?; + InMemDicomObject::build_object(&mut dataset, dict, false, Length::UNDEFINED, None) + } + } + } + + // Standard methods follow. They are not placed as a trait implementation + // because they may require outputs to reference the lifetime of self, + // which is not possible without GATs. + + /// Retrieve a particular DICOM element by its tag. + /// + /// An error is returned if the element does not exist. + /// For an alternative to this behavior, + /// see [`element_opt`](InMemDicomObject::element_opt). + pub fn element(&self, tag: Tag) -> Result<&InMemElement> { + self.entries + .get(&tag) + .context(NoSuchDataElementTagSnafu { tag }) + } + + /// Retrieve a particular DICOM element by its name. + /// + /// This method translates the given attribute name into its tag + /// before retrieving the element. + /// If the attribute is known in advance, + /// using [`element`](InMemDicomObject::element) + /// with a tag constant is preferred. + /// + /// An error is returned if the element does not exist. + /// For an alternative to this behavior, + /// see [`element_by_name_opt`](InMemDicomObject::element_by_name_opt). + pub fn element_by_name(&self, name: &str) -> Result<&InMemElement, AccessByNameError> { + let tag = self.lookup_name(name)?; + self.entries + .get(&tag) + .with_context(|| NoSuchDataElementAliasSnafu { + tag, + alias: name.to_string(), + }) + } + + /// Retrieve a particular DICOM element that might not exist by its tag. + /// + /// If the element does not exist, + /// `None` is returned. + pub fn element_opt(&self, tag: Tag) -> Result>, AccessError> { + match self.element(tag) { + Ok(e) => Ok(Some(e)), + Err(super::AccessError::NoSuchDataElementTag { .. }) => Ok(None), + } + } + + /// Get a particular DICOM attribute from this object by tag. + /// + /// If the element does not exist, + /// `None` is returned. + pub fn get(&self, tag: Tag) -> Option<&InMemElement> { + self.entries.get(&tag) + } + + // Get a mutable reference to a particular DICOM attribute from this object by tag. + // + // Should be private as it would allow a user to change the tag of an + // element and diverge from the dictionary + fn get_mut(&mut self, tag: Tag) -> Option<&mut InMemElement> { + self.entries.get_mut(&tag) + } + + /// Retrieve a particular DICOM element that might not exist by its name. + /// + /// If the element does not exist, + /// `None` is returned. + /// + /// This method translates the given attribute name into its tag + /// before retrieving the element. + /// If the attribute is known in advance, + /// using [`element_opt`](InMemDicomObject::element_opt) + /// with a tag constant is preferred. + pub fn element_by_name_opt( + &self, + name: &str, + ) -> Result>, AccessByNameError> { + match self.element_by_name(name) { + Ok(e) => Ok(Some(e)), + Err(AccessByNameError::NoSuchDataElementAlias { .. }) => Ok(None), + Err(e) => Err(e), + } + } + + 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/medical/dicom/2024a/output/chtml/part05/sect_7.8.html + if elem.header().vr() == VR::LO && elem.to_str().unwrap_or_default() == creator { + return Some(tag); + } + } + None + } + + /// Get a private element from the dataset using the group number, creator and element number. + /// + /// An error is raised when the group number is not odd, + /// the private creator is not found in the group, + /// or the private element is not found. + /// + /// For more info, see the [DICOM standard section on private elements][1]. + /// + /// [1]: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part05/sect_7.8.html + /// + /// ## Example + /// + /// ``` + /// # use dicom_core::{VR, PrimitiveValue, Tag, DataElement}; + /// # use dicom_object::{InMemDicomObject, PrivateElementError}; + /// # use std::error::Error; + /// let mut ds = InMemDicomObject::from_element_iter([ + /// DataElement::new( + /// Tag(0x0009, 0x0010), + /// VR::LO, + /// PrimitiveValue::from("CREATOR 1"), + /// ), + /// DataElement::new(Tag(0x0009, 0x01001), VR::DS, "1.0"), + /// ]); + /// assert_eq!( + /// ds.private_element(0x0009, "CREATOR 1", 0x01)? + /// .value() + /// .to_str()?, + /// "1.0" + /// ); + /// # Ok::<(), Box>(()) + /// ``` + pub fn private_element( + &self, + group: GroupNumber, + creator: &str, + element: u8, + ) -> Result<&InMemElement, 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 + /// element changes. + pub fn put(&mut self, elt: InMemElement) -> Option> { + self.put_element(elt) + } + + /// 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 + /// element changes. + pub fn put_element(&mut self, elt: InMemElement) -> Option> { + self.len = Length::UNDEFINED; + self.invalidate_if_charset_changed(elt.tag()); + self.entries.insert(elt.tag(), elt) + } + + /// Insert a private element into the dataset, replacing (and returning) any + /// previous element of the same attribute. + /// + /// This function will find the next available private element block in the given + /// group. If the creator already exists, the element will be added to the block + /// already reserved for that creator. If it does not exist, then a new block + /// will be reserved for the creator in the specified group. + /// An error is returned if there is no space left in the group. + /// + /// For more info, see the [DICOM standard section on private elements][1]. + /// + /// [1]: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part05/sect_7.8.html + /// + /// ## Example + /// ``` + /// # use dicom_core::{VR, PrimitiveValue, Tag, DataElement, header::Header}; + /// # use dicom_object::InMemDicomObject; + /// # use std::error::Error; + /// let mut ds = InMemDicomObject::new_empty(); + /// ds.put_private_element( + /// 0x0009, + /// "CREATOR 1", + /// 0x02, + /// VR::DS, + /// PrimitiveValue::from("1.0"), + /// )?; + /// assert_eq!( + /// ds.private_element(0x0009, "CREATOR 1", 0x02)? + /// .value() + /// .to_str()?, + /// "1.0" + /// ); + /// assert_eq!( + /// ds.private_element(0x0009, "CREATOR 1", 0x02)? + /// .header() + /// .tag(), + /// Tag(0x0009, 0x0102) + /// ); + /// # Ok::<(), Box>(()) + /// ``` + pub fn put_private_element( + &mut self, + group: GroupNumber, + creator: &str, + element: u8, + vr: VR, + value: PrimitiveValue, + ) -> Result>, 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); + Ok(self.put_element(DataElement::new(tag, vr, value))) + } else { + // Find last reserved block of tags. + let range = Tag(group, 0)..Tag(group, 0xFF); + let last_entry = self.entries.range(range).next_back(); + 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); + Ok(self.put_element(DataElement::new(tag, vr, value))) + } else { + NoSpaceSnafu { group }.fail() + } + } + } + + /// 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( + &mut self, + tag: Tag, + vr: VR, + string: impl Into, + ) -> Option> { + self.put_element(DataElement::new(tag, vr, string.into())) + } + + /// Remove a DICOM element by its tag, + /// reporting whether it was present. + pub fn remove_element(&mut self, tag: Tag) -> bool { + if self.entries.remove(&tag).is_some() { + self.len = Length::UNDEFINED; + true + } else { + false + } + } + + /// Remove a DICOM element by its keyword, + /// reporting whether it was present. + pub fn remove_element_by_name(&mut self, name: &str) -> Result { + let tag = self.lookup_name(name)?; + Ok(self.entries.remove(&tag).is_some()).map(|removed| { + if removed { + self.len = Length::UNDEFINED; + } + removed + }) + } + + /// Remove and return a particular DICOM element by its tag. + pub fn take_element(&mut self, tag: Tag) -> Result> { + self.entries + .remove(&tag) + .map(|e| { + self.len = Length::UNDEFINED; + e + }) + .context(NoSuchDataElementTagSnafu { tag }) + } + + /// Remove and return a particular DICOM element by its tag, + /// if it is present, + /// returns `None` otherwise. + pub fn take(&mut self, tag: Tag) -> Option> { + self.entries.remove(&tag).map(|e| { + self.len = Length::UNDEFINED; + e + }) + } + + /// Remove and return a particular DICOM element by its name. + pub fn take_element_by_name( + &mut self, + name: &str, + ) -> Result, AccessByNameError> { + let tag = self.lookup_name(name)?; + self.entries + .remove(&tag) + .map(|e| { + self.len = Length::UNDEFINED; + e + }) + .with_context(|| NoSuchDataElementAliasSnafu { + tag, + alias: name.to_string(), + }) + } + + /// Modify the object by + /// retaining only the DICOM data elements specified by the predicate. + /// + /// The elements are visited in ascending tag order, + /// and those for which `f(&element)` returns `false` are removed. + pub fn retain(&mut self, mut f: impl FnMut(&InMemElement) -> bool) { + self.entries.retain(|_, elem| f(elem)); + self.len = Length::UNDEFINED; + } + + /// Obtain a temporary mutable reference to a DICOM value by tag, + /// so that mutations can be applied within. + /// + /// If found, this method resets all related lengths recorded + /// and returns `true`. + /// Returns `false` otherwise. + /// + /// # Example + /// + /// ``` + /// # use dicom_core::{DataElement, VR, dicom_value}; + /// # use dicom_dictionary_std::tags; + /// # use dicom_object::InMemDicomObject; + /// let mut obj = InMemDicomObject::from_element_iter([ + /// DataElement::new(tags::LOSSY_IMAGE_COMPRESSION_RATIO, VR::DS, dicom_value!(Strs, ["25"])), + /// ]); + /// + /// // update lossy image compression ratio + /// obj.update_value(tags::LOSSY_IMAGE_COMPRESSION_RATIO, |e| { + /// e.primitive_mut().unwrap().extend_str(["2.56"]); + /// }); + /// + /// assert_eq!( + /// obj.get(tags::LOSSY_IMAGE_COMPRESSION_RATIO).unwrap().value().to_str().unwrap(), + /// "25\\2.56" + /// ); + /// ``` + pub fn update_value( + &mut self, + tag: Tag, + f: impl FnMut(&mut Value, InMemFragment>), + ) -> bool { + self.invalidate_if_charset_changed(tag); + if let Some(e) = self.entries.get_mut(&tag) { + e.update_value(f); + self.len = Length::UNDEFINED; + true + } else { + false + } + } + + /// Obtain a temporary mutable reference to a DICOM value by AttributeSelector, + /// so that mutations can be applied within. + /// + /// If found, this method resets all related lengths recorded + /// and returns `true`. + /// Returns `false` otherwise. + /// + /// See the documentation of [`AttributeSelector`] for more information + /// on how to write attribute selectors. + /// + /// Note: Consider using [`apply`](ApplyOp::apply) when possible. + /// + /// # Example + /// + /// ``` + /// # use dicom_core::{DataElement, VR, dicom_value, value::DataSetSequence}; + /// # use dicom_dictionary_std::tags; + /// # use dicom_object::InMemDicomObject; + /// # use dicom_core::ops::{AttributeAction, AttributeOp, ApplyOp}; + /// let mut dcm = InMemDicomObject::from_element_iter([ + /// DataElement::new( + /// tags::OTHER_PATIENT_I_DS_SEQUENCE, + /// VR::SQ, + /// DataSetSequence::from(vec![InMemDicomObject::from_element_iter([ + /// DataElement::new( + /// tags::PATIENT_ID, + /// VR::LO, + /// dicom_value!(Str, "1234") + /// )]) + /// ]) + /// ), + /// ]); + /// let selector = ( + /// tags::OTHER_PATIENT_I_DS_SEQUENCE, + /// 0, + /// tags::PATIENT_ID + /// ); + /// + /// // update referenced SOP instance UID for deidentification potentially + /// dcm.update_value_at(*&selector, |e| { + /// let mut v = e.primitive_mut().unwrap(); + /// *v = dicom_value!(Str, "abcd"); + /// }); + /// + /// assert_eq!( + /// dcm.entry_at(*&selector).unwrap().value().to_str().unwrap(), + /// "abcd" + /// ); + /// ``` + pub fn update_value_at( + &mut self, + selector: impl Into, + f: impl FnMut(&mut Value, InMemFragment>), + ) -> Result<(), AtAccessError> { + self.entry_at_mut(selector) + .map(|e| e.update_value(f)) + .map(|_| { + self.len = Length::UNDEFINED; + }) + } + + /// Obtain the DICOM value by finding the element + /// that matches the given selector. + /// + /// Returns an error if the respective element or any of its parents + /// cannot be found. + /// + /// See the documentation of [`AttributeSelector`] for more information + /// on how to write attribute selectors. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_core::prelude::*; + /// # use dicom_core::ops::AttributeSelector; + /// # use dicom_dictionary_std::tags; + /// # use dicom_object::InMemDicomObject; + /// # let obj: InMemDicomObject = unimplemented!(); + /// let referenced_sop_instance_iod = obj.value_at( + /// ( + /// tags::SHARED_FUNCTIONAL_GROUPS_SEQUENCE, + /// tags::REFERENCED_IMAGE_SEQUENCE, + /// tags::REFERENCED_SOP_INSTANCE_UID, + /// ))? + /// .to_str()?; + /// # Ok::<_, Box>(()) + /// ``` + pub fn value_at( + &self, + selector: impl Into, + ) -> Result<&Value, InMemFragment>, AtAccessError> { + let selector: AttributeSelector = selector.into(); + + let mut obj = self; + for (i, step) in selector.iter().enumerate() { + match step { + // reached the leaf + AttributeSelectorStep::Tag(tag) => { + return obj.get(*tag).map(|e| e.value()).with_context(|| { + MissingLeafElementSnafu { + selector: selector.clone(), + } + }); + } + // navigate further down + AttributeSelectorStep::Nested { tag, item } => { + let e = obj + .entries + .get(tag) + .with_context(|| crate::MissingSequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + + // get items + let items = e.items().with_context(|| NotASequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + + // if item.length == i and action is a constructive action, append new item + obj = + items + .get(*item as usize) + .with_context(|| crate::MissingSequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + } + } + } + + unreachable!() + } + + /// Change the 'specific_character_set' tag to ISO_IR 192, marking the dataset as UTF-8 + pub fn convert_to_utf8(&mut self) { + self.put(DataElement::new( + tags::SPECIFIC_CHARACTER_SET, + VR::CS, + "ISO_IR 192", + )); + } + + /// Get a DataElement by AttributeSelector + /// + /// If the element or other intermediate elements do not exist, the method will return an error. + /// + /// See the documentation of [`AttributeSelector`] for more information + /// on how to write attribute selectors. + /// + /// If you only need the value, use [`value_at`](Self::value_at). + pub fn entry_at( + &self, + selector: impl Into, + ) -> Result<&InMemElement, AtAccessError> { + let selector: AttributeSelector = selector.into(); + + let mut obj = self; + for (i, step) in selector.iter().enumerate() { + match step { + // reached the leaf + AttributeSelectorStep::Tag(tag) => { + return obj.get(*tag).with_context(|| MissingLeafElementSnafu { + selector: selector.clone(), + }) + } + // navigate further down + AttributeSelectorStep::Nested { tag, item } => { + let e = obj + .entries + .get(tag) + .with_context(|| crate::MissingSequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + + // get items + let items = e.items().with_context(|| NotASequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + + // if item.length == i and action is a constructive action, append new item + obj = + items + .get(*item as usize) + .with_context(|| crate::MissingSequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + } + } + } + + unreachable!() + } + + // Get a mutable reference to a particular entry by AttributeSelector + // + // Should be private for the same reason as `self.get_mut` + fn entry_at_mut( + &mut self, + selector: impl Into, + ) -> Result<&mut InMemElement, AtAccessError> { + let selector: AttributeSelector = selector.into(); + + let mut obj = self; + for (i, step) in selector.iter().enumerate() { + match step { + // reached the leaf + AttributeSelectorStep::Tag(tag) => { + return obj.get_mut(*tag).with_context(|| MissingLeafElementSnafu { + selector: selector.clone(), + }) + } + // navigate further down + AttributeSelectorStep::Nested { tag, item } => { + let e = + obj.entries + .get_mut(tag) + .with_context(|| crate::MissingSequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + + // get items + let items = e.items_mut().with_context(|| NotASequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + })?; + + // if item.length == i and action is a constructive action, append new item + obj = items.get_mut(*item as usize).with_context(|| { + crate::MissingSequenceSnafu { + selector: selector.clone(), + step_index: i as u32, + } + })?; + } + } + } + + unreachable!() + } + + /// Apply the given attribute operation on this object. + /// + /// For more complex updates, see [`update_value_at`]. + /// + /// See the [`dicom_core::ops`] module + /// for more information. + /// + /// # Examples + /// + /// ```rust + /// # use dicom_core::header::{DataElement, VR}; + /// # use dicom_core::value::PrimitiveValue; + /// # use dicom_dictionary_std::tags; + /// # use dicom_object::mem::*; + /// # use dicom_object::ops::ApplyResult; + /// use dicom_core::ops::{ApplyOp, AttributeAction, AttributeOp}; + /// # fn main() -> Result<(), Box> { + /// // given an in-memory DICOM object + /// let mut obj = InMemDicomObject::from_element_iter([ + /// DataElement::new( + /// tags::PATIENT_NAME, + /// VR::PN, + /// PrimitiveValue::from("Rosling^Hans") + /// ), + /// ]); + /// + /// // apply patient name change + /// obj.apply(AttributeOp::new( + /// tags::PATIENT_NAME, + /// AttributeAction::SetStr("Patient^Anonymous".into()), + /// ))?; + /// + /// assert_eq!( + /// obj.element(tags::PATIENT_NAME)?.to_str()?, + /// "Patient^Anonymous", + /// ); + /// # Ok(()) + /// # } + /// ``` + fn apply(&mut self, op: AttributeOp) -> ApplyResult { + let AttributeOp { selector, action } = op; + let dict = self.dict.clone(); + + let mut obj = self; + for (i, step) in selector.iter().enumerate() { + match step { + // reached the leaf + AttributeSelectorStep::Tag(tag) => return obj.apply_leaf(*tag, action), + // navigate further down + AttributeSelectorStep::Nested { tag, item } => { + if !obj.entries.contains_key(tag) { + // missing sequence, create it if action is constructive + if action.is_constructive() { + let vr = dict + .by_tag(*tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::UN); + + if vr != VR::SQ && vr != VR::UN { + return Err(ApplyError::NotASequence { + selector: selector.clone(), + step_index: i as u32, + }); + } + + obj.put(DataElement::new(*tag, vr, DataSetSequence::empty())); + } else { + return Err(ApplyError::MissingSequence { + selector: selector.clone(), + step_index: i as u32, + }); + } + }; + + // get items + let items = obj + .entries + .get_mut(tag) + .expect("sequence element should exist at this point") + .items_mut() + .ok_or_else(|| ApplyError::NotASequence { + selector: selector.clone(), + step_index: i as u32, + })?; + + // if item.length == i and action is a constructive action, append new item + obj = if items.len() == *item as usize && action.is_constructive() { + items.push(InMemDicomObject::new_empty_with_dict(dict.clone())); + items.last_mut().unwrap() + } else { + items.get_mut(*item as usize).ok_or_else(|| { + ApplyError::MissingSequence { + selector: selector.clone(), + step_index: i as u32, + } + })? + }; + } + } + } + unreachable!() + } + + fn apply_leaf(&mut self, tag: Tag, action: AttributeAction) -> ApplyResult { + self.invalidate_if_charset_changed(tag); + match action { + AttributeAction::Remove => { + self.remove_element(tag); + Ok(()) + } + AttributeAction::Empty => { + if let Some(e) = self.entries.get_mut(&tag) { + let vr = e.vr(); + // replace element + *e = DataElement::empty(tag, vr); + self.len = Length::UNDEFINED; + } + Ok(()) + } + AttributeAction::SetVr(new_vr) => { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + let e = DataElement::new(header.tag, new_vr, value); + self.put(e); + } else { + self.put(DataElement::empty(tag, new_vr)); + } + Ok(()) + } + AttributeAction::Set(new_value) => { + self.apply_change_value_impl(tag, new_value); + Ok(()) + } + AttributeAction::SetStr(string) => { + let new_value = PrimitiveValue::from(&*string); + self.apply_change_value_impl(tag, new_value); + Ok(()) + } + AttributeAction::SetIfMissing(new_value) => { + if self.get(tag).is_none() { + self.apply_change_value_impl(tag, new_value); + } + Ok(()) + } + AttributeAction::SetStrIfMissing(string) => { + if self.get(tag).is_none() { + let new_value = PrimitiveValue::from(&*string); + self.apply_change_value_impl(tag, new_value); + } + Ok(()) + } + AttributeAction::Replace(new_value) => { + if self.get(tag).is_some() { + self.apply_change_value_impl(tag, new_value); + } + Ok(()) + } + AttributeAction::ReplaceStr(string) => { + if self.get(tag).is_some() { + let new_value = PrimitiveValue::from(&*string); + self.apply_change_value_impl(tag, new_value); + } + Ok(()) + } + AttributeAction::PushStr(string) => self.apply_push_str_impl(tag, string), + AttributeAction::PushI32(integer) => self.apply_push_i32_impl(tag, integer), + AttributeAction::PushU32(integer) => self.apply_push_u32_impl(tag, integer), + AttributeAction::PushI16(integer) => self.apply_push_i16_impl(tag, integer), + AttributeAction::PushU16(integer) => self.apply_push_u16_impl(tag, integer), + AttributeAction::PushF32(number) => self.apply_push_f32_impl(tag, number), + AttributeAction::PushF64(number) => self.apply_push_f64_impl(tag, number), + AttributeAction::Truncate(limit) => { + self.update_value(tag, |value| value.truncate(limit)); + Ok(()) + } + _ => UnsupportedActionSnafu.fail(), + } + } + + fn apply_change_value_impl(&mut self, tag: Tag, new_value: PrimitiveValue) { + self.invalidate_if_charset_changed(tag); + + if let Some(e) = self.entries.get_mut(&tag) { + let vr = e.vr(); + // handle edge case: if VR is SQ and suggested value is empty, + // then create an empty data set sequence + let new_value = if vr == VR::SQ && new_value.is_empty() { + DataSetSequence::empty().into() + } else { + Value::from(new_value) + }; + *e = DataElement::new(tag, vr, new_value); + self.len = Length::UNDEFINED; + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::UN); + // insert element + + // handle edge case: if VR is SQ and suggested value is empty, + // then create an empty data set sequence + let new_value = if vr == VR::SQ && new_value.is_empty() { + DataSetSequence::empty().into() + } else { + Value::from(new_value) + }; + + self.put(DataElement::new(tag, vr, new_value)); + } + } + + fn invalidate_if_charset_changed(&mut self, tag: Tag) { + if tag == tags::SPECIFIC_CHARACTER_SET { + self.charset_changed = true; + } + } + + fn apply_push_str_impl(&mut self, tag: Tag, string: Cow<'static, str>) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + self.invalidate_if_charset_changed(tag); + // extend value + v.extend_str([string]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::UN); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(&*string))); + Ok(()) + } + } + + fn apply_push_i32_impl(&mut self, tag: Tag, integer: i32) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + // extend value + v.extend_i32([integer]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::SL); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); + Ok(()) + } + } + + fn apply_push_u32_impl(&mut self, tag: Tag, integer: u32) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + // extend value + v.extend_u32([integer]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::UL); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); + Ok(()) + } + } + + fn apply_push_i16_impl(&mut self, tag: Tag, integer: i16) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + // extend value + v.extend_i16([integer]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::SS); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); + Ok(()) + } + } + + fn apply_push_u16_impl(&mut self, tag: Tag, integer: u16) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + // extend value + v.extend_u16([integer]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::US); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(integer))); + Ok(()) + } + } + + fn apply_push_f32_impl(&mut self, tag: Tag, number: f32) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + // extend value + v.extend_f32([number]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::FL); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(number))); + Ok(()) + } + } + + fn apply_push_f64_impl(&mut self, tag: Tag, number: f64) -> ApplyResult { + if let Some(e) = self.entries.remove(&tag) { + let (header, value) = e.into_parts(); + match value { + Value::Primitive(mut v) => { + // extend value + v.extend_f64([number]).context(ModifySnafu)?; + // reinsert element + self.put(DataElement::new(tag, header.vr, v)); + Ok(()) + } + + Value::PixelSequence(..) => IncompatibleTypesSnafu { + kind: ValueType::PixelSequence, + } + .fail(), + Value::Sequence(..) => IncompatibleTypesSnafu { + kind: ValueType::DataSetSequence, + } + .fail(), + } + } else { + // infer VR from tag + let vr = dicom_dictionary_std::StandardDataDictionary + .by_tag(tag) + .and_then(|entry| entry.vr().exact()) + .unwrap_or(VR::FD); + // insert element + self.put(DataElement::new(tag, vr, PrimitiveValue::from(number))); + Ok(()) + } + } + + /// Write this object's data set into the given writer, + /// with the given encoder specifications, + /// without preamble, magic code, nor file meta group. + /// + /// The text encoding to use will be the default character set + /// until _Specific Character Set_ is found in the data set, + /// in which then that character set will be used. + /// + /// Uses the default [DataSetWriterOptions] for the writer. + /// + /// Note: [`write_dataset_with_ts`] and [`write_dataset_with_ts_cs`] + /// may be easier to use and _will_ apply a dataset adapter (such as + /// DeflatedExplicitVRLittleEndian (1.2.840.10008.1.2.99)) whereas this + /// method will _not_ + /// + /// [`write_dataset_with_ts`]: #method.write_dataset_with_ts + /// [`write_dataset_with_ts_cs`]: #method.write_dataset_with_ts_cs + pub fn write_dataset(&self, to: W, encoder: E) -> Result<(), WriteError> + where + W: Write, + E: EncodeTo, + { + // prepare data set writer + let mut dset_writer = DataSetWriter::new(to, encoder); + let required_options = IntoTokensOptions::new(self.charset_changed); + // write object + dset_writer + .write_sequence(self.into_tokens_with_options(required_options)) + .context(PrintDataSetSnafu)?; + + Ok(()) + } + + /// Write this object's data set into the given printer, + /// with the specified transfer syntax and character set, + /// without preamble, magic code, nor file meta group. + /// + /// The default [DataSetWriterOptions] is used for the writer. To change + /// that, use [`write_dataset_with_ts_cs_options`](Self::write_dataset_with_ts_cs_options). + /// + /// If the attribute _Specific Character Set_ is found in the data set, + /// the last parameter is overridden accordingly. + /// See also [`write_dataset_with_ts`](Self::write_dataset_with_ts). + pub fn write_dataset_with_ts_cs( + &self, + to: W, + ts: &TransferSyntax, + cs: SpecificCharacterSet, + ) -> Result<(), WriteError> + where + W: Write, + { + if let Codec::Dataset(Some(adapter)) = ts.codec() { + let adapter = adapter.adapt_writer(Box::new(to)); + // prepare data set writer + let mut dset_writer = + DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence(self.into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) + } else { + // prepare data set writer + let mut dset_writer = + DataSetWriter::with_ts_cs(to, ts, cs).context(CreatePrinterSnafu)?; + + // write object + dset_writer + .write_sequence(self.into_tokens()) + .context(PrintDataSetSnafu)?; + + Ok(()) + } + } + + /// Write this object's data set into the given printer, + /// with the specified transfer syntax and character set, + /// without preamble, magic code, nor file meta group. + /// + /// If the attribute _Specific Character Set_ is found in the data set, + /// the last parameter is overridden accordingly. + /// See also [`write_dataset_with_ts`](Self::write_dataset_with_ts). + pub fn write_dataset_with_ts_cs_options( + &self, + to: W, + ts: &TransferSyntax, + cs: SpecificCharacterSet, + options: DataSetWriterOptions, + ) -> Result<(), WriteError> + where + W: Write, + { + // prepare data set writer + let mut dset_writer = + DataSetWriter::with_ts_cs_options(to, ts, cs, options).context(CreatePrinterSnafu)?; + let required_options = IntoTokensOptions::new(self.charset_changed); + + // write object + dset_writer + .write_sequence(self.into_tokens_with_options(required_options)) + .context(PrintDataSetSnafu)?; + + Ok(()) + } + + /// Write this object's data set into the given writer, + /// with the specified transfer syntax, + /// without preamble, magic code, nor file meta group. + /// + /// The default [DataSetWriterOptions] is used for the writer. To change + /// that, use [`write_dataset_with_ts_options`](Self::write_dataset_with_ts_options). + /// + /// The default character set is assumed + /// until the _Specific Character Set_ is found in the data set, + /// after which the text encoder is overridden accordingly. + pub fn write_dataset_with_ts(&self, to: W, ts: &TransferSyntax) -> Result<(), WriteError> + where + W: Write, + { + self.write_dataset_with_ts_cs(to, ts, SpecificCharacterSet::default()) + } + + /// Write this object's data set into the given writer, + /// with the specified transfer syntax, + /// without preamble, magic code, nor file meta group. + /// + /// The default character set is assumed + /// until the _Specific Character Set_ is found in the data set, + /// after which the text encoder is overridden accordingly. + pub fn write_dataset_with_ts_options( + &self, + to: W, + ts: &TransferSyntax, + options: DataSetWriterOptions, + ) -> Result<(), WriteError> + where + W: Write, + { + self.write_dataset_with_ts_cs_options(to, ts, SpecificCharacterSet::default(), options) + } + + /// Encapsulate this object to contain a file meta group + /// as described exactly by the given table. + /// + /// **Note:** this method will not adjust the file meta group + /// to be semantically valid for the object. + /// Namely, the _Media Storage SOP Instance UID_ + /// and _Media Storage SOP Class UID_ + /// are not updated based on the receiving data set. + pub fn with_exact_meta(self, meta: FileMetaTable) -> FileDicomObject { + FileDicomObject { meta, obj: self } + } + + /// Encapsulate this object to contain a file meta group, + /// created through the given file meta table builder. + /// + /// A complete file meta group should provide + /// the _Transfer Syntax UID_, + /// the _Media Storage SOP Instance UID_, + /// and the _Media Storage SOP Class UID_. + /// The last two will be filled with the values of + /// _SOP Instance UID_ and _SOP Class UID_ + /// if they are present in this object. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_core::{DataElement, VR}; + /// # use dicom_dictionary_std::tags; + /// # use dicom_dictionary_std::uids; + /// use dicom_object::{InMemDicomObject, meta::FileMetaTableBuilder}; + /// + /// let obj = InMemDicomObject::from_element_iter([ + /// DataElement::new(tags::SOP_CLASS_UID, VR::UI, uids::COMPUTED_RADIOGRAPHY_IMAGE_STORAGE), + /// DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.60156688944589400766024286894543900794"), + /// // ... + /// ]); + /// + /// let obj = obj.with_meta(FileMetaTableBuilder::new() + /// .transfer_syntax(uids::EXPLICIT_VR_LITTLE_ENDIAN))?; + /// + /// // can now save everything to a file + /// let meta = obj.write_to_file("out.dcm")?; + /// # Result::<_, Box>::Ok(()) + /// ``` + pub fn with_meta( + self, + mut meta: FileMetaTableBuilder, + ) -> Result, WithMetaError> { + if let Some(elem) = self.get(tags::SOP_INSTANCE_UID) { + meta = meta.media_storage_sop_instance_uid( + elem.value().to_str().context(PrepareMetaTableSnafu)?, + ); + } + if let Some(elem) = self.get(tags::SOP_CLASS_UID) { + meta = meta + .media_storage_sop_class_uid(elem.value().to_str().context(PrepareMetaTableSnafu)?); + } + Ok(FileDicomObject { + meta: meta.build().context(BuildMetaTableSnafu)?, + obj: self, + }) + } + + /// Obtain an iterator over the elements of this object. + pub fn iter(&self) -> impl Iterator> + '_ { + self.into_iter() + } + + /// Obtain an iterator over the tags of the object's elements. + pub fn tags(&self) -> impl Iterator + '_ { + self.entries.keys().copied() + } + + // private methods + + /// Build an object by consuming a data set parser. + fn build_object( + dataset: &mut I, + dict: D, + in_item: bool, + len: Length, + read_until: Option, + ) -> Result + where + I: ?Sized + Iterator>, + { + let mut entries: BTreeMap> = BTreeMap::new(); + // perform a structured parsing of incoming tokens + while let Some(token) = dataset.next() { + let elem = match token.context(ReadTokenSnafu)? { + DataToken::PixelSequenceStart => { + // stop reading if reached `read_until` tag + if read_until + .map(|t| t <= Tag(0x7fe0, 0x0010)) + .unwrap_or(false) + { + break; + } + let value = InMemDicomObject::build_encapsulated_data(&mut *dataset)?; + DataElement::new(Tag(0x7fe0, 0x0010), VR::OB, value) + } + DataToken::ElementHeader(header) => { + // stop reading if reached `read_until` tag + if read_until.map(|t| t <= header.tag).unwrap_or(false) { + break; + } + + // fetch respective value, place it in the entries + let next_token = dataset.next().context(MissingElementValueSnafu)?; + match next_token.context(ReadTokenSnafu)? { + DataToken::PrimitiveValue(v) => InMemElement::new_with_len( + header.tag, + header.vr, + header.len, + Value::Primitive(v), + ), + token => { + return UnexpectedTokenSnafu { token }.fail(); + } + } + } + DataToken::SequenceStart { tag, len } => { + // stop reading if reached `read_until` tag + if read_until.map(|t| t <= tag).unwrap_or(false) { + break; + } + + // delegate sequence building to another function + let items = Self::build_sequence(tag, len, &mut *dataset, &dict)?; + DataElement::new_with_len( + tag, + VR::SQ, + len, + Value::Sequence(DataSetSequence::new(items, len)), + ) + } + DataToken::ItemEnd if in_item => { + // end of item, leave now + return Ok(InMemDicomObject { + entries, + dict, + len, + charset_changed: false, + }); + } + token => return UnexpectedTokenSnafu { token }.fail(), + }; + entries.insert(elem.tag(), elem); + } + + Ok(InMemDicomObject { + entries, + dict, + len, + charset_changed: false, + }) + } + + /// Build an encapsulated pixel data by collecting all fragments into an + /// in-memory DICOM value. + fn build_encapsulated_data( + dataset: I, + ) -> Result, InMemFragment>, ReadError> + where + I: Iterator>, + { + // continue fetching tokens to retrieve: + // - the offset table + // - the various compressed fragments + + let mut offset_table = None; + + let mut fragments = C::new(); + + for token in dataset { + match token.context(ReadTokenSnafu)? { + DataToken::OffsetTable(table) => { + offset_table = Some(table); + } + DataToken::ItemValue(data) => { + fragments.push(data); + } + DataToken::ItemEnd => { + // at the end of the first item ensure the presence of + // an empty offset_table here, so that the next items + // are seen as compressed fragments + if offset_table.is_none() { + offset_table = Some(Vec::new()) + } + } + DataToken::ItemStart { len: _ } => { /* no-op */ } + DataToken::SequenceEnd => { + // end of pixel data + break; + } + // the following variants are unexpected + token @ DataToken::ElementHeader(_) + | token @ DataToken::PixelSequenceStart + | token @ DataToken::SequenceStart { .. } + | token @ DataToken::PrimitiveValue(_) => { + return UnexpectedTokenSnafu { token }.fail(); + } + } + } + + Ok(Value::PixelSequence(PixelFragmentSequence::new( + offset_table.unwrap_or_default(), + fragments, + ))) + } + + /// Build a DICOM sequence by consuming a data set parser. + fn build_sequence( + _tag: Tag, + _len: Length, + dataset: &mut I, + dict: &D, + ) -> Result>, ReadError> + where + I: ?Sized + Iterator>, + { + let mut items: C<_> = SmallVec::new(); + while let Some(token) = dataset.next() { + match token.context(ReadTokenSnafu)? { + DataToken::ItemStart { len } => { + items.push(Self::build_object( + &mut *dataset, + dict.clone(), + true, + len, + None, + )?); + } + DataToken::SequenceEnd => { + return Ok(items); + } + token => return UnexpectedTokenSnafu { token }.fail(), + }; + } + + // iterator fully consumed without a sequence delimiter + PrematureEndSnafu.fail() + } + + fn lookup_name(&self, name: &str) -> Result { + self.dict + .by_name(name) + .context(NoSuchAttributeNameSnafu { name }) + .map(|e| e.tag()) + } +} + +impl ApplyOp for InMemDicomObject +where + D: DataDictionary, + D: Clone, +{ + type Err = ApplyError; + + #[inline] + fn apply(&mut self, op: AttributeOp) -> ApplyResult { + self.apply(op) + } +} + +impl<'a, D> IntoIterator for &'a InMemDicomObject { + type Item = &'a InMemElement; + type IntoIter = ::std::collections::btree_map::Values<'a, Tag, InMemElement>; + + fn into_iter(self) -> Self::IntoIter { + self.entries.values() + } +} + +impl IntoIterator for InMemDicomObject { + type Item = InMemElement; + type IntoIter = Iter; + + fn into_iter(self) -> Self::IntoIter { + Iter { + inner: self.entries.into_iter(), + } + } +} + +/// Base iterator type for an in-memory DICOM object. +#[derive(Debug)] +pub struct Iter { + inner: ::std::collections::btree_map::IntoIter>, +} + +impl Iterator for Iter { + type Item = InMemElement; + + fn next(&mut self) -> Option { + self.inner.next().map(|x| x.1) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize { + self.inner.count() + } +} + +impl Extend> for InMemDicomObject { + fn extend(&mut self, iter: I) + where + I: IntoIterator>, + { + self.len = Length::UNDEFINED; + self.entries.extend(iter.into_iter().map(|e| (e.tag(), e))) + } +} + +fn even_len(l: u32) -> u32 { + (l + 1) & !1 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{open_file, DicomAttribute as _}; + use byteordered::Endianness; + use dicom_core::chrono::FixedOffset; + use dicom_core::value::{DicomDate, DicomDateTime, DicomTime}; + use dicom_core::{dicom_value, header::DataElementHeader}; + use dicom_encoding::{ + decode::{basic::BasicDecoder, implicit_le::ImplicitVRLittleEndianDecoder}, + encode::{implicit_le::ImplicitVRLittleEndianEncoder, EncoderFor}, + }; + use dicom_parser::StatefulDecoder; + + fn assert_obj_eq(obj1: &InMemDicomObject, obj2: &InMemDicomObject) + where + D: std::fmt::Debug, + { + // debug representation because it makes a stricter comparison and + // assumes that Undefined lengths are equal. + assert_eq!(format!("{obj1:?}"), format!("{:?}", obj2)) + } + + #[test] + fn inmem_object_compare() { + let mut obj1 = InMemDicomObject::new_empty(); + let mut obj2 = InMemDicomObject::new_empty(); + assert_eq!(obj1, obj2); + let empty_patient_name = DataElement::empty(Tag(0x0010, 0x0010), VR::PN); + obj1.put(empty_patient_name.clone()); + assert_ne!(obj1, obj2); + obj2.put(empty_patient_name.clone()); + assert_obj_eq(&obj1, &obj2); + } + + #[test] + fn inmem_object_read_dataset() { + let data_in = [ + 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) + 0x08, 0x00, 0x00, 0x00, // Length: 8 + b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', + ]; + + let decoder = ImplicitVRLittleEndianDecoder::default(); + let text = SpecificCharacterSet::default(); + let mut cursor = &data_in[..]; + let parser = StatefulDecoder::new( + &mut cursor, + decoder, + BasicDecoder::new(Endianness::Little), + text, + ); + + let obj = InMemDicomObject::read_dataset(parser).unwrap(); + + let mut gt = InMemDicomObject::new_empty(); + + let patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + dicom_value!(Strs, ["Doe^John"]), + ); + gt.put(patient_name); + + assert_eq!(obj, gt); + } + + #[test] + fn inmem_object_read_dataset_with_ts_cs() { + let data_in = [ + 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) + 0x08, 0x00, 0x00, 0x00, // Length: 8 + b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', + ]; + + let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2").unwrap(); + let cs = SpecificCharacterSet::default(); + let mut cursor = &data_in[..]; + + let obj = InMemDicomObject::read_dataset_with_dict_ts_cs( + &mut cursor, + StandardDataDictionary, + ts, + cs, + ) + .unwrap(); + + let mut gt = InMemDicomObject::new_empty(); + + let patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + dicom_value!(Strs, ["Doe^John"]), + ); + gt.put(patient_name); + + assert_eq!(obj, gt); + } + + /// Reading a data set + /// saves the original length of a text element. + #[test] + fn inmem_object_read_dataset_saves_len() { + let data_in = [ + // SpecificCharacterSet (0008,0005) + 0x08, 0x00, 0x05, 0x00, // + // Length: 10 + 0x0a, 0x00, 0x00, 0x00, // + b'I', b'S', b'O', b'_', b'I', b'R', b' ', b'1', b'0', b'0', + // ReferringPhysicianName (0008,0090) + 0x08, 0x00, 0x90, 0x00, // + // Length: 12 + 0x0c, 0x00, 0x00, 0x00, b'S', b'i', b'm', 0xF5, b'e', b's', b'^', b'J', b'o', 0xE3, + b'o', b' ', + ]; + + let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2").unwrap(); + let mut cursor = &data_in[..]; + + let obj = + InMemDicomObject::read_dataset_with_dict_ts(&mut cursor, StandardDataDictionary, ts) + .unwrap(); + + let physician_name = obj.element(Tag(0x0008, 0x0090)).unwrap(); + assert_eq!(physician_name.header().len, Length(12)); + assert_eq!(physician_name.value().to_str().unwrap(), "Simões^João"); + } + + #[test] + fn inmem_object_write_dataset() { + let mut obj = InMemDicomObject::new_empty(); + + let patient_name = + DataElement::new(Tag(0x0010, 0x0010), VR::PN, dicom_value!(Str, "Doe^John")); + obj.put(patient_name); + + let mut out = Vec::new(); + + let printer = EncoderFor::new(ImplicitVRLittleEndianEncoder::default()); + + obj.write_dataset(&mut out, printer).unwrap(); + + assert_eq!( + out, + &[ + 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) + 0x08, 0x00, 0x00, 0x00, // Length: 8 + b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', + ][..], + ); + } + + #[test] + fn inmem_object_write_dataset_with_ts() { + let mut obj = InMemDicomObject::new_empty(); + + let patient_name = + DataElement::new(Tag(0x0010, 0x0010), VR::PN, dicom_value!(Str, "Doe^John")); + obj.put(patient_name); + + let mut out = Vec::new(); + + let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.1").unwrap(); + + obj.write_dataset_with_ts(&mut out, ts).unwrap(); + + assert_eq!( + out, + &[ + 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) + b'P', b'N', // VR: PN + 0x08, 0x00, // Length: 8 + b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', + ][..], + ); + } + + #[test] + fn inmem_object_write_dataset_encapsulated_pixel_data() { + let mut obj = InMemDicomObject::new_empty(); + + let sop_instance_uid = + DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.44399302050596340528032699331187776010"); + obj.put(sop_instance_uid); + + obj.put(DataElement::new( + tags::PIXEL_DATA, + VR::OB, + PixelFragmentSequence::new_fragments([ + vec![0x01, 0x02, 0x03, 0x04], + vec![0x05, 0x06, 0x07, 0x08], + ]) + )); + + let mut out = Vec::new(); + + let ts = TransferSyntaxRegistry.get(uids::ENCAPSULATED_UNCOMPRESSED_EXPLICIT_VR_LITTLE_ENDIAN).unwrap(); + + obj.write_dataset_with_ts(&mut out, ts).unwrap(); + + assert_eq!( + out, + &[ + 0x08, 0x00, 0x18, 0x00, // Tag(0x0008, 0x0018) + b'U', b'I', // VR: UI + 0x2c, 0x00, // Length: 44 + // 2.25.44399302050596340528032699331187776010 + b'2', b'.', b'2', b'5', b'.', b'4', b'4', b'3', b'9', b'9', b'3', + b'0', b'2', b'0', b'5', b'0', b'5', b'9', b'6', b'3', b'4', b'0', + b'5', b'2', b'8', b'0', b'3', b'2', b'6', b'9', b'9', b'3', b'3', + b'1', b'1', b'8', b'7', b'7', b'7', b'6', b'0', b'1', b'0', b'\0', + // pixel data + 0xe0, 0x7f, 0x10, 0x00, // Tag(0x7fe0, 0x0010) + b'O', b'B', // VR: OB + 0x00, 0x00, // reserved + 0xff, 0xff, 0xff, 0xff, // Length: undefined + // first fragment (offset table) + 0xfe, 0xff, 0x00, 0xe0, + 0x00, 0x00, 0x00, 0x00, // Length: 0 + // second fragment + 0xfe, 0xff, 0x00, 0xe0, + 0x04, 0x00, 0x00, 0x00, // Length: 4 + 0x01, 0x02, 0x03, 0x04, + // third fragment + 0xfe, 0xff, 0x00, 0xe0, + 0x04, 0x00, 0x00, 0x00, // Length: 4 + 0x05, 0x06, 0x07, 0x08, + // sequence delimitation item + 0xfe, 0xff, 0xdd, 0xe0, + 0x00, 0x00, 0x00, 0x00, // Length: 0 + ][..], + ); + } + + #[test] + fn inmem_object_write_dataset_with_ts_cs() { + let mut obj = InMemDicomObject::new_empty(); + + let patient_name = + DataElement::new(Tag(0x0010, 0x0010), VR::PN, dicom_value!(Str, "Doe^John")); + obj.put(patient_name); + + let mut out = Vec::new(); + + let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2").unwrap(); + let cs = SpecificCharacterSet::default(); + + obj.write_dataset_with_ts_cs(&mut out, ts, cs).unwrap(); + + assert_eq!( + out, + &[ + 0x10, 0x00, 0x10, 0x00, // Tag(0x0010, 0x0010) + 0x08, 0x00, 0x00, 0x00, // Length: 8 + b'D', b'o', b'e', b'^', b'J', b'o', b'h', b'n', + ][..], + ); + } + + /// writing a DICOM date time into an object + /// should include value padding + #[test] + fn inmem_object_write_datetime_odd() { + let mut obj = InMemDicomObject::new_empty(); + + // add a number that will be encoded in text + let instance_number = + DataElement::new(Tag(0x0020, 0x0013), VR::IS, PrimitiveValue::from(1_i32)); + obj.put(instance_number); + + // add a date time + let dt = DicomDateTime::from_date_and_time_with_time_zone( + DicomDate::from_ymd(2022, 11, 22).unwrap(), + DicomTime::from_hms(18, 9, 35).unwrap(), + FixedOffset::east_opt(3600).unwrap(), + ) + .unwrap(); + let instance_coercion_date_time = + DataElement::new(Tag(0x0008, 0x0015), VR::DT, dicom_value!(DateTime, dt)); + obj.put(instance_coercion_date_time); + + // explicit VR Little Endian + let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.1").unwrap(); + + let mut out = Vec::new(); + obj.write_dataset_with_ts(&mut out, ts) + .expect("should write DICOM data without errors"); + + assert_eq!( + out, + &[ + // instance coercion date time + 0x08, 0x00, 0x15, 0x00, // Tag(0x0008, 0x0015) + b'D', b'T', // VR: DT + 0x14, 0x00, // Length: 20 bytes + b'2', b'0', b'2', b'2', b'1', b'1', b'2', b'2', // date + b'1', b'8', b'0', b'9', b'3', b'5', // time + b'+', b'0', b'1', b'0', b'0', // offset + b' ', // padding to even length + // instance number + 0x20, 0x00, 0x13, 0x00, // Tag(0x0020, 0x0013) + b'I', b'S', // VR: IS + 0x02, 0x00, // Length: 2 bytes + b'1', b' ' // 1, with padding + ][..], + ); + } + + /// Writes a file from scratch + /// and opens it to check that the data is equivalent. + #[test] + fn inmem_write_to_file_with_meta() { + let sop_uid = "1.4.645.212121"; + let mut obj = InMemDicomObject::new_empty(); + + obj.put(DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + dicom_value!(Strs, ["Doe^John"]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0060), + VR::CS, + dicom_value!(Strs, ["CR"]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0018), + VR::UI, + dicom_value!(Strs, [sop_uid]), + )); + + let file_object = obj + .with_meta( + FileMetaTableBuilder::default() + // Explicit VR Little Endian + .transfer_syntax("1.2.840.10008.1.2.1") + // Computed Radiography image storage + .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1") + .media_storage_sop_instance_uid(sop_uid), + ) + .unwrap(); + + // create temporary file path and write object to that file + let dir = tempfile::tempdir().unwrap(); + let mut file_path = dir.keep(); + file_path.push(format!("{sop_uid}.dcm")); + + file_object.write_to_file(&file_path).unwrap(); + + // read the file back to validate the outcome + let saved_object = open_file(file_path).unwrap(); + assert_eq!(file_object, saved_object); + } + + /// Creating a file DICOM object from an in-mem DICOM object + /// infers the SOP instance UID. + #[test] + fn inmem_with_meta_infers_sop_instance_uid() { + let sop_uid = "1.4.645.252521"; + let mut obj = InMemDicomObject::new_empty(); + + obj.put(DataElement::new( + tags::SOP_INSTANCE_UID, + VR::UI, + PrimitiveValue::from(sop_uid), + )); + + let file_object = obj + .with_meta( + // Media Storage SOP Instance UID deliberately not set + FileMetaTableBuilder::default() + // Explicit VR Little Endian + .transfer_syntax("1.2.840.10008.1.2.1") + // Computed Radiography image storage + .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1"), + ) + .unwrap(); + + let meta = file_object.meta(); + + assert_eq!( + meta.media_storage_sop_instance_uid.trim_end_matches('\0'), + sop_uid.trim_end_matches('\0'), + ); + } + + /// Write a file from scratch, with exact file meta table. + #[test] + fn inmem_write_to_file_with_exact_meta() { + let sop_uid = "1.4.645.212121"; + let mut obj = InMemDicomObject::new_empty(); + + obj.put(DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + dicom_value!(Strs, ["Doe^John"]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0060), + VR::CS, + dicom_value!(Strs, ["CR"]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0018), + VR::UI, + dicom_value!(Strs, [sop_uid]), + )); + + let file_object = obj.with_exact_meta( + FileMetaTableBuilder::default() + // Explicit VR Little Endian + .transfer_syntax("1.2.840.10008.1.2.1") + // Computed Radiography image storage + .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1") + .media_storage_sop_instance_uid(sop_uid) + .build() + .unwrap(), + ); + + // create temporary file path and write object to that file + let dir = tempfile::tempdir().unwrap(); + let mut file_path = dir.keep(); + file_path.push(format!("{sop_uid}.dcm")); + + file_object.write_to_file(&file_path).unwrap(); + + // read the file back to validate the outcome + let saved_object = open_file(file_path).unwrap(); + assert_eq!(file_object, saved_object); + } + + #[test] + fn inmem_object_get() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + let elem1 = obj.element(Tag(0x0010, 0x0010)).unwrap(); + assert_eq!(elem1, &another_patient_name); + } + + #[test] + fn infer_media_sop_from_dataset_sop_elements() { + let sop_instance_uid = "1.4.645.313131"; + let sop_class_uid = "1.2.840.10008.5.1.4.1.1.2"; + let mut obj = InMemDicomObject::new_empty(); + + obj.put(DataElement::new( + Tag(0x0008, 0x0018), + VR::UI, + dicom_value!(Strs, [sop_instance_uid]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0016), + VR::UI, + dicom_value!(Strs, [sop_class_uid]), + )); + + let file_object = obj.with_exact_meta( + FileMetaTableBuilder::default() + .transfer_syntax("1.2.840.10008.1.2.1") + // Media Storage SOP Class and Instance UIDs are missing and set to an empty string + .media_storage_sop_class_uid("") + .media_storage_sop_instance_uid("") + .build() + .unwrap(), + ); + + // create temporary file path and write object to that file + let dir = tempfile::tempdir().unwrap(); + let mut file_path = dir.keep(); + file_path.push(format!("{sop_instance_uid}.dcm")); + + file_object.write_to_file(&file_path).unwrap(); + + // read the file back to validate the outcome + let saved_object = open_file(file_path).unwrap(); + + // verify that the empty string media storage sop instance and class UIDs have been inferred from the sop instance and class UID + assert_eq!( + saved_object.meta().media_storage_sop_instance_uid(), + sop_instance_uid + ); + assert_eq!( + saved_object.meta().media_storage_sop_class_uid(), + sop_class_uid + ); + } + + #[test] + fn inmem_object_get_opt() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + let elem1 = obj.element_opt(Tag(0x0010, 0x0010)).unwrap(); + assert_eq!(elem1, Some(&another_patient_name)); + + // try a missing element, should return None + assert_eq!(obj.element_opt(Tag(0x0010, 0x0020)).unwrap(), None); + } + + #[test] + fn inmem_object_get_by_name() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + let elem1 = obj.element_by_name("PatientName").unwrap(); + assert_eq!(elem1, &another_patient_name); + } + + #[test] + fn inmem_object_get_by_name_opt() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + let elem1 = obj.element_by_name_opt("PatientName").unwrap(); + assert_eq!(elem1, Some(&another_patient_name)); + + // try a missing element, should return None + assert_eq!(obj.element_by_name_opt("PatientID").unwrap(), None); + } + + #[test] + fn inmem_object_take_element() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + let elem1 = obj.take_element(Tag(0x0010, 0x0010)).unwrap(); + assert_eq!(elem1, another_patient_name); + assert!(matches!( + obj.take_element(Tag(0x0010, 0x0010)), + Err(AccessError::NoSuchDataElementTag { + tag: Tag(0x0010, 0x0010), + .. + }) + )); + } + + #[test] + fn inmem_object_take_element_by_name() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + let elem1 = obj.take_element_by_name("PatientName").unwrap(); + assert_eq!(elem1, another_patient_name); + assert!(matches!( + obj.take_element_by_name("PatientName"), + Err(AccessByNameError::NoSuchDataElementAlias { + tag: Tag(0x0010, 0x0010), + alias, + .. + }) if alias == "PatientName")); + } + + #[test] + fn inmem_object_remove_element() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + assert!(obj.remove_element(Tag(0x0010, 0x0010))); + assert!(!obj.remove_element(Tag(0x0010, 0x0010))); + } + + #[test] + fn inmem_object_remove_element_by_name() { + let another_patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(another_patient_name.clone()); + assert!(obj.remove_element_by_name("PatientName").unwrap()); + assert!(!obj.remove_element_by_name("PatientName").unwrap()); + } + + /// Elements are traversed in tag order. + #[test] + fn inmem_traverse_elements() { + let sop_uid = "1.4.645.212121"; + let mut obj = InMemDicomObject::new_empty(); + + obj.put(DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + dicom_value!(Strs, ["Doe^John"]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0060), + VR::CS, + dicom_value!(Strs, ["CR"]), + )); + obj.put(DataElement::new( + Tag(0x0008, 0x0018), + VR::UI, + dicom_value!(Strs, [sop_uid]), + )); + + { + let mut iter = obj.iter(); + assert_eq!( + *iter.next().unwrap().header(), + DataElementHeader::new(Tag(0x0008, 0x0018), VR::UI, Length(sop_uid.len() as u32)), + ); + assert_eq!( + *iter.next().unwrap().header(), + DataElementHeader::new(Tag(0x0008, 0x0060), VR::CS, Length(2)), + ); + assert_eq!( + *iter.next().unwrap().header(), + DataElementHeader::new(Tag(0x0010, 0x0010), VR::PN, Length(8)), + ); + } + + // .tags() + let tags: Vec<_> = obj.tags().collect(); + assert_eq!( + tags, + vec![ + Tag(0x0008, 0x0018), + Tag(0x0008, 0x0060), + Tag(0x0010, 0x0010), + ] + ); + + // .into_iter() + let mut iter = obj.into_iter(); + assert_eq!( + iter.next(), + Some(DataElement::new( + Tag(0x0008, 0x0018), + VR::UI, + dicom_value!(Strs, [sop_uid]), + )), + ); + assert_eq!( + iter.next(), + Some(DataElement::new( + Tag(0x0008, 0x0060), + VR::CS, + dicom_value!(Strs, ["CR"]), + )), + ); + assert_eq!( + iter.next(), + Some(DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::from("Doe^John"), + )), + ); + } + + #[test] + fn inmem_empty_object_into_tokens() { + let obj = InMemDicomObject::new_empty(); + let tokens = obj.into_tokens(); + assert_eq!(tokens.count(), 0); + } + + #[test] + fn inmem_shallow_object_from_tokens() { + let tokens = vec![ + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x0060), + vr: VR::CS, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::Str("MG".to_owned())), + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0010, 0x0010), + vr: VR::PN, + len: Length(8), + }), + DataToken::PrimitiveValue(PrimitiveValue::Str("Doe^John".to_owned())), + ]; + + let gt_obj = InMemDicomObject::from_element_iter(vec![ + DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ), + DataElement::new( + Tag(0x0008, 0x0060), + VR::CS, + PrimitiveValue::Str("MG".to_string()), + ), + ]); + + let obj = InMemDicomObject::build_object( + &mut tokens.into_iter().map(Result::Ok), + StandardDataDictionary, + false, + Length::UNDEFINED, + None, + ) + .unwrap(); + + assert_obj_eq(&obj, >_obj); + } + + #[test] + fn inmem_shallow_object_into_tokens() { + let patient_name = DataElement::new( + Tag(0x0010, 0x0010), + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + ); + let modality = DataElement::new( + Tag(0x0008, 0x0060), + VR::CS, + PrimitiveValue::Str("MG".to_string()), + ); + let mut obj = InMemDicomObject::new_empty(); + obj.put(patient_name); + obj.put(modality); + + let tokens: Vec<_> = obj.into_tokens().collect(); + + assert_eq!( + tokens, + vec![ + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x0060), + vr: VR::CS, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::Str("MG".to_owned())), + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0010, 0x0010), + vr: VR::PN, + len: Length(8), + }), + DataToken::PrimitiveValue(PrimitiveValue::Str("Doe^John".to_owned())), + ] + ); + } + + #[test] + fn inmem_deep_object_from_tokens() { + use smallvec::smallvec; + + let obj_1 = InMemDicomObject::from_element_iter(vec![ + DataElement::new(Tag(0x0018, 0x6012), VR::US, Value::Primitive(1_u16.into())), + DataElement::new(Tag(0x0018, 0x6014), VR::US, Value::Primitive(2_u16.into())), + ]); + + let obj_2 = InMemDicomObject::from_element_iter(vec![DataElement::new( + Tag(0x0018, 0x6012), + VR::US, + Value::Primitive(4_u16.into()), + )]); + + let gt_obj = InMemDicomObject::from_element_iter(vec![ + DataElement::new( + Tag(0x0018, 0x6011), + VR::SQ, + Value::from(DataSetSequence::new( + smallvec![obj_1, obj_2], + Length::UNDEFINED, + )), + ), + DataElement::new(Tag(0x0020, 0x4000), VR::LT, Value::Primitive("TEST".into())), + ]); + + let tokens: Vec<_> = vec![ + DataToken::SequenceStart { + tag: Tag(0x0018, 0x6011), + len: Length::UNDEFINED, + }, + DataToken::ItemStart { + len: Length::UNDEFINED, + }, + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0018, 0x6012), + vr: VR::US, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::U16([1].as_ref().into())), + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0018, 0x6014), + vr: VR::US, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::U16([2].as_ref().into())), + DataToken::ItemEnd, + DataToken::ItemStart { + len: Length::UNDEFINED, + }, + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0018, 0x6012), + vr: VR::US, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::U16([4].as_ref().into())), + DataToken::ItemEnd, + DataToken::SequenceEnd, + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0020, 0x4000), + vr: VR::LT, + len: Length(4), + }), + DataToken::PrimitiveValue(PrimitiveValue::Str("TEST".into())), + ]; + + let obj = InMemDicomObject::build_object( + &mut tokens.into_iter().map(Result::Ok), + StandardDataDictionary, + false, + Length::UNDEFINED, + None, + ) + .unwrap(); + + assert_obj_eq(&obj, >_obj); + } + + #[test] + fn inmem_deep_object_into_tokens() { + use smallvec::smallvec; + + let obj_1 = InMemDicomObject::from_element_iter(vec![ + DataElement::new(Tag(0x0018, 0x6012), VR::US, Value::Primitive(1_u16.into())), + DataElement::new(Tag(0x0018, 0x6014), VR::US, Value::Primitive(2_u16.into())), + ]); + + let obj_2 = InMemDicomObject::from_element_iter(vec![DataElement::new( + Tag(0x0018, 0x6012), + VR::US, + Value::Primitive(4_u16.into()), + )]); + + let main_obj = InMemDicomObject::from_element_iter(vec![ + DataElement::new( + Tag(0x0018, 0x6011), + VR::SQ, + Value::from(DataSetSequence::new( + smallvec![obj_1, obj_2], + Length::UNDEFINED, + )), + ), + DataElement::new(Tag(0x0020, 0x4000), VR::LT, Value::Primitive("TEST".into())), + ]); + + let tokens: Vec<_> = main_obj.into_tokens().collect(); + + assert_eq!( + tokens, + vec![ + DataToken::SequenceStart { + tag: Tag(0x0018, 0x6011), + len: Length::UNDEFINED, + }, + DataToken::ItemStart { + len: Length::UNDEFINED, + }, + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0018, 0x6012), + vr: VR::US, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::U16([1].as_ref().into())), + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0018, 0x6014), + vr: VR::US, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::U16([2].as_ref().into())), + DataToken::ItemEnd, + DataToken::ItemStart { + len: Length::UNDEFINED, + }, + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0018, 0x6012), + vr: VR::US, + len: Length(2), + }), + DataToken::PrimitiveValue(PrimitiveValue::U16([4].as_ref().into())), + DataToken::ItemEnd, + DataToken::SequenceEnd, + DataToken::ElementHeader(DataElementHeader { + tag: Tag(0x0020, 0x4000), + vr: VR::LT, + len: Length(4), + }), + DataToken::PrimitiveValue(PrimitiveValue::Str("TEST".into())), + ] + ); + } + + #[test] + fn inmem_encapsulated_pixel_data_from_tokens() { + use smallvec::smallvec; + + let gt_obj = InMemDicomObject::from_element_iter(vec![DataElement::new( + Tag(0x7fe0, 0x0010), + VR::OB, + Value::from(PixelFragmentSequence::new_fragments(smallvec![vec![ + 0x33; + 32 + ]])), + )]); + + let tokens: Vec<_> = vec![ + DataToken::PixelSequenceStart, + DataToken::ItemStart { len: Length(0) }, + DataToken::ItemEnd, + DataToken::ItemStart { len: Length(32) }, + DataToken::ItemValue(vec![0x33; 32]), + DataToken::ItemEnd, + DataToken::SequenceEnd, + ]; + + let obj = InMemDicomObject::build_object( + &mut tokens.into_iter().map(Result::Ok), + StandardDataDictionary, + false, + Length::UNDEFINED, + None, + ) + .unwrap(); + + assert_obj_eq(&obj, >_obj); + } + + #[test] + fn inmem_encapsulated_pixel_data_into_tokens() { + use smallvec::smallvec; + + let main_obj = InMemDicomObject::from_element_iter(vec![DataElement::new( + Tag(0x7fe0, 0x0010), + VR::OB, + Value::from(PixelFragmentSequence::new_fragments(smallvec![vec![ + 0x33; + 32 + ]])), + )]); + + let tokens: Vec<_> = main_obj.into_tokens().collect(); + + assert_eq!( + tokens, + vec![ + DataToken::PixelSequenceStart, + DataToken::ItemStart { len: Length(0) }, + DataToken::ItemEnd, + DataToken::ItemStart { len: Length(32) }, + DataToken::ItemValue(vec![0x33; 32]), + DataToken::ItemEnd, + DataToken::SequenceEnd, + ] + ); + } + + /// Test that a DICOM object can be reliably used + /// behind the `DicomObject` trait. + #[test] + fn can_use_behind_trait() { + fn dicom_dataset() -> impl DicomObject { + InMemDicomObject::from_element_iter([DataElement::new( + tags::PATIENT_NAME, + VR::PN, + PrimitiveValue::Str("Doe^John".to_string()), + )]) + } + + let obj = dicom_dataset(); + let elem1 = obj + .attr_by_name_opt("PatientName") + .unwrap() + .expect("PatientName should be present"); + assert_eq!( + &elem1 + .to_str() + .expect("should be able to retrieve patient name as string"), + "Doe^John" + ); + + // try a missing element, should return None + assert!(obj.attr_opt(tags::PATIENT_ID).unwrap().is_none()); + } + + /// Test attribute operations on in-memory DICOM objects. + #[test] + fn inmem_ops() { + // create a base DICOM object + let base_obj = InMemDicomObject::from_element_iter([ + DataElement::new( + tags::SERIES_INSTANCE_UID, + VR::UI, + PrimitiveValue::from("2.25.137041794342168732369025909031346220736.1"), + ), + DataElement::new( + tags::SERIES_INSTANCE_UID, + VR::UI, + PrimitiveValue::from("2.25.137041794342168732369025909031346220736.1"), + ), + DataElement::new( + tags::SOP_INSTANCE_UID, + VR::UI, + PrimitiveValue::from("2.25.137041794342168732369025909031346220736.1.1"), + ), + DataElement::new( + tags::STUDY_DESCRIPTION, + VR::LO, + PrimitiveValue::from("Test study"), + ), + DataElement::new( + tags::INSTITUTION_NAME, + VR::LO, + PrimitiveValue::from("Test Hospital"), + ), + DataElement::new(tags::ROWS, VR::US, PrimitiveValue::from(768_u16)), + DataElement::new(tags::COLUMNS, VR::US, PrimitiveValue::from(1024_u16)), + DataElement::new( + tags::LOSSY_IMAGE_COMPRESSION, + VR::CS, + PrimitiveValue::from("01"), + ), + DataElement::new( + tags::LOSSY_IMAGE_COMPRESSION_RATIO, + VR::DS, + PrimitiveValue::from("5"), + ), + DataElement::new( + tags::LOSSY_IMAGE_COMPRESSION_METHOD, + VR::DS, + PrimitiveValue::from("ISO_10918_1"), + ), + ]); + + { + // remove + let mut obj = base_obj.clone(); + let op = AttributeOp { + selector: AttributeSelector::from(tags::STUDY_DESCRIPTION), + action: AttributeAction::Remove, + }; + + obj.apply(op).unwrap(); + + assert_eq!(obj.get(tags::STUDY_DESCRIPTION), None); + } + { + let mut obj = base_obj.clone(); + + // set if missing does nothing + // on an existing string + let op = AttributeOp { + selector: tags::INSTITUTION_NAME.into(), + action: AttributeAction::SetIfMissing("Nope Hospital".into()), + }; + + obj.apply(op).unwrap(); + + assert_eq!( + obj.get(tags::INSTITUTION_NAME), + Some(&DataElement::new( + tags::INSTITUTION_NAME, + VR::LO, + PrimitiveValue::from("Test Hospital"), + )) + ); + + // replace string + let op = AttributeOp::new( + tags::INSTITUTION_NAME, + AttributeAction::ReplaceStr("REMOVED".into()), + ); + + obj.apply(op).unwrap(); + + assert_eq!( + obj.get(tags::INSTITUTION_NAME), + Some(&DataElement::new( + tags::INSTITUTION_NAME, + VR::LO, + PrimitiveValue::from("REMOVED"), + )) + ); + + // replacing a non-existing attribute + // does nothing + let op = AttributeOp::new( + tags::REQUESTING_PHYSICIAN, + AttributeAction::ReplaceStr("Doctor^Anonymous".into()), + ); + + obj.apply(op).unwrap(); + + assert_eq!(obj.get(tags::REQUESTING_PHYSICIAN), None); + + // but DetIfMissing works + let op = AttributeOp::new( + tags::REQUESTING_PHYSICIAN, + AttributeAction::SetStrIfMissing("Doctor^Anonymous".into()), + ); + + obj.apply(op).unwrap(); + + assert_eq!( + obj.get(tags::REQUESTING_PHYSICIAN), + Some(&DataElement::new( + tags::REQUESTING_PHYSICIAN, + VR::PN, + PrimitiveValue::from("Doctor^Anonymous"), + )) + ); + } + { + // reset string + let mut obj = base_obj.clone(); + let op = AttributeOp::new( + tags::REQUESTING_PHYSICIAN, + AttributeAction::SetStr("Doctor^Anonymous".into()), + ); + + obj.apply(op).unwrap(); + + assert_eq!( + obj.get(tags::REQUESTING_PHYSICIAN), + Some(&DataElement::new( + tags::REQUESTING_PHYSICIAN, + VR::PN, + PrimitiveValue::from("Doctor^Anonymous"), + )) + ); + } + + { + // extend with number + let mut obj = base_obj.clone(); + let op = AttributeOp::new( + tags::LOSSY_IMAGE_COMPRESSION_RATIO, + AttributeAction::PushF64(1.25), + ); + + obj.apply(op).unwrap(); + + assert_eq!( + obj.get(tags::LOSSY_IMAGE_COMPRESSION_RATIO), + Some(&DataElement::new( + tags::LOSSY_IMAGE_COMPRESSION_RATIO, + VR::DS, + dicom_value!(Strs, ["5", "1.25"]), + )) + ); + } + } + + /// Test attribute operations on nested data sets. + #[test] + fn nested_inmem_ops() { + let obj_1 = InMemDicomObject::from_element_iter([ + DataElement::new(Tag(0x0018, 0x6012), VR::US, PrimitiveValue::from(1_u16)), + DataElement::new(Tag(0x0018, 0x6014), VR::US, PrimitiveValue::from(2_u16)), + ]); + + let obj_2 = InMemDicomObject::from_element_iter([DataElement::new( + Tag(0x0018, 0x6012), + VR::US, + PrimitiveValue::from(4_u16), + )]); + + let mut main_obj = InMemDicomObject::from_element_iter(vec![ + DataElement::new( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + VR::SQ, + DataSetSequence::from(vec![obj_1, obj_2]), + ), + DataElement::new(Tag(0x0020, 0x4000), VR::LT, Value::Primitive("TEST".into())), + ]); + + let selector: AttributeSelector = + (tags::SEQUENCE_OF_ULTRASOUND_REGIONS, 0, Tag(0x0018, 0x6014)).into(); + + main_obj + .apply(AttributeOp::new(selector, AttributeAction::Set(3.into()))) + .unwrap(); + + assert_eq!( + main_obj + .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .unwrap() + .items() + .unwrap()[0] + .get(Tag(0x0018, 0x6014)) + .unwrap() + .value(), + &PrimitiveValue::from(3).into(), + ); + + let selector: AttributeSelector = + (tags::SEQUENCE_OF_ULTRASOUND_REGIONS, 1, Tag(0x0018, 0x6012)).into(); + + main_obj + .apply(AttributeOp::new(selector, AttributeAction::Remove)) + .unwrap(); + + // item should be empty + assert_eq!( + main_obj + .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .unwrap() + .items() + .unwrap()[1] + .tags() + .collect::>(), + Vec::::new(), + ); + + // trying to access the removed element returns an error + assert!(matches!( + main_obj.value_at((tags::SEQUENCE_OF_ULTRASOUND_REGIONS, 1, Tag(0x0018, 0x6012),)), + Err(AtAccessError::MissingLeafElement { .. }) + )) + } + + /// Test that constructive operations create items if necessary. + #[test] + fn constructive_op() { + let mut obj = InMemDicomObject::from_element_iter([DataElement::new( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + VR::SQ, + DataSetSequence::empty(), + )]); + + let op = AttributeOp::new( + ( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + 0, + tags::REGION_SPATIAL_FORMAT, + ), + AttributeAction::Set(5_u16.into()), + ); + + obj.apply(op).unwrap(); + + // should have an item + assert_eq!( + obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .unwrap() + .items() + .unwrap() + .len(), + 1, + ); + + // item should have 1 element + assert_eq!( + &obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .unwrap() + .items() + .unwrap()[0], + &InMemDicomObject::from_element_iter([DataElement::new( + tags::REGION_SPATIAL_FORMAT, + VR::US, + PrimitiveValue::from(5_u16) + )]), + ); + + // new value can be accessed using value_at + assert_eq!( + obj.value_at(( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + 0, + tags::REGION_SPATIAL_FORMAT + )) + .unwrap(), + &Value::from(PrimitiveValue::from(5_u16)), + ) + } + + /// Test that operations on in-memory DICOM objects + /// can create sequences from scratch. + #[test] + fn inmem_ops_can_create_seq() { + let mut obj = InMemDicomObject::new_empty(); + + obj.apply(AttributeOp::new( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + AttributeAction::SetIfMissing(PrimitiveValue::Empty), + )) + .unwrap(); + + { + // should create an empty sequence + let sequence_ultrasound = obj + .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .expect("should have sequence element"); + + assert_eq!(sequence_ultrasound.vr(), VR::SQ); + + assert_eq!(sequence_ultrasound.items(), Some(&[][..]),); + } + + obj.apply(AttributeOp::new( + ( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + tags::REGION_SPATIAL_FORMAT, + ), + AttributeAction::Set(1_u16.into()), + )) + .unwrap(); + + { + // sequence should now have an item + assert_eq!( + obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .unwrap() + .items() + .map(|items| items.len()), + Some(1), + ); + } + } + + /// Test that operations on in-memory DICOM objects + /// can create deeply nested attributes from scratch. + #[test] + fn inmem_ops_can_create_nested_attribute() { + let mut obj = InMemDicomObject::new_empty(); + + obj.apply(AttributeOp::new( + ( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + tags::REGION_SPATIAL_FORMAT, + ), + AttributeAction::Set(1_u16.into()), + )) + .unwrap(); + + { + // should create a sequence with a single item + assert_eq!( + obj.get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .unwrap() + .items() + .map(|items| items.len()), + Some(1), + ); + + // item should have Region Spatial Format + assert_eq!( + obj.value_at(( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + tags::REGION_SPATIAL_FORMAT + )) + .unwrap(), + &PrimitiveValue::from(1_u16).into(), + ); + + // same result when using `DicomObject::at` + assert_eq!( + DicomObject::at(&obj, ( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + tags::REGION_SPATIAL_FORMAT + )) + .unwrap(), + &PrimitiveValue::from(1_u16).into(), + ); + } + } + + /// Test that operations on in-memory DICOM objects + /// can truncate sequences. + #[test] + fn inmem_ops_can_truncate_seq() { + let mut obj = InMemDicomObject::from_element_iter([ + DataElement::new( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + VR::SQ, + DataSetSequence::from(vec![InMemDicomObject::new_empty()]), + ), + DataElement::new_with_len( + tags::PIXEL_DATA, + VR::OB, + Length::UNDEFINED, + PixelFragmentSequence::new(vec![], vec![vec![0xcc; 8192], vec![0x55; 1024]]), + ), + ]); + + // removes the single item in the sequences + obj.apply(AttributeOp::new( + tags::SEQUENCE_OF_ULTRASOUND_REGIONS, + AttributeAction::Truncate(0), + )) + .unwrap(); + + { + let sequence_ultrasound = obj + .get(tags::SEQUENCE_OF_ULTRASOUND_REGIONS) + .expect("should have sequence element"); + assert_eq!(sequence_ultrasound.items(), Some(&[][..]),); + } + + // remove one of the fragments + obj.apply(AttributeOp::new( + tags::PIXEL_DATA, + AttributeAction::Truncate(1), + )) + .unwrap(); + + { + // pixel data should now have a single fragment + assert_eq!( + obj.get(tags::PIXEL_DATA) + .unwrap() + .fragments() + .map(|fragments| fragments.len()), + Some(1), + ); + } + } + + #[test] + fn inmem_obj_reset_defined_length() { + let mut entries: BTreeMap> = BTreeMap::new(); + + let patient_name = + DataElement::new(tags::PATIENT_NAME, VR::CS, PrimitiveValue::from("Doe^John")); + + let study_description = DataElement::new( + tags::STUDY_DESCRIPTION, + VR::LO, + PrimitiveValue::from("Test study"), + ); + + entries.insert(tags::PATIENT_NAME, patient_name.clone()); + + // create object and force an arbitrary defined Length value + let obj = InMemDicomObject:: { + entries, + dict: StandardDataDictionary, + len: Length(1), + charset_changed: false, + }; + + assert!(obj.length().is_defined()); + + let mut o = obj.clone(); + o.put_element(study_description); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.remove_element(tags::PATIENT_NAME); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.remove_element_by_name("PatientName").unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.take_element(tags::PATIENT_NAME).unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.take_element_by_name("PatientName").unwrap(); + assert!(o.length().is_undefined()); + + // resets Length even when retain does not make any changes + let mut o = obj.clone(); + o.retain(|e| e.tag() == tags::PATIENT_NAME); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::Remove, + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new(tags::PATIENT_NAME, AttributeAction::Empty)) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::SetVr(VR::IS), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::Set(dicom_value!(Str, "Unknown")), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::SetStr("Patient^Anonymous".into()), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_AGE, + AttributeAction::SetIfMissing(dicom_value!(75)), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_ADDRESS, + AttributeAction::SetStrIfMissing("Chicago".into()), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::Replace(dicom_value!(Str, "Unknown")), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::ReplaceStr("Unknown".into()), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushStr("^Prof".into()), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushI32(-16), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushU32(16), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushI16(-16), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushU16(16), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushF32(16.16), + )) + .unwrap(); + assert!(o.length().is_undefined()); + + let mut o = obj.clone(); + o.apply(AttributeOp::new( + tags::PATIENT_NAME, + AttributeAction::PushF64(16.1616), + )) + .unwrap(); + assert!(o.length().is_undefined()); + } + + #[test] + fn create_commands() { + // empty + let obj = InMemDicomObject::command_from_element_iter([]); + assert_eq!( + obj.get(tags::COMMAND_GROUP_LENGTH) + .map(|e| e.value().to_int::().unwrap()), + Some(0) + ); + + // C-FIND-RQ + let obj = InMemDicomObject::command_from_element_iter([ + // affected SOP class UID: 8 + 28 = 36 + DataElement::new( + tags::AFFECTED_SOP_CLASS_UID, + VR::UI, + PrimitiveValue::from("1.2.840.10008.5.1.4.1.2.1.1"), + ), + // command field: 36 + 8 + 2 = 46 + DataElement::new( + tags::COMMAND_FIELD, + VR::US, + // 0020H: C-FIND-RQ message + dicom_value!(U16, [0x0020]), + ), + // message ID: 46 + 8 + 2 = 56 + DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [0])), + //priority: 56 + 8 + 2 = 66 + DataElement::new( + tags::PRIORITY, + VR::US, + // medium + dicom_value!(U16, [0x0000]), + ), + // data set type: 66 + 8 + 2 = 76 + DataElement::new( + tags::COMMAND_DATA_SET_TYPE, + VR::US, + dicom_value!(U16, [0x0001]), + ), + ]); + assert_eq!( + obj.get(tags::COMMAND_GROUP_LENGTH) + .map(|e| e.value().to_int::().unwrap()), + Some(76) + ); + + let storage_sop_class_uid = "1.2.840.10008.5.1.4.1.1.4"; + let storage_sop_instance_uid = "2.25.221314879990624101283043547144116927116"; + + // C-STORE-RQ + let obj = InMemDicomObject::command_from_element_iter([ + // group length (should be ignored in calculations and overridden) + DataElement::new( + tags::COMMAND_GROUP_LENGTH, + VR::UL, + PrimitiveValue::from(9999_u32), + ), + // SOP Class UID: 8 + 26 = 34 + DataElement::new( + tags::AFFECTED_SOP_CLASS_UID, + VR::UI, + dicom_value!(Str, storage_sop_class_uid), + ), + // command field: 34 + 8 + 2 = 44 + DataElement::new(tags::COMMAND_FIELD, VR::US, dicom_value!(U16, [0x0001])), + // message ID: 44 + 8 + 2 = 54 + DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [1])), + //priority: 54 + 8 + 2 = 64 + DataElement::new(tags::PRIORITY, VR::US, dicom_value!(U16, [0x0000])), + // data set type: 64 + 8 + 2 = 74 + DataElement::new( + tags::COMMAND_DATA_SET_TYPE, + VR::US, + dicom_value!(U16, [0x0000]), + ), + // affected SOP Instance UID: 74 + 8 + 44 = 126 + DataElement::new( + tags::AFFECTED_SOP_INSTANCE_UID, + VR::UI, + dicom_value!(Str, storage_sop_instance_uid), + ), + ]); + + assert_eq!( + obj.get(tags::COMMAND_GROUP_LENGTH) + .map(|e| e.value().to_int::().unwrap()), + Some(126) + ); + } + + #[test] + fn test_even_len() { + assert_eq!(even_len(0), 0); + assert_eq!(even_len(1), 2); + assert_eq!(even_len(2), 2); + assert_eq!(even_len(3), 4); + assert_eq!(even_len(4), 4); + assert_eq!(even_len(5), 6); + } + + #[test] + fn can_update_value() { + let mut obj = InMemDicomObject::from_element_iter([DataElement::new( + tags::ANATOMIC_REGION_SEQUENCE, + VR::SQ, + DataSetSequence::empty(), + )]); + assert_eq!( + obj.get(tags::ANATOMIC_REGION_SEQUENCE).map(|e| e.length()), + Some(Length(0)), + ); + + assert!(!obj.update_value(tags::BURNED_IN_ANNOTATION, |_value| { + panic!("should not be called") + }),); + + let o = obj.update_value(tags::ANATOMIC_REGION_SEQUENCE, |value| { + // add an item + let items = value.items_mut().unwrap(); + items.push(InMemDicomObject::from_element_iter([DataElement::new( + tags::INSTANCE_NUMBER, + VR::IS, + PrimitiveValue::from(1), + )])); + }); + assert!(o); + + assert!(obj + .get(tags::ANATOMIC_REGION_SEQUENCE) + .unwrap() + .length() + .is_undefined()); + } + + #[test] + fn deep_sequence_change_encoding_writes_undefined_sequence_length() { + use smallvec::smallvec; + + let obj_1 = InMemDicomObject::from_element_iter(vec![ + //The length of this string is 20 bytes in ISO_IR 100 but should be 22 bytes in ISO_IR 192 (UTF-8) + DataElement::new( + tags::STUDY_DESCRIPTION, + VR::SL, + Value::Primitive("MORFOLOGÍA Y FUNCIÓN".into()), + ), + //ISO_IR 100 and ISO_IR 192 length are the same + DataElement::new( + tags::SERIES_DESCRIPTION, + VR::SL, + Value::Primitive("0123456789".into()), + ), + ]); + + let some_tag = Tag(0x0018, 0x6011); + + let inner_sequence = InMemDicomObject::from_element_iter(vec![DataElement::new( + some_tag, + VR::SQ, + Value::from(DataSetSequence::new( + smallvec![obj_1], + Length(30), //20 bytes from study, 10 from series + )), + )]); + let outer_sequence = DataElement::new( + some_tag, + VR::SQ, + Value::from(DataSetSequence::new( + smallvec![inner_sequence.clone(), inner_sequence], + Length(60), //20 bytes from study, 10 from series + )), + ); + + let original_object = InMemDicomObject::from_element_iter(vec![ + DataElement::new(tags::SPECIFIC_CHARACTER_SET, VR::CS, "ISO_IR 100"), + outer_sequence, + ]); + + assert_eq!( + original_object + .get(some_tag) + .expect("object should be present") + .length(), + Length(60) + ); + + let mut changed_charset = original_object.clone(); + changed_charset.convert_to_utf8(); + assert!(changed_charset.charset_changed); + + use dicom_parser::dataset::DataToken as token; + let options = IntoTokensOptions::new(true); + let converted_tokens: Vec<_> = changed_charset.into_tokens_with_options(options).collect(); + + assert_eq!( + vec![ + token::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x0005), + vr: VR::CS, + len: Length(10), + }), + token::PrimitiveValue("ISO_IR 192".into()), + token::SequenceStart { + tag: Tag(0x0018, 0x6011), + len: Length::UNDEFINED, + }, + token::ItemStart { + len: Length::UNDEFINED + }, + token::SequenceStart { + tag: Tag(0x0018, 0x6011), + len: Length::UNDEFINED, + }, + token::ItemStart { + len: Length::UNDEFINED + }, + token::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x1030), + vr: VR::SL, + len: Length(22), + }), + token::PrimitiveValue("MORFOLOGÍA Y FUNCIÓN".into()), + token::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x103E), + vr: VR::SL, + len: Length(10), + }), + token::PrimitiveValue("0123456789".into()), + token::ItemEnd, + token::SequenceEnd, + token::ItemEnd, + token::ItemStart { + len: Length::UNDEFINED + }, + token::SequenceStart { + tag: Tag(0x0018, 0x6011), + len: Length::UNDEFINED, + }, + token::ItemStart { + len: Length::UNDEFINED + }, + token::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x1030), + vr: VR::SL, + len: Length(22), + }), + token::PrimitiveValue("MORFOLOGÍA Y FUNCIÓN".into()), + token::ElementHeader(DataElementHeader { + tag: Tag(0x0008, 0x103E), + vr: VR::SL, + len: Length(10), + }), + token::PrimitiveValue("0123456789".into()), + token::ItemEnd, + token::SequenceEnd, + token::ItemEnd, + token::SequenceEnd, + ], + 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(); + + let res = ds.put_private_element( + 0x0012, + "CREATOR 4", + 0x02, + VR::DS, + PrimitiveValue::Str("1.0".to_string()), + ); + assert_eq!( + &res.err().unwrap().to_string(), + "Group number must be odd, found 0x0012" + ); + + 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) + ); + } + + #[test] + fn private_element_group_full() { + let mut ds = InMemDicomObject::from_element_iter( + (0..=0x00FFu16) + .map(|i| { + DataElement::new(Tag(0x0009, i), VR::LO, PrimitiveValue::from("CREATOR 1")) + }) + .collect::>>(), + ); + let res = ds.put_private_element(0x0009, "TEST", 0x01, VR::DS, PrimitiveValue::from("1.0")); + assert_eq!( + res.err().unwrap().to_string(), + "No space available in group 0x0009" + ); + } +} diff --git a/object/src/meta.rs b/object/src/meta.rs new file mode 100644 index 00000000..a4dbaa6b --- /dev/null +++ b/object/src/meta.rs @@ -0,0 +1,1802 @@ +//! Module containing data structures and readers of DICOM file meta information tables. +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, AttributeSelector, AttributeSelectorStep, +}; +use dicom_core::value::{ + ConvertValueError, DicomValueType, InMemFragment, 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::borrow::Cow; +use std::io::{Read, Write}; + +use crate::ops::{ + ApplyError, ApplyResult, IllegalExtendSnafu, IncompatibleTypesSnafu, MandatorySnafu, + UnsupportedActionSnafu, UnsupportedAttributeSnafu, +}; +use crate::{ + AtAccessError, AttributeError, DicomAttribute, DicomObject, 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 { + /// The file meta group parser could not read + /// the magic code `DICM` from its source. + #[snafu(display("Could not start reading DICOM data"))] + ReadMagicCode { + backtrace: Backtrace, + source: std::io::Error, + }, + + /// The file meta group parser could not fetch + /// the value of a data element from its source. + #[snafu(display("Could not read data value"))] + ReadValueData { + backtrace: Backtrace, + source: std::io::Error, + }, + + /// The parser could not allocate memory for the + /// given length of a data element. + #[snafu(display("Could not allocate memory"))] + AllocationSize { + backtrace: Backtrace, + source: std::collections::TryReserveError, + }, + + /// The file meta group parser could not decode + /// the text in one of its data elements. + #[snafu(display("Could not decode text in {}", name))] + DecodeText { + name: std::borrow::Cow<'static, str>, + #[snafu(backtrace)] + source: dicom_encoding::text::DecodeTextError, + }, + + /// Invalid DICOM data, detected by checking the `DICM` code. + #[snafu(display("Invalid DICOM file (magic code check failed)"))] + NotDicom { backtrace: Backtrace }, + + /// An issue occurred while decoding the next data element + /// in the file meta data set. + #[snafu(display("Could not decode data element"))] + DecodeElement { + #[snafu(backtrace)] + source: dicom_encoding::decode::Error, + }, + + /// A data element with an unexpected tag was retrieved: + /// the parser was expecting another tag first, + /// or at least one that is part of the the file meta group. + #[snafu(display("Unexpected data element tagged {}", tag))] + UnexpectedTag { tag: Tag, backtrace: Backtrace }, + + /// A required file meta data element is missing. + #[snafu(display("Missing data element `{}`", alias))] + MissingElement { + alias: &'static str, + backtrace: Backtrace, + }, + + /// The value length of a data elements in the file meta group + /// was unexpected. + #[snafu(display("Unexpected length {} for data element tagged {}", length, tag))] + UnexpectedDataValueLength { + tag: Tag, + length: Length, + backtrace: Backtrace, + }, + + /// The value length of a data element is undefined, + /// but knowing the length is required in its context. + #[snafu(display("Undefined value length for data element tagged {}", tag))] + UndefinedValueLength { tag: Tag, backtrace: Backtrace }, + + /// The file meta group data set could not be written. + #[snafu(display("Could not write file meta group data set"))] + WriteSet { + #[snafu(backtrace)] + source: dicom_parser::dataset::write::Error, + }, +} + +type Result = std::result::Result; + +/// DICOM File Meta Information Table. +/// +/// This data type contains the relevant parts of the file meta information table, +/// as specified in [part 6, chapter 7][1] of the standard. +/// +/// Creating a new file meta table from scratch +/// is more easily done using a [`FileMetaTableBuilder`]. +/// When modifying the struct's public fields, +/// it is possible to update the information group length +/// through method [`update_information_group_length`][2]. +/// +/// [1]: http://dicom.nema.org/medical/dicom/current/output/chtml/part06/chapter_7.html +/// [2]: FileMetaTable::update_information_group_length +#[derive(Debug, Clone, PartialEq)] +pub struct FileMetaTable { + /// File Meta Information Group Length + pub information_group_length: u32, + /// File Meta Information Version + pub information_version: [u8; 2], + /// Media Storage SOP Class UID + pub media_storage_sop_class_uid: String, + /// Media Storage SOP Instance UID + pub media_storage_sop_instance_uid: String, + /// Transfer Syntax UID + pub transfer_syntax: String, + /// Implementation Class UID + pub implementation_class_uid: String, + + /// Implementation Version Name + pub implementation_version_name: Option, + /// Source Application Entity Title + pub source_application_entity_title: Option, + /// Sending Application Entity Title + pub sending_application_entity_title: Option, + /// Receiving Application Entity Title + pub receiving_application_entity_title: Option, + /// Private Information Creator UID + pub private_information_creator_uid: Option, + /// Private Information + pub private_information: Option>, + /* + Missing attributes: + + (0002,0026) Source Presentation Address Source​Presentation​Address UR 1 + (0002,0027) Sending Presentation Address Sending​Presentation​Address UR 1 + (0002,0028) Receiving Presentation Address Receiving​Presentation​Address UR 1 + (0002,0031) RTV Meta Information Version RTV​Meta​Information​Version OB 1 + (0002,0032) RTV Communication SOP Class UID RTV​Communication​SOP​Class​UID UI 1 + (0002,0033) RTV Communication SOP Instance UID RTV​Communication​SOP​Instance​UID UI 1 + (0002,0035) RTV Source Identifier RTV​Source​Identifier OB 1 + (0002,0036) RTV Flow Identifier RTV​Flow​Identifier OB 1 + (0002,0037) RTV Flow RTP Sampling Rate RTV​Flow​RTP​Sampling​Rate UL 1 + (0002,0038) RTV Flow Actual Frame Duration RTV​Flow​Actual​Frame​Duration FD 1 + */ +} + +/// Utility function for reading the body of the DICOM element as a UID. +fn read_str_body<'s, S, T>(source: &'s mut S, text: &T, len: u32) -> Result +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 { + /// Construct a file meta group table + /// by parsing a DICOM data set from a reader. + /// + /// This method fails if the first four bytes + /// are not the DICOM magic code `DICM`. + pub fn from_reader(file: R) -> Result { + FileMetaTable::read_from(file) + } + + /// Getter for the transfer syntax UID, + /// with trailing characters already excluded. + pub fn transfer_syntax(&self) -> &str { + self.transfer_syntax + .trim_end_matches(|c: char| c.is_whitespace() || c == '\0') + } + + /// Getter for the media storage SOP instance UID, + /// with trailing characters already excluded. + 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') + } + + /// Getter for the media storage SOP class UID, + /// with trailing characters already excluded. + 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') + } + + /// Getter for the implementation class UID, + /// with trailing characters already excluded. + pub fn implementation_class_uid(&self) -> &str { + self.implementation_class_uid + .trim_end_matches(|c: char| c.is_whitespace() || c == '\0') + } + + /// Getter for the private information creator UID, + /// with trailing characters already excluded. + 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')) + } + + /// Set the file meta table's transfer syntax + /// according to the given transfer syntax descriptor. + /// + /// This replaces the table's transfer syntax UID + /// to the given transfer syntax, without padding to even length. + /// The information group length field is automatically recalculated. + pub fn set_transfer_syntax(&mut self, ts: &TransferSyntax) { + self.transfer_syntax = ts + .uid() + .trim_end_matches(|c: char| c.is_whitespace() || c == '\0') + .to_string(); + self.update_information_group_length(); + } + + /// Calculate the expected file meta group length + /// according to the file meta attributes currently set, + /// and assign it to the field `information_group_length`. + pub fn update_information_group_length(&mut self) { + self.information_group_length = self.calculate_information_group_length(); + } + + /// Apply the given attribute operation on this file meta information table. + /// + /// See the [`dicom_core::ops`] module + /// for more information. + 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(_) + ) => + { + // any other attribute is not supported + // (ignore Remove, Empty, 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(_) => { + // ignore + Ok(()) + } + AttributeAction::Set(value) | AttributeAction::Replace(value) => { + // require value to be textual + 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(_) => { + // no-op + 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, + ) -> 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(_) => { + // ignore + Ok(()) + } + AttributeAction::Set(value) => { + // require value to be textual + 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(()); + } + + // require value to be textual + 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(()); + } + + // require value to be textual + 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(), + } + } + + /// Calculate the expected file meta group length, + /// ignoring `information_group_length`. + fn calculate_information_group_length(&self) -> u32 { + // determine the expected meta group size based on the given fields. + // attribute FileMetaInformationGroupLength is not included + // in the calculations intentionally + 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) + } + + /// Read the DICOM magic code (`b"DICM"`) + /// and the whole file meta group from the given reader. + fn read_from(mut file: S) -> Result { + let mut buff: [u8; 4] = [0; 4]; + { + // check magic code + 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); + + // Fetch optional data elements + 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) => { + // Implementation Version + 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) + } + // Media Storage SOP Class UID + Tag(0x0002, 0x0002) => { + builder.media_storage_sop_class_uid(read_str_body(&mut file, &text, elem_len)?) + } + // Media Storage SOP Instance UID + Tag(0x0002, 0x0003) => builder + .media_storage_sop_instance_uid(read_str_body(&mut file, &text, elem_len)?), + // Transfer Syntax + Tag(0x0002, 0x0010) => { + builder.transfer_syntax(read_str_body(&mut file, &text, elem_len)?) + } + // Implementation Class UID + Tag(0x0002, 0x0012) => { + builder.implementation_class_uid(read_str_body(&mut file, &text, elem_len)?) + } + Tag(0x0002, 0x0013) => { + // Implementation Version Name + 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) => { + // Source Application Entity Title + 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) => { + // Sending Application Entity Title + 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) => { + // Receiving Application Entity Title + 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) => { + // Private Information Creator UID + 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) => { + // Private Information + 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, _) => { + // unknown tag, do nothing + // could be an unsupported or non-standard attribute + tracing::info!("Unknown tag {}", tag); + // consume value without saving it + 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 { + // reported element length longer than actual stream + return UnexpectedDataValueLengthSnafu { + tag: elem.tag(), + length: elem_len, + } + .fail(); + } + builder + } + tag => { + // unexpected tag from another group! do nothing for now, + // but this could pose an issue up ahead (see #50) + tracing::warn!("Unexpected off-group tag {}", tag); + // consume value without saving it + 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 { + // reported element length longer than actual stream + 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() + } + + /// Create an iterator over the defined data elements + /// of the file meta group, + /// consuming the file meta table. + /// + /// See [`to_element_iter`](FileMetaTable::to_element_iter) + /// for a version which copies the element from the table. + pub fn into_element_iter(self) -> impl Iterator> { + let mut elems = vec![ + // file information group length + 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() + } + + /// Create an iterator of data elements copied from the file meta group. + /// + /// See [`into_element_iter`](FileMetaTable::into_element_iter) + /// for a version which consumes the table. + pub fn to_element_iter(&self) -> impl Iterator> + '_ { + self.clone().into_element_iter() + } + + pub fn write(&self, writer: W) -> Result<()> { + let mut dset = DataSetWriter::new( + writer, + EncoderFor::new(ExplicitVRLittleEndianEncoder::default()), + ); + //There are no sequences in the `FileMetaTable`, so the value of `invalidate_sq_len` is + //not important + dset.write_sequence( + self.clone() + .into_element_iter() + .flat_map(IntoTokens::into_tokens), + ) + .context(WriteSetSnafu)?; + + dset.flush().context(WriteSetSnafu) + } +} + +/// An attribute selector for a file meta information table. +#[derive(Debug)] +pub struct FileMetaAttribute<'a> { + meta: &'a FileMetaTable, + tag_e: u16, +} + +impl HasLength for FileMetaAttribute<'_> { + fn length(&self) -> Length { + match Tag(0x0002, self.tag_e) { + tags::FILE_META_INFORMATION_GROUP_LENGTH => Length(4), + tags::MEDIA_STORAGE_SOP_CLASS_UID => { + Length(self.meta.media_storage_sop_class_uid.len() as u32) + } + tags::MEDIA_STORAGE_SOP_INSTANCE_UID => { + Length(self.meta.media_storage_sop_instance_uid.len() as u32) + } + tags::IMPLEMENTATION_CLASS_UID => { + Length(self.meta.implementation_class_uid.len() as u32) + } + tags::IMPLEMENTATION_VERSION_NAME => Length( + self.meta + .implementation_version_name + .as_ref() + .map(|s| s.len() as u32) + .unwrap_or(0), + ), + tags::SOURCE_APPLICATION_ENTITY_TITLE => Length( + self.meta + .source_application_entity_title + .as_ref() + .map(|s| s.len() as u32) + .unwrap_or(0), + ), + tags::SENDING_APPLICATION_ENTITY_TITLE => Length( + self.meta + .sending_application_entity_title + .as_ref() + .map(|s| s.len() as u32) + .unwrap_or(0), + ), + tags::TRANSFER_SYNTAX_UID => Length(self.meta.transfer_syntax.len() as u32), + tags::PRIVATE_INFORMATION_CREATOR_UID => Length( + self.meta + .private_information_creator_uid + .as_ref() + .map(|s| s.len() as u32) + .unwrap_or(0), + ), + _ => unreachable!(), + } + } +} + +impl DicomValueType for FileMetaAttribute<'_> { + fn value_type(&self) -> ValueType { + match Tag(0x0002, self.tag_e) { + tags::MEDIA_STORAGE_SOP_CLASS_UID + | tags::MEDIA_STORAGE_SOP_INSTANCE_UID + | tags::TRANSFER_SYNTAX_UID + | tags::IMPLEMENTATION_CLASS_UID + | tags::IMPLEMENTATION_VERSION_NAME + | tags::SOURCE_APPLICATION_ENTITY_TITLE + | tags::SENDING_APPLICATION_ENTITY_TITLE + | tags::RECEIVING_APPLICATION_ENTITY_TITLE + | tags::PRIVATE_INFORMATION_CREATOR_UID => ValueType::Str, + tags::FILE_META_INFORMATION_GROUP_LENGTH => ValueType::U32, + tags::FILE_META_INFORMATION_VERSION => ValueType::U8, + tags::PRIVATE_INFORMATION => ValueType::U8, + _ => unreachable!(), + } + } + + fn cardinality(&self) -> usize { + match Tag(0x0002, self.tag_e) { + tags::MEDIA_STORAGE_SOP_CLASS_UID + | tags::MEDIA_STORAGE_SOP_INSTANCE_UID + | tags::SOURCE_APPLICATION_ENTITY_TITLE + | tags::SENDING_APPLICATION_ENTITY_TITLE + | tags::RECEIVING_APPLICATION_ENTITY_TITLE + | tags::TRANSFER_SYNTAX_UID + | tags::IMPLEMENTATION_CLASS_UID + | tags::IMPLEMENTATION_VERSION_NAME + | tags::PRIVATE_INFORMATION_CREATOR_UID => 1, + tags::FILE_META_INFORMATION_GROUP_LENGTH => 1, + tags::PRIVATE_INFORMATION => 1, + tags::FILE_META_INFORMATION_VERSION => 2, + _ => 1, + } + } +} + +impl DicomAttribute for FileMetaAttribute<'_> { + type Item<'b> + = EmptyObject + where + Self: 'b; + type PixelData<'b> + = InMemFragment + where + Self: 'b; + + fn to_primitive_value(&self) -> Result { + Ok(match Tag(0x0002, self.tag_e) { + tags::FILE_META_INFORMATION_GROUP_LENGTH => { + PrimitiveValue::from(self.meta.information_group_length) + } + tags::FILE_META_INFORMATION_VERSION => { + PrimitiveValue::from(self.meta.information_version) + } + tags::MEDIA_STORAGE_SOP_CLASS_UID => { + PrimitiveValue::from(self.meta.media_storage_sop_class_uid.clone()) + } + tags::MEDIA_STORAGE_SOP_INSTANCE_UID => { + PrimitiveValue::from(self.meta.media_storage_sop_instance_uid.clone()) + } + tags::SOURCE_APPLICATION_ENTITY_TITLE => { + PrimitiveValue::from(self.meta.source_application_entity_title.clone().unwrap()) + } + tags::SENDING_APPLICATION_ENTITY_TITLE => { + PrimitiveValue::from(self.meta.sending_application_entity_title.clone().unwrap()) + } + tags::RECEIVING_APPLICATION_ENTITY_TITLE => PrimitiveValue::from( + self.meta + .receiving_application_entity_title + .clone() + .unwrap(), + ), + tags::TRANSFER_SYNTAX_UID => PrimitiveValue::from(self.meta.transfer_syntax.clone()), + tags::IMPLEMENTATION_CLASS_UID => { + PrimitiveValue::from(self.meta.implementation_class_uid.clone()) + } + tags::IMPLEMENTATION_VERSION_NAME => { + PrimitiveValue::from(self.meta.implementation_version_name.clone().unwrap()) + } + tags::PRIVATE_INFORMATION_CREATOR_UID => { + PrimitiveValue::from(self.meta.private_information_creator_uid.clone().unwrap()) + } + tags::PRIVATE_INFORMATION => { + PrimitiveValue::from(self.meta.private_information.clone().unwrap()) + } + _ => unreachable!(), + }) + } + + fn to_str(&self) -> std::result::Result, AttributeError> { + match Tag(0x0002, self.tag_e) { + tags::FILE_META_INFORMATION_GROUP_LENGTH => { + Ok(self.meta.information_group_length.to_string().into()) + } + tags::FILE_META_INFORMATION_VERSION => Ok(format!( + "{:02X}{:02X}", + self.meta.information_version[0], self.meta.information_version[1] + ) + .into()), + tags::MEDIA_STORAGE_SOP_CLASS_UID => { + Ok(Cow::Borrowed(self.meta.media_storage_sop_class_uid())) + } + tags::MEDIA_STORAGE_SOP_INSTANCE_UID => { + Ok(Cow::Borrowed(self.meta.media_storage_sop_instance_uid())) + } + tags::TRANSFER_SYNTAX_UID => Ok(Cow::Borrowed(self.meta.transfer_syntax())), + tags::IMPLEMENTATION_CLASS_UID => { + Ok(Cow::Borrowed(self.meta.implementation_class_uid())) + } + tags::IMPLEMENTATION_VERSION_NAME => Ok(self + .meta + .implementation_version_name + .as_deref() + .map(Cow::Borrowed) + .unwrap_or_default()), + tags::SOURCE_APPLICATION_ENTITY_TITLE => Ok(self + .meta + .source_application_entity_title + .as_deref() + .map(Cow::Borrowed) + .unwrap_or_default()), + tags::SENDING_APPLICATION_ENTITY_TITLE => Ok(self + .meta + .sending_application_entity_title + .as_deref() + .map(Cow::Borrowed) + .unwrap_or_default()), + tags::RECEIVING_APPLICATION_ENTITY_TITLE => Ok(self + .meta + .receiving_application_entity_title + .as_deref() + .map(Cow::Borrowed) + .unwrap_or_default()), + tags::PRIVATE_INFORMATION_CREATOR_UID => Ok(self + .meta + .private_information_creator_uid + .as_deref() + .map(|v| { + Cow::Borrowed(v.trim_end_matches(|c: char| c.is_whitespace() || c == '\0')) + }) + .unwrap_or_default()), + tags::PRIVATE_INFORMATION => Err(AttributeError::ConvertValue { + source: ConvertValueError { + cause: None, + original: ValueType::U8, + requested: "str", + }, + }), + _ => unreachable!(), + } + } + + fn item(&self, _index: u32) -> Result, AttributeError> { + Err(AttributeError::NotDataSet) + } + + fn num_items(&self) -> Option { + None + } + + fn fragment(&self, _index: u32) -> Result, AttributeError> { + Err(AttributeError::NotPixelData) + } + + fn num_fragments(&self) -> Option { + None + } +} + +impl DicomObject for FileMetaTable { + type Attribute<'a> + = FileMetaAttribute<'a> + where + Self: 'a; + + type LeafAttribute<'a> + = FileMetaAttribute<'a> + where + Self: 'a; + + fn attr_opt( + &self, + tag: Tag, + ) -> std::result::Result>, crate::AccessError> { + // check that the attribute value is in the table, + // then return a suitable `FileMetaAttribute` + + if match tag { + // mandatory attributes + tags::FILE_META_INFORMATION_GROUP_LENGTH + | tags::FILE_META_INFORMATION_VERSION + | tags::MEDIA_STORAGE_SOP_CLASS_UID + | tags::MEDIA_STORAGE_SOP_INSTANCE_UID + | tags::TRANSFER_SYNTAX_UID + | tags::IMPLEMENTATION_CLASS_UID + | tags::IMPLEMENTATION_VERSION_NAME => true, + // optional attributes + tags::SOURCE_APPLICATION_ENTITY_TITLE + if self.source_application_entity_title.is_some() => + { + true + } + tags::SENDING_APPLICATION_ENTITY_TITLE + if self.sending_application_entity_title.is_some() => + { + true + } + tags::RECEIVING_APPLICATION_ENTITY_TITLE + if self.receiving_application_entity_title.is_some() => + { + true + } + tags::PRIVATE_INFORMATION_CREATOR_UID + if self.private_information_creator_uid.is_some() => + { + true + } + tags::PRIVATE_INFORMATION if self.private_information.is_some() => true, + _ => false, + } { + Ok(Some(FileMetaAttribute { + meta: self, + tag_e: tag.element(), + })) + } else { + Ok(None) + } + } + + fn attr_by_name_opt<'a>( + &'a self, + name: &str, + ) -> std::result::Result>, crate::AccessByNameError> { + let tag = match name { + "FileMetaInformationGroupLength" => tags::FILE_META_INFORMATION_GROUP_LENGTH, + "FileMetaInformationVersion" => tags::FILE_META_INFORMATION_VERSION, + "MediaStorageSOPClassUID" => tags::MEDIA_STORAGE_SOP_CLASS_UID, + "MediaStorageSOPInstanceUID" => tags::MEDIA_STORAGE_SOP_INSTANCE_UID, + "TransferSyntaxUID" => tags::TRANSFER_SYNTAX_UID, + "ImplementationClassUID" => tags::IMPLEMENTATION_CLASS_UID, + "ImplementationVersionName" => tags::IMPLEMENTATION_VERSION_NAME, + "SourceApplicationEntityTitle" => tags::SOURCE_APPLICATION_ENTITY_TITLE, + "SendingApplicationEntityTitle" => tags::SENDING_APPLICATION_ENTITY_TITLE, + "ReceivingApplicationEntityTitle" => tags::RECEIVING_APPLICATION_ENTITY_TITLE, + "PrivateInformationCreatorUID" => tags::PRIVATE_INFORMATION_CREATOR_UID, + "PrivateInformation" => tags::PRIVATE_INFORMATION, + _ => return Ok(None), + }; + self.attr_opt(tag) + .map_err(|_| crate::NoSuchAttributeNameSnafu { name }.build()) + } + + fn at( + &self, + selector: impl Into, + ) -> Result, crate::AtAccessError> { + let selector: AttributeSelector = selector.into(); + match selector.split_first() { + (AttributeSelectorStep::Tag(tag), None) => self + .attr(tag) + .map_err(|_| AtAccessError::MissingLeafElement { selector }), + (_, Some(_)) => crate::NotASequenceSnafu { + selector, + step_index: 0_u32, + } + .fail(), + (_, None) => unreachable!("broken invariant: nested step at end of selector"), + } + } +} + +impl ApplyOp for FileMetaTable { + type Err = ApplyError; + + /// Apply the given attribute operation on this file meta information table. + /// + /// See the [`dicom_core::ops`] module + /// for more information. + fn apply(&mut self, op: AttributeOp) -> ApplyResult { + self.apply(op) + } +} + +/// A builder for DICOM meta information tables. +#[derive(Debug, Default, Clone)] +pub struct FileMetaTableBuilder { + /// File Meta Information Group Length (UL) + information_group_length: Option, + /// File Meta Information Version (OB) + information_version: Option<[u8; 2]>, + /// Media Storage SOP Class UID (UI) + media_storage_sop_class_uid: Option, + /// Media Storage SOP Instance UID (UI) + media_storage_sop_instance_uid: Option, + /// Transfer Syntax UID (UI) + transfer_syntax: Option, + /// Implementation Class UID (UI) + implementation_class_uid: Option, + + /// Implementation Version Name (SH) + implementation_version_name: Option, + /// Source Application Entity Title (AE) + source_application_entity_title: Option, + /// Sending Application Entity Title (AE) + sending_application_entity_title: Option, + /// Receiving Application Entity Title (AE) + receiving_application_entity_title: Option, + /// Private Information Creator UID (UI) + private_information_creator_uid: Option, + /// Private Information (OB) + private_information: Option>, +} + +/// Ensure that the string is even lengthed, by adding a trailing character +/// if not. +#[inline] +fn padded(s: T, pad: char) -> String +where + T: Into, +{ + let mut s = s.into(); + if s.len() % 2 == 1 { + s.push(pad); + } + s +} + +/// Ensure that the string is even lengthed with trailing '\0's. +fn ui_padded(s: T) -> String +where + T: Into, +{ + padded(s, '\0') +} + +/// Ensure that the string is even lengthed with trailing spaces. +fn txt_padded(s: T) -> String +where + T: Into, +{ + padded(s, ' ') +} + +impl FileMetaTableBuilder { + /// Create a new, empty builder. + pub fn new() -> FileMetaTableBuilder { + FileMetaTableBuilder::default() + } + + /// Define the meta information group length. + pub fn group_length(mut self, value: u32) -> FileMetaTableBuilder { + self.information_group_length = Some(value); + self + } + + /// Define the meta information version. + pub fn information_version(mut self, value: [u8; 2]) -> FileMetaTableBuilder { + self.information_version = Some(value); + self + } + + /// Define the media storage SOP class UID. + pub fn media_storage_sop_class_uid(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.media_storage_sop_class_uid = Some(ui_padded(value)); + self + } + + /// Define the media storage SOP instance UID. + pub fn media_storage_sop_instance_uid(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.media_storage_sop_instance_uid = Some(ui_padded(value)); + self + } + + /// Define the transfer syntax UID. + pub fn transfer_syntax(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.transfer_syntax = Some(ui_padded(value)); + self + } + + /// Define the implementation class UID. + pub fn implementation_class_uid(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.implementation_class_uid = Some(ui_padded(value)); + self + } + + /// Define the implementation version name. + pub fn implementation_version_name(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.implementation_version_name = Some(txt_padded(value)); + self + } + + /// Define the source application entity title. + pub fn source_application_entity_title(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.source_application_entity_title = Some(txt_padded(value)); + self + } + + /// Define the sending application entity title. + pub fn sending_application_entity_title(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.sending_application_entity_title = Some(txt_padded(value)); + self + } + + /// Define the receiving application entity title. + pub fn receiving_application_entity_title(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.receiving_application_entity_title = Some(txt_padded(value)); + self + } + + /// Define the private information creator UID. + pub fn private_information_creator_uid(mut self, value: T) -> FileMetaTableBuilder + where + T: Into, + { + self.private_information_creator_uid = Some(ui_padded(value)); + self + } + + /// Define the private information as a vector of bytes. + pub fn private_information(mut self, value: T) -> FileMetaTableBuilder + where + T: Into>, + { + self.private_information = Some(value.into()); + self + } + + /// Build the table. + pub fn build(self) -> Result { + let information_version = self.information_version.unwrap_or( + // Missing information version, will assume (00H, 01H). See #28 + [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(|| { + // override implementation version name + implementation_version_name = Some(IMPLEMENTATION_VERSION_NAME.to_string()); + + IMPLEMENTATION_CLASS_UID.to_string() + }); + + let mut table = FileMetaTable { + // placeholder value which will be replaced on update + 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>(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: &[u8] = &[ + // magic code + b'D', b'I', b'C', b'M', + // File Meta Information Group Length: (0000,0002) ; UL ; 4 ; 200 + 0x02, 0x00, 0x00, 0x00, b'U', b'L', 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, + // File Meta Information Version: (0002, 0001) ; OB ; 2 ; [0x00, 0x01] + 0x02, 0x00, 0x01, 0x00, b'O', b'B', 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, + // Media Storage SOP Class UID (0002, 0002) ; UI ; 26 ; "1.2.840.10008.5.1.4.1.1.1\0" (ComputedRadiographyImageStorage) + 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, + // Media Storage SOP Instance UID (0002, 0003) ; UI ; 56 ; "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0" + 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, + // Transfer Syntax UID (0002, 0010) ; UI ; 20 ; "1.2.840.10008.1.2.1\0" (LittleEndianExplicit) + 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, + // Implementation Class UID (0002, 0012) ; UI ; 20 ; "1.2.345.6.7890.1.234" + 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, + // optional elements: + + // Implementation Version Name (0002,0013) ; SH ; "RUSTY_DICOM_269" + 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, + // Source Application Entity Title (0002, 0016) ; AE ; 0 (no data) + 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); + } + + /// Build a file meta table with the minimum set of parameters. + #[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); + } + + /// Changing the transfer syntax updates the file meta group length. + #[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![ + // Information Group Length + DataElement::new(Tag(0x0002, 0x0000), VR::UL, dicom_value!(U32, 200)), + // Information Version + DataElement::new(Tag(0x0002, 0x0001), VR::OB, dicom_value!(U8, [0, 1])), + // Media Storage SOP Class UID + DataElement::new( + Tag(0x0002, 0x0002), + VR::UI, + Value::Primitive("1.2.840.10008.5.1.4.1.1.1\0".into()), + ), + // Media Storage SOP Instance UID + DataElement::new( + Tag(0x0002, 0x0003), + VR::UI, + Value::Primitive( + "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".into(), + ), + ), + // Transfer Syntax + DataElement::new( + Tag(0x0002, 0x0010), + VR::UI, + Value::Primitive("1.2.840.10008.1.2.1\0".into()), + ), + // Implementation Class UID + DataElement::new( + Tag(0x0002, 0x0012), + VR::UI, + Value::Primitive("1.2.345.6.7890.1.234".into()), + ), + // Implementation Version Name + DataElement::new( + Tag(0x0002, 0x0013), + VR::SH, + Value::Primitive("RUSTY_DICOM_269 ".into()), + ), + // Source Application Entity Title + 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, // dummy value + 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, + }; + + // replace does not set missing attributes + table + .apply(AttributeOp::new( + tags::IMPLEMENTATION_VERSION_NAME, + AttributeAction::ReplaceStr("MY_DICOM_1.1".into()), + )) + .unwrap(); + + assert_eq!(table.implementation_version_name, None); + + // but SetStr does + 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"), + ); + + // Set (primitive) also works + 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"), + ); + + // set if missing works only if value isn't set yet + 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"), + ); + + // replacing mandatory field + 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", + ); + } + + /// writing file meta information and reading it back + /// should not fail and the the group length should be the same + #[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 + ); + } + + /// Can access file meta properties via the DicomObject trait + #[test] + fn dicom_object_api() { + use crate::{DicomAttribute as _, DicomObject as _}; + use dicom_dictionary_std::uids; + + let meta = FileMetaTableBuilder::new() + .transfer_syntax(uids::RLE_LOSSLESS) + .media_storage_sop_class_uid(uids::ENHANCED_MR_IMAGE_STORAGE) + .media_storage_sop_instance_uid("2.25.94766187067244888884745908966163363746") + .implementation_version_name("RUSTY_DICOM_269") + .build() + .unwrap(); + + assert_eq!( + meta.attr(tags::TRANSFER_SYNTAX_UID) + .unwrap() + .to_str() + .unwrap(), + uids::RLE_LOSSLESS + ); + + let sop_class_uid = meta.attr_opt(tags::MEDIA_STORAGE_SOP_CLASS_UID).unwrap(); + let sop_class_uid = sop_class_uid.as_ref().map(|v| v.to_str().unwrap()); + assert_eq!( + sop_class_uid.as_deref(), + Some(uids::ENHANCED_MR_IMAGE_STORAGE) + ); + + assert_eq!( + meta.attr_by_name("MediaStorageSOPInstanceUID") + .unwrap() + .to_str() + .unwrap(), + "2.25.94766187067244888884745908966163363746" + ); + + assert!(meta.attr_opt(tags::PRIVATE_INFORMATION).unwrap().is_none()); + } +} From 9a27570fc52a5a9542184e264691f404a1cfac3f Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 13:22:48 +0200 Subject: [PATCH 04/12] Implement DicomDateTime --- core/src/value/partial.rs | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index e0ea86f0..851cb9d2 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -817,6 +817,17 @@ impl DicomDateTime { } } + // Constructs a new `DicomDate` now from the local timezone + #[cfg(feature = "now")] + pub fn now_local() -> Result { + return DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?); + } + #[cfg(feature = "now")] + // Constructs a new `DicomDate` now from the utc timezone + pub fn now_utc() -> Result { + return DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?); + } + /** Retrieves a reference to the internal date value */ pub fn date(&self) -> &DicomDate { &self.date @@ -1444,12 +1455,24 @@ mod tests { assert_eq!(time.fraction_str(), frac.to_string()); } // test fraction retrieval: without a fraction, it returns None - assert_eq!( - DicomTime::from_hms(9, 1, 1) - .unwrap() - .fraction_micro(), - None - ); + assert_eq!(DicomTime::from_hms(9, 1, 1).unwrap().fraction_micro(), None); + } + + #[test] + #[cfg(feature = "now")] + fn test_dicom_now() { + let time_now_local = DicomTime::now_local(); + println!("asdf time_now_local {:?}", time_now_local); + let time_now_utc = DicomTime::now_utc(); + println!("asdf time_now_utc {:?}", time_now_utc); + let date_now_local = DicomTime::now_local(); + println!("asdf date_now_local {:?}", date_now_local); + let date_now_utc = DicomTime::now_utc(); + println!("asdf date_now_utc {:?}", date_now_utc); + let date_time_now_local = DicomDateTime::now_local(); + println!("asdf date_time_now_local {:?}", date_now_local); + let date_time_now_utc = DicomDateTime::now_utc(); + println!("asdf date_time_now_utc {:?}", date_now_utc); } #[test] From fb0b8f85be2a8aa37d1e8380ef63d5aa95fc835b Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 13:24:33 +0200 Subject: [PATCH 05/12] Fix formatting --- core/src/value/partial.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 851cb9d2..59b406a5 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1455,7 +1455,12 @@ mod tests { assert_eq!(time.fraction_str(), frac.to_string()); } // test fraction retrieval: without a fraction, it returns None - assert_eq!(DicomTime::from_hms(9, 1, 1).unwrap().fraction_micro(), None); + assert_eq!( + DicomTime::from_hms(9, 1, 1) + .unwrap() + .fraction_micro(), + None + ); } #[test] From 3d77ac3e332593bff2792646d9aa8ca09fcf3bf8 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 13:26:16 +0200 Subject: [PATCH 06/12] Fix comments --- core/src/value/partial.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 59b406a5..d4e951b3 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -464,12 +464,12 @@ impl DicomTime { ))) } - // Constructs a new `DicomDate` now from the local timezone + // Constructs a new `DicomTime` now from the local timezone #[cfg(feature = "now")] pub fn now_local() -> Result { return DicomTime::try_from(&Local::now().naive_local().time()); } - // Constructs a new `DicomDate` now from the utc timezone + // Constructs a new `DicomTime` now from the utc timezone #[cfg(feature = "now")] pub fn now_utc() -> Result { return DicomTime::try_from(&Utc::now().naive_utc().time()); @@ -817,13 +817,13 @@ impl DicomDateTime { } } - // Constructs a new `DicomDate` now from the local timezone + // Constructs a new `DicomDateTime` now from the local timezone #[cfg(feature = "now")] pub fn now_local() -> Result { return DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?); } #[cfg(feature = "now")] - // Constructs a new `DicomDate` now from the utc timezone + // Constructs a new `DicomDateTime` now from the utc timezone pub fn now_utc() -> Result { return DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?); } From 3d82a3812cd006e826a4f1093cfbd5d374f06d97 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 22:16:18 +0200 Subject: [PATCH 07/12] Add PR feedback --- core/Cargo.toml | 3 --- core/src/value/partial.rs | 16 +++++----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 2b365852..3b5a672e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,6 +18,3 @@ num-traits = "0.2.12" safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.8" - -[features] -now = [] diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index d4e951b3..5fb835a6 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1,9 +1,9 @@ //! Handling of partial precision of Date, Time and DateTime values. use crate::value::AsRange; -use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; -#[cfg(feature = "now")] -use chrono::{Local, Utc}; +use chrono::{ + DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc, +}; use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; use std::fmt; @@ -310,12 +310,10 @@ impl DicomDate { } // Constructs a new `DicomDate` now from the local timezone - #[cfg(feature = "now")] pub fn now_local() -> Result { return DicomDate::try_from(&Local::now().date_naive()); } // Constructs a new `DicomDate` now from the utc timezone - #[cfg(feature = "now")] pub fn now_utc() -> Result { return DicomDate::try_from(&Utc::now().date_naive()); } @@ -465,12 +463,10 @@ impl DicomTime { } // Constructs a new `DicomTime` now from the local timezone - #[cfg(feature = "now")] pub fn now_local() -> Result { return DicomTime::try_from(&Local::now().naive_local().time()); } // Constructs a new `DicomTime` now from the utc timezone - #[cfg(feature = "now")] pub fn now_utc() -> Result { return DicomTime::try_from(&Utc::now().naive_utc().time()); } @@ -818,11 +814,9 @@ impl DicomDateTime { } // Constructs a new `DicomDateTime` now from the local timezone - #[cfg(feature = "now")] pub fn now_local() -> Result { return DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?); } - #[cfg(feature = "now")] // Constructs a new `DicomDateTime` now from the utc timezone pub fn now_utc() -> Result { return DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?); @@ -1470,9 +1464,9 @@ mod tests { println!("asdf time_now_local {:?}", time_now_local); let time_now_utc = DicomTime::now_utc(); println!("asdf time_now_utc {:?}", time_now_utc); - let date_now_local = DicomTime::now_local(); + let date_now_local = DicomDate::now_local(); println!("asdf date_now_local {:?}", date_now_local); - let date_now_utc = DicomTime::now_utc(); + let date_now_utc = DicomDate::now_utc(); println!("asdf date_now_utc {:?}", date_now_utc); let date_time_now_local = DicomDateTime::now_local(); println!("asdf date_time_now_local {:?}", date_now_local); From 09761ad897c77cebe29b7fc14ba37eded48c656a Mon Sep 17 00:00:00 2001 From: cryt1c Date: Sat, 4 Oct 2025 22:20:16 +0200 Subject: [PATCH 08/12] Fix linting --- core/src/value/partial.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 5fb835a6..3c079910 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1362,12 +1362,12 @@ mod tests { ); // specific date-time from chrono - let date_time: DateTime<_> = DateTime::::from_naive_utc_and_offset( + let date_time: DateTime<_> = DateTime::::from_naive_utc_and_offset( NaiveDateTime::new( NaiveDate::from_ymd_opt(2024, 8, 9).unwrap(), NaiveTime::from_hms_opt(9, 9, 39).unwrap(), ), - chrono::Utc, + Utc, ).with_timezone(&FixedOffset::east_opt(0).unwrap()); let dicom_date_time = DicomDateTime::try_from(&date_time).unwrap(); assert!(dicom_date_time.has_time_zone()); @@ -1458,7 +1458,6 @@ mod tests { } #[test] - #[cfg(feature = "now")] fn test_dicom_now() { let time_now_local = DicomTime::now_local(); println!("asdf time_now_local {:?}", time_now_local); From 99bfce336ce5fd810b3b4fb8e64be51525d5d6ff Mon Sep 17 00:00:00 2001 From: cryt1c Date: Wed, 15 Oct 2025 20:53:13 +0200 Subject: [PATCH 09/12] Implement tests --- core/src/value/partial.rs | 133 ++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 2e039b77..a50eab12 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1124,7 +1124,7 @@ impl PartialOrd for PreciseDateTime { #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; + use chrono::{Duration, TimeZone}; #[test] fn test_dicom_date() { @@ -1449,19 +1449,124 @@ mod tests { } #[test] - fn test_dicom_now() { - let time_now_local = DicomTime::now_local(); - println!("asdf time_now_local {:?}", time_now_local); - let time_now_utc = DicomTime::now_utc(); - println!("asdf time_now_utc {:?}", time_now_utc); - let date_now_local = DicomDate::now_local(); - println!("asdf date_now_local {:?}", date_now_local); - let date_now_utc = DicomDate::now_utc(); - println!("asdf date_now_utc {:?}", date_now_utc); - let date_time_now_local = DicomDateTime::now_local(); - println!("asdf date_time_now_local {:?}", date_now_local); - let date_time_now_utc = DicomDateTime::now_utc(); - println!("asdf date_time_now_utc {:?}", date_now_utc); + fn test_dicom_time_now_local() { + // Test DicomTime::now_local() + let dicom_time = DicomTime::now_local() + .expect("Failed to get current local time from DicomTime::now_local()"); + let system_time = Local::now().naive_local().time(); + let dicom_naive_time = dicom_time + .to_naive_time() + .expect("Failed to convert DicomTime to NaiveTime"); + let time_difference = system_time - dicom_naive_time; + assert!( + time_difference.abs() < Duration::seconds(1), + "Time difference between system and DICOM time exceeds 1 second: {:?}", + time_difference + ); + } + + #[test] + fn test_dicom_time_now_utc() { + let dicom_time_utc = + DicomTime::now_utc().expect("Failed to get current UTC time from DicomTime::now_utc()"); + let system_time_utc = Utc::now().naive_utc().time(); + let dicom_naive_time_utc = dicom_time_utc + .to_naive_time() + .expect("Failed to convert DicomTime (UTC) to NaiveTime"); + let time_difference_utc = system_time_utc - dicom_naive_time_utc; + assert!( + time_difference_utc.abs() < Duration::seconds(1), + "Time difference between system and DICOM UTC time exceeds 1 second: {:?}", + time_difference_utc + ); + } + + #[test] + fn test_dicom_date_now_local() { + let dicom_date_local = DicomDate::now_local() + .expect("Failed to get current local date from DicomDate::now_local()"); + let system_date_local = Local::now().naive_local().date(); + let dicom_naive_date_local = dicom_date_local + .to_naive_date() + .expect("Failed to convert DicomDate (local) to NaiveDate"); + assert_eq!( + dicom_naive_date_local, system_date_local, + "Local date mismatch between DicomDate and system" + ); + } + + #[test] + fn test_dicom_date_now_utc() { + let dicom_date_utc = + DicomDate::now_utc().expect("Failed to get current UTC date from DicomDate::now_utc()"); + let system_date_utc = Utc::now().naive_utc().date(); + let dicom_naive_date_utc = dicom_date_utc + .to_naive_date() + .expect("Failed to convert DicomDate (UTC) to NaiveDate"); + assert_eq!( + dicom_naive_date_utc, system_date_utc, + "UTC date mismatch between DicomDate and system" + ); + } + + #[test] + fn test_dicom_date_time_now_local() { + let dicom_datetime_local = DicomDateTime::now_local() + .expect("Failed to get current local datetime from DicomDateTime::now_local()"); + let dicom_datetime_local_time = dicom_datetime_local + .time() + .expect("Failed to get time from DicomDateTime"); + let dicom_datetime_local_date = dicom_datetime_local.date(); + + let system_time_local = Local::now().naive_local().time(); + let dicom_naive_time_local = dicom_datetime_local_time + .to_naive_time() + .expect("Failed to convert DicomDateTime time component to NaiveTime"); + let time_difference_local = system_time_local - dicom_naive_time_local; + assert!( + time_difference_local.abs() < Duration::seconds(1), + "Time component difference between system and DicomDateTime local exceeds 1 second: {:?}", + time_difference_local + ); + + let system_date_local = Local::now().naive_local().date(); + let dicom_naive_date_local = dicom_datetime_local_date + .to_naive_date() + .expect("Failed to convert DicomDateTime date component to NaiveDate"); + assert_eq!( + dicom_naive_date_local, system_date_local, + "Date component mismatch between DicomDateTime and system" + ); + } + + #[test] + fn test_dicom_date_time_now_utc() { + let dicom_datetime_utc = DicomDateTime::now_utc() + .expect("Failed to get current UTC datetime from DicomDateTime::now_utc()"); + let dicom_datetime_utc_time = dicom_datetime_utc + .time() + .expect("Failed to get time from DicomDateTime (UTC)"); + let dicom_datetime_utc_date = dicom_datetime_utc.date(); + + let system_time_utc = Utc::now().naive_utc().time(); + let dicom_naive_time_utc = dicom_datetime_utc_time + .to_naive_time() + .expect("Failed to convert DicomDateTime UTC time component to NaiveTime"); + let time_difference_utc = system_time_utc - dicom_naive_time_utc; + assert!( + time_difference_utc.abs() < Duration::seconds(1), + "Time component difference between system and DicomDateTime UTC exceeds 1 second: {:?}", + time_difference_utc + ); + + let system_date_utc = Utc::now().naive_utc().date(); + let dicom_naive_date_utc = dicom_datetime_utc_date + .to_naive_date() + .expect("Failed to convert DicomDateTime UTC date component to NaiveDate"); + assert_eq!( + dicom_naive_date_utc, system_date_utc, + "Date component mismatch between DicomDateTime UTC and system" + ); } #[test] From 2e3ff09dbd0bbfc3da0dfcc560a4babd0dbf09bf Mon Sep 17 00:00:00 2001 From: cryt1c Date: Wed, 15 Oct 2025 20:56:09 +0200 Subject: [PATCH 10/12] Fix clippy --- core/src/value/partial.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index a50eab12..9ca65e29 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -311,11 +311,11 @@ impl DicomDate { // Constructs a new `DicomDate` now from the local timezone pub fn now_local() -> Result { - return DicomDate::try_from(&Local::now().date_naive()); + DicomDate::try_from(&Local::now().date_naive()); } // Constructs a new `DicomDate` now from the utc timezone pub fn now_utc() -> Result { - return DicomDate::try_from(&Utc::now().date_naive()); + DicomDate::try_from(&Utc::now().date_naive()); } /// Retrieves the year from a date as a reference @@ -464,11 +464,11 @@ impl DicomTime { // Constructs a new `DicomTime` now from the local timezone pub fn now_local() -> Result { - return DicomTime::try_from(&Local::now().naive_local().time()); + DicomTime::try_from(&Local::now().naive_local().time()); } // Constructs a new `DicomTime` now from the utc timezone pub fn now_utc() -> Result { - return DicomTime::try_from(&Utc::now().naive_utc().time()); + DicomTime::try_from(&Utc::now().naive_utc().time()); } /** Retrieves the hour from a time as a reference */ @@ -815,11 +815,11 @@ impl DicomDateTime { // Constructs a new `DicomDateTime` now from the local timezone pub fn now_local() -> Result { - return DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?); + DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?); } // Constructs a new `DicomDateTime` now from the utc timezone pub fn now_utc() -> Result { - return DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?); + DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?); } /** Retrieves a reference to the internal date value */ From e821fd79ba3615cf3c058d49a13317a115f57cc5 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Wed, 15 Oct 2025 21:01:32 +0200 Subject: [PATCH 11/12] Fix clippy --- core/src/value/partial.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 9ca65e29..a3d07aeb 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -311,11 +311,11 @@ impl DicomDate { // Constructs a new `DicomDate` now from the local timezone pub fn now_local() -> Result { - DicomDate::try_from(&Local::now().date_naive()); + DicomDate::try_from(&Local::now().date_naive()) } // Constructs a new `DicomDate` now from the utc timezone pub fn now_utc() -> Result { - DicomDate::try_from(&Utc::now().date_naive()); + DicomDate::try_from(&Utc::now().date_naive()) } /// Retrieves the year from a date as a reference @@ -464,11 +464,11 @@ impl DicomTime { // Constructs a new `DicomTime` now from the local timezone pub fn now_local() -> Result { - DicomTime::try_from(&Local::now().naive_local().time()); + DicomTime::try_from(&Local::now().naive_local().time()) } // Constructs a new `DicomTime` now from the utc timezone pub fn now_utc() -> Result { - DicomTime::try_from(&Utc::now().naive_utc().time()); + DicomTime::try_from(&Utc::now().naive_utc().time()) } /** Retrieves the hour from a time as a reference */ @@ -815,11 +815,11 @@ impl DicomDateTime { // Constructs a new `DicomDateTime` now from the local timezone pub fn now_local() -> Result { - DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?); + DicomDateTime::from_date_and_time(DicomDate::now_local()?, DicomTime::now_local()?) } // Constructs a new `DicomDateTime` now from the utc timezone pub fn now_utc() -> Result { - DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?); + DicomDateTime::from_date_and_time(DicomDate::now_utc()?, DicomTime::now_utc()?) } /** Retrieves a reference to the internal date value */ From 6269c3934f384cc2ab5657702dd3e28becc1d7a0 Mon Sep 17 00:00:00 2001 From: cryt1c Date: Wed, 15 Oct 2025 21:04:53 +0200 Subject: [PATCH 12/12] Remove leftover comment --- core/src/value/partial.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index a3d07aeb..523dfb3f 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1450,7 +1450,6 @@ mod tests { #[test] fn test_dicom_time_now_local() { - // Test DicomTime::now_local() let dicom_time = DicomTime::now_local() .expect("Failed to get current local time from DicomTime::now_local()"); let system_time = Local::now().naive_local().time();