dicom_object/
lib.rs

1#![allow(clippy::derive_partial_eq_without_eq)]
2//! This crate contains a high-level abstraction for reading and manipulating
3//! DICOM objects.
4//! At this level, objects are comparable to a dictionary of elements,
5//! in which some of them can have DICOM objects themselves.
6//! The end user should prefer using this abstraction when dealing with DICOM
7//! objects.
8//!
9//!
10//! ## Overview
11//!
12//! - Most interactions with DICOM objects
13//!   will involve in-memory representations of the data set,
14//!   through the type [`InMemDicomObject`].
15//!   New DICOM instances can also be built from scratch using this type.
16//!   A wide assortment of methods are available
17//!   for reading and manipulating the data set.
18//!   See the [`mem`] module for more details.
19//! - Loading a DICOM file can be done with ease via the function [`open_file`].
20//!   For additional file reading options, use [`OpenFileOptions`].
21//!   These are all available in the [`mod@file`] module.
22//! - Other implementations of a DICOM object may exist
23//!   to better serve other use cases.
24//!   If generic support for any DICOM object implementation is a requirement,
25//!   you can try using the [`DicomObject`] trait.
26//! - Currently, the default file DICOM object implementation
27//!   loads the complete contents of the file in memory,
28//!   including pixel data.
29//!   To read DICOM data sets in smaller portions,
30//!   you can use the [DICOM collector API](collector).
31//!
32//! # Encodings
33//!
34//! By default, `dicom-object` supports reading and writing DICOM files
35//! in any transfer syntax without data set compression.
36//! This includes _Explicit VR Little Endian_
37//! (with or without encapsulated pixel data),
38//! _Implicit VR Little Endian_,
39//! and _Explicit VR Big Endian_ (retired).
40//! To enable support for reading and writing deflated data sets
41//! (such as _Deflated Explicit VR Little Endian_),
42//! enable **Cargo feature `deflated`**.
43//!
44//! When working with imaging data,
45//! consider using the [`dicom-pixeldata`] crate,
46//! which offers methods to convert pixel data from objects
47//! into images or multi-dimensional arrays.
48//!
49//! # Examples
50//!
51//! ## Reading a DICOM file
52//!
53//! To read an object from a DICOM file and inspect some attributes:
54//!
55//! ```no_run
56//! use dicom_dictionary_std::tags;
57//! use dicom_object::open_file;
58//! # fn foo() -> Result<(), Box<dyn std::error::Error>> {
59//! let obj = open_file("0001.dcm")?;
60//!
61//! let patient_name = obj.element(tags::PATIENT_NAME)?.to_str()?;
62//! let modality = obj.element_by_name("Modality")?.to_str()?;
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! Elements can be fetched by tag,
68//! either by creating a [`Tag`]
69//! or by using one of the [readily available constants][const]
70//! from the [`dicom-dictionary-std`][dictionary-std] crate.
71//!
72//! [const]: dicom_dictionary_std::tags
73//! [dictionary-std]: https://docs.rs/dicom-dictionary-std
74//!
75//! By default, the entire data set is fully loaded into memory.
76//! The pixel data and following elements can be ignored
77//! by using [`OpenFileOptions`]:
78//!
79//! ```no_run
80//! use dicom_object::OpenFileOptions;
81//!
82//! let obj = OpenFileOptions::new()
83//!     .read_until(dicom_dictionary_std::tags::PIXEL_DATA)
84//!     .open_file("0002.dcm")?;
85//! # Result::<(), dicom_object::ReadError>::Ok(())
86//! ```
87//!
88//! When reading from other data sources without file meta information,
89//! you may need to use [`InMemDicomObject::read_dataset`]
90//! and specify the transfer syntax explicitly.
91//!
92//! ## Fetching data in a DICOM file
93//!
94//! Once a data set element is looked up,
95//! one will typically wish to inspect the value within.
96//! Methods are available for converting the element's DICOM value
97//! into something more usable in Rust.
98//!
99//! ```
100//! # use dicom_dictionary_std::tags;
101//! # use dicom_object::{DefaultDicomObject, Tag};
102//! # fn something(obj: DefaultDicomObject) -> Result<(), Box<dyn std::error::Error>> {
103//! let patient_date = obj.element(tags::PATIENT_BIRTH_DATE)?.to_date()?;
104//! let pixel_data_bytes = obj.element(tags::PIXEL_DATA)?.to_bytes()?;
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! ### Writing DICOM
110//!
111//! **Note:** the code above will only work if you are fetching native pixel data.
112//! If you are potentially working with encapsulated pixel data,
113//! see the [`dicom-pixeldata`] crate.
114//!
115//! [`dicom-pixeldata`]: https://docs.rs/dicom-pixeldata
116//!
117//! ## Writing DICOM data
118//!
119//! Finally, DICOM objects can be serialized back into DICOM encoded bytes.
120//! A method is provided for writing a file DICOM object into a new DICOM file.
121//!
122//! ```no_run
123//! # use dicom_object::{DefaultDicomObject, Tag};
124//! # fn something(obj: DefaultDicomObject) -> Result<(), Box<dyn std::error::Error>> {
125//! obj.write_to_file("0001_new.dcm")?;
126//! # Ok(())
127//! # }
128//! ```
129//!
130//! This method requires you to write a [file meta table] first.
131//! When creating a new DICOM object from scratch,
132//! use a [`FileMetaTableBuilder`] to construct the file meta group,
133//! then use [`with_meta`] or [`with_exact_meta`]:
134//!
135//! [file meta table]: crate::meta::FileMetaTable
136//! [`FileMetaTableBuilder`]: crate::meta::FileMetaTableBuilder
137//! [`with_meta`]: crate::InMemDicomObject::with_meta
138//! [`with_exact_meta`]: crate::InMemDicomObject::with_exact_meta
139//!
140//! ```no_run
141//! # use dicom_object::{InMemDicomObject, FileMetaTableBuilder};
142//! # fn something(obj: InMemDicomObject) -> Result<(), Box<dyn std::error::Error>> {
143//! use dicom_dictionary_std::uids;
144//!
145//! let file_obj = obj.with_meta(
146//!     FileMetaTableBuilder::new()
147//!         // Implicit VR Little Endian
148//!         .transfer_syntax(uids::IMPLICIT_VR_LITTLE_ENDIAN)
149//!         // Computed Radiography image storage
150//!         .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
151//! )?;
152//! file_obj.write_to_file("0001_new.dcm")?;
153//! # Ok(())
154//! # }
155//! ```
156//!
157//! In order to write a plain DICOM data set,
158//! use one of the various data set writing methods
159//! such as [`write_dataset_with_ts`]:
160//!
161//! [`write_dataset_with_ts`]: InMemDicomObject::write_dataset_with_ts
162//!
163//! ```
164//! # use dicom_object::InMemDicomObject;
165//! # use dicom_core::{DataElement, Tag, VR};
166//! # fn run() -> Result<(), Box<dyn std::error::Error>> {
167//! // build your object
168//! let mut obj = InMemDicomObject::from_element_iter([
169//!     DataElement::new(
170//!         Tag(0x0010, 0x0010),
171//!         VR::PN,
172//!         "Doe^John",
173//!     ),
174//! ]);
175//!
176//! // write the object's data set
177//! let mut serialized = Vec::new();
178//! let ts = dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.erased();
179//! obj.write_dataset_with_ts(&mut serialized, &ts)?;
180//! assert_eq!(
181//!     serialized.len(),
182//!     16, // 4 (tag) + 2 (VR) + 2 (length) + 8 (data)
183//! );
184//! # Ok(())
185//! # }
186//! # run().unwrap();
187//! ```
188pub mod collector;
189pub mod file;
190pub mod mem;
191pub mod meta;
192pub mod ops;
193pub mod tokens;
194
195pub use crate::collector::{DicomCollector, DicomCollectorOptions};
196pub use crate::file::{from_reader, open_file, OpenFileOptions};
197pub use crate::mem::InMemDicomObject;
198pub use crate::meta::{FileMetaTable, FileMetaTableBuilder};
199use dicom_core::ops::{AttributeSelector, AttributeSelectorStep};
200use dicom_core::value::{DicomValueType, ValueType};
201pub use dicom_core::Tag;
202use dicom_core::{DataDictionary, DicomValue, PrimitiveValue};
203use dicom_dictionary_std::uids;
204pub use dicom_dictionary_std::StandardDataDictionary;
205
206/// The default implementation of a root DICOM object.
207pub type DefaultDicomObject<D = StandardDataDictionary> = FileDicomObject<mem::InMemDicomObject<D>>;
208
209use dicom_core::header::{GroupNumber, HasLength};
210use dicom_encoding::adapters::{PixelDataObject, RawPixelData};
211use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
212use dicom_encoding::Codec;
213use dicom_parser::dataset::{DataSetWriter, IntoTokens};
214use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
215use itertools::Either;
216use meta::FileMetaAttribute;
217use smallvec::SmallVec;
218use snafu::{prelude::*, Backtrace};
219use std::borrow::Cow;
220use std::fs::File;
221use std::io::{BufWriter, Write};
222use std::path::Path;
223
224/// The current implementation class UID generically referring to DICOM-rs.
225///
226/// Automatically generated as per the standard, part 5, section B.2.
227///
228/// This UID may change in future versions,
229/// even between patch versions.
230pub const IMPLEMENTATION_CLASS_UID: &str = "2.25.214312761802046835989399652652980912193";
231
232/// The current implementation version name generically referring to DICOM-rs.
233///
234/// This name may change in future versions,
235/// even between patch versions.
236pub const IMPLEMENTATION_VERSION_NAME: &str = "DICOM-rs 0.9.0";
237
238/// An error which occurs when fetching a value
239#[derive(Debug, Snafu)]
240#[non_exhaustive]
241pub enum AttributeError {
242    /// A custom error when encoding fails.
243    /// Read the `message` and the underlying `source`
244    /// for more details.
245    #[snafu(whatever, display("{}", message))]
246    Custom {
247        /// The error message.
248        message: String,
249        /// The underlying error cause, if any.
250        #[snafu(source(from(Box<dyn std::error::Error + Send + Sync + 'static>, Some)))]
251        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
252    },
253
254    /// the value cannot be converted to the requested type
255    ConvertValue {
256        source: dicom_core::value::ConvertValueError,
257    },
258
259    /// the value is not a data set sequence
260    NotDataSet,
261
262    /// the requested item index does not exist
263    DataSetItemOutOfBounds,
264
265    /// the value is not a pixel data element
266    NotPixelData,
267
268    /// the requested pixel data fragment index does not exist
269    FragmentOutOfBounds,
270}
271
272/// Interface type for access to a DICOM object' attribute in a DICOM object.
273///
274/// Accessing an attribute via the [`DicomObject`] trait
275/// will generally result in a value of this type,
276/// which can then be converted into a more usable form.
277///
278/// Both [`DicomValue`] and references to it implement this trait.
279pub trait DicomAttribute: DicomValueType {
280    /// The data type of an item in a data set sequence
281    type Item<'a>: HasLength
282    where
283        Self: 'a;
284
285    /// The data type of a single contiguous pixel data fragment
286    type PixelData<'a>
287    where
288        Self: 'a;
289
290    /// Obtain an in-memory representation of the primitive value,
291    /// cloning the value if necessary.
292    ///
293    /// An error is returned if the value is a data set sequence or
294    /// an encapsulated pixel data fragment sequence.
295    fn to_primitive_value(&self) -> Result<PrimitiveValue, AttributeError>;
296
297    /// Obtain one of the items in the attribute,
298    /// if the attribute value represents a data set sequence.
299    ///
300    /// Returns an error if the attribute is not a sequence.
301    fn item(&self, index: u32) -> Result<Self::Item<'_>, AttributeError>;
302
303    /// Obtain the number of data set items or pixel data fragments,
304    /// if the attribute value represents a data set sequence or pixel data.
305    /// Returns `None` otherwise.
306    fn num_items(&self) -> Option<u32>;
307
308    /// Obtain one of the fragments in the attribute,
309    /// if the attribute value represents a pixel data fragment sequence.
310    ///
311    /// Returns an error if the attribute is not a pixel data sequence.
312    fn fragment(&self, index: u32) -> Result<Self::PixelData<'_>, AttributeError>;
313
314    /// Obtain the number of pixel data fragments,
315    /// if the attribute value represents encapsulated pixel data.
316    ///
317    /// It should equivalent to `num_items`,
318    /// save for returning `None` if the attribute is a data set sequence.
319    fn num_fragments(&self) -> Option<u32> {
320        if DicomValueType::value_type(self) == ValueType::PixelSequence {
321            self.num_items()
322        } else {
323            None
324        }
325    }
326
327    /// Obtain the attribute's value as a string,
328    /// converting it if necessary.
329    fn to_str(&self) -> Result<Cow<'_, str>, AttributeError> {
330        Ok(Cow::Owned(self.to_primitive_value()?.to_str().to_string()))
331    }
332
333    /// Obtain the attribute's value as a 16-bit unsigned integer,
334    /// converting it if necessary.
335    fn to_u16(&self) -> Result<u16, AttributeError> {
336        self.to_primitive_value()?
337            .to_int()
338            .context(ConvertValueSnafu)
339    }
340
341    /// Obtain the attribute's value as a 32-bit signed integer,
342    /// converting it if necessary.
343    fn to_i32(&self) -> Result<i32, AttributeError> {
344        self.to_primitive_value()?
345            .to_int()
346            .context(ConvertValueSnafu)
347    }
348
349    /// Obtain the attribute's value as a 32-bit unsigned integer,
350    /// converting it if necessary.
351    fn to_u32(&self) -> Result<u32, AttributeError> {
352        self.to_primitive_value()?
353            .to_int()
354            .context(ConvertValueSnafu)
355    }
356
357    /// Obtain the attribute's value as a 32-bit floating-point number,
358    /// converting it if necessary.
359    fn to_f32(&self) -> Result<f32, AttributeError> {
360        self.to_primitive_value()?
361            .to_float32()
362            .context(ConvertValueSnafu)
363    }
364
365    /// Obtain the attribute's value as a 64-bit floating-point number,
366    /// converting it if necessary.
367    fn to_f64(&self) -> Result<f64, AttributeError> {
368        self.to_primitive_value()?
369            .to_float64()
370            .context(ConvertValueSnafu)
371    }
372
373    /// Obtain the attribute's value as bytes,
374    /// converting it if necessary.
375    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, AttributeError> {
376        Ok(Cow::Owned(self.to_primitive_value()?.to_bytes().to_vec()))
377    }
378}
379
380/// Trait type for a generalized interpretation of a DICOM object.
381///
382/// This is a high-level abstraction where an object is accessed and
383/// manipulated as a dictionary of entries indexed by tags, which in
384/// turn may contain a DICOM object.
385///
386/// ## Examples
387///
388/// You can use this trait to operate on DICOM data sets
389/// when the exact DICOM object type is not known.
390///
391/// ```
392/// # use dicom_core::prelude::*;
393/// # use dicom_dictionary_std::tags;
394/// # use dicom_object::mem::InMemDicomObject;
395/// use dicom_object::DicomObject;
396/// use dicom_object::DicomAttribute as _;
397///
398/// fn my_data() -> impl DicomObject {
399///     InMemDicomObject::from_element_iter([
400///         DataElement::new(
401///             tags::SOP_INSTANCE_UID,
402///             VR::UI,
403///             "2.25.60131396312732822704775296119377475501",
404///         ),
405///     ])
406/// }
407///
408/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
409/// let obj = my_data();
410/// assert_eq!(
411///     obj.attr(tags::SOP_INSTANCE_UID)?.to_str()?,
412///    "2.25.60131396312732822704775296119377475501"
413/// );
414/// # Ok(())
415/// # }
416/// ```
417///
418/// It works for in-memory data sets, file meta groups,
419/// and other similar structures.
420/// When operating on DICOM file objects,
421/// the look-up will be directed to the right group
422/// depending on the attribute requested.
423///
424/// ```no_run
425/// # use dicom_dictionary_std::tags;
426/// use dicom_object::{DicomObject as _, open_file};
427///
428/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
429/// let file = open_file("00001.dcm")?;
430///
431/// let Some(media_storage_sop_class_uid) = file.attr_opt(tags::MEDIA_STORAGE_SOP_CLASS_UID)? else {
432///     panic!("DICOM file should have a Media Storage SOP Class UID");
433/// };
434/// # Ok(())
435/// # }
436/// ```
437///
438/// Types implementing this trait
439/// will likely also implement [`ApplyOp`](dicom_core::ops::ApplyOp),
440/// enabling more operations on DICOM objects.
441pub trait DicomObject {
442    /// The type representing a DICOM attribute in the object
443    /// and/or the necessary means to retrieve the value from it.
444    ///
445    /// This is the outcome of a shallow look-up
446    /// using the methods [`attr`](DicomObject::attr)
447    /// or [`attr_opt`](DicomObject::attr_opt).
448    type Attribute<'a>: DicomAttribute
449    where
450        Self: 'a;
451
452    /// The type representing a DICOM leaf attribute in the object
453    /// and/or the necessary means to retrieve the value from it.
454    ///
455    /// This is the outcome of a potentially deep look-up
456    /// using the method [`at`](DicomObject::at).
457    type LeafAttribute<'a>: DicomAttribute
458    where
459        Self: 'a;
460
461    /// Retrieve a particular DICOM attribute
462    /// by looking up the given tag at the object's root.
463    ///
464    /// `Ok(None)` is returned when the object was successfully looked up
465    /// but the element is not present.
466    /// This is not a recursive search.
467    fn attr_opt(&self, tag: Tag) -> Result<Option<Self::Attribute<'_>>, AccessError>;
468
469    /// Retrieve a particular DICOM element by its name (keyword).
470    ///
471    /// `Ok(None)` is returned when the object was successfully looked up
472    /// but the element is not present.
473    ///
474    /// If the DICOM tag is already known,
475    /// prefer calling [`attr_opt`](Self::attr_opt)
476    /// with one of the [constants from the DICOM dictionary][tags].
477    ///
478    /// [tags]: dicom_dictionary_std::tags
479    fn attr_by_name_opt(
480        &self,
481        name: &str,
482    ) -> Result<Option<Self::Attribute<'_>>, AccessByNameError>;
483
484    /// Retrieve a particular DICOM attribute
485    /// by looking up the given tag at the object's root.
486    ///
487    /// Unlike [`attr_opt`](Self::attr_opt),
488    /// this method returns an error if the element is not present.
489    fn attr(&self, tag: Tag) -> Result<Self::Attribute<'_>, AccessError> {
490        self.attr_opt(tag)?
491            .context(NoSuchDataElementTagSnafu { tag })
492    }
493
494    /// Retrieve a particular DICOM attribute
495    /// with a look-up by attribute selector.
496    ///
497    /// This method returns an error if the element is not present.
498    fn at(
499        &self,
500        selector: impl Into<AttributeSelector>,
501    ) -> Result<Self::LeafAttribute<'_>, AtAccessError>;
502
503    /// Retrieve a particular DICOM element by its name (keyword).
504    ///
505    /// Unlike [`attr_by_name_opt`](Self::attr_by_name_opt),
506    /// this method returns an error if the element is not present.
507    ///
508    /// If the DICOM tag is already known,
509    /// prefer calling [`attr`](Self::attr)
510    /// with one of the [constants from the DICOM dictionary][tags].
511    ///
512    /// [tags]: dicom_dictionary_std::tags
513    fn attr_by_name(&self, name: &str) -> Result<Self::Attribute<'_>, AccessByNameError> {
514        self.attr_by_name_opt(name)?
515            .context(NoSuchAttributeNameSnafu { name })
516    }
517}
518
519impl<O, P> DicomAttribute for DicomValue<O, P>
520where
521    O: HasLength,
522    for<'a> &'a O: HasLength,
523{
524    type Item<'a>
525        = &'a O
526    where
527        Self: 'a,
528        O: 'a;
529    type PixelData<'a>
530        = &'a P
531    where
532        Self: 'a,
533        P: 'a;
534
535    #[inline]
536    fn to_primitive_value(&self) -> Result<PrimitiveValue, AttributeError> {
537        match self {
538            DicomValue::Primitive(value) => Ok(value.clone()),
539            _ => Err(AttributeError::ConvertValue {
540                source: dicom_core::value::ConvertValueError {
541                    requested: "primitive",
542                    original: self.value_type(),
543                    cause: None,
544                },
545            }),
546        }
547    }
548
549    #[inline]
550    fn item(&self, index: u32) -> Result<&O, AttributeError> {
551        let items = self.items().context(NotDataSetSnafu)?;
552        items
553            .get(index as usize)
554            .context(DataSetItemOutOfBoundsSnafu)
555    }
556
557    #[inline]
558    fn num_items(&self) -> Option<u32> {
559        match self {
560            DicomValue::PixelSequence(seq) => Some(seq.fragments().len() as u32),
561            DicomValue::Sequence(seq) => Some(seq.multiplicity()),
562            _ => None,
563        }
564    }
565
566    #[inline]
567    fn fragment(&self, index: u32) -> Result<Self::PixelData<'_>, AttributeError> {
568        match self {
569            DicomValue::PixelSequence(seq) => Ok(seq
570                .fragments()
571                .get(index as usize)
572                .context(FragmentOutOfBoundsSnafu)?),
573            _ => Err(AttributeError::NotPixelData),
574        }
575    }
576
577    #[inline]
578    fn to_str(&self) -> Result<Cow<'_, str>, AttributeError> {
579        DicomValue::to_str(self).context(ConvertValueSnafu)
580    }
581
582    #[inline]
583    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, AttributeError> {
584        DicomValue::to_bytes(self).context(ConvertValueSnafu)
585    }
586}
587
588impl<'b, O, P> DicomAttribute for &'b DicomValue<O, P>
589where
590    O: HasLength,
591    &'b O: HasLength,
592    O: Clone,
593    P: Clone,
594{
595    type Item<'a>
596        = &'b O
597    where
598        Self: 'a,
599        O: 'a;
600    type PixelData<'a>
601        = &'b P
602    where
603        Self: 'a,
604        P: 'a;
605
606    #[inline]
607    fn to_primitive_value(&self) -> Result<PrimitiveValue, AttributeError> {
608        match self {
609            DicomValue::Primitive(value) => Ok(value.clone()),
610            _ => Err(AttributeError::ConvertValue {
611                source: dicom_core::value::ConvertValueError {
612                    requested: "primitive",
613                    original: self.value_type(),
614                    cause: None,
615                },
616            }),
617        }
618    }
619
620    #[inline]
621    fn item(&self, index: u32) -> Result<Self::Item<'_>, AttributeError> {
622        let items = self.items().context(NotDataSetSnafu)?;
623        items
624            .get(index as usize)
625            .context(DataSetItemOutOfBoundsSnafu)
626    }
627
628    #[inline]
629    fn num_items(&self) -> Option<u32> {
630        match self {
631            DicomValue::PixelSequence(seq) => Some(seq.fragments().len() as u32),
632            DicomValue::Sequence(seq) => Some(seq.multiplicity()),
633            _ => None,
634        }
635    }
636
637    #[inline]
638    fn fragment(&self, index: u32) -> Result<Self::PixelData<'_>, AttributeError> {
639        match self {
640            DicomValue::PixelSequence(seq) => seq
641                .fragments()
642                .get(index as usize)
643                .context(FragmentOutOfBoundsSnafu),
644            _ => Err(AttributeError::NotPixelData),
645        }
646    }
647
648    #[inline]
649    fn num_fragments(&self) -> Option<u32> {
650        match self {
651            DicomValue::PixelSequence(seq) => Some(seq.fragments().len() as u32),
652            _ => None,
653        }
654    }
655
656    #[inline]
657    fn to_str(&self) -> Result<Cow<'_, str>, AttributeError> {
658        DicomValue::to_str(self).context(ConvertValueSnafu)
659    }
660
661    #[inline]
662    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, AttributeError> {
663        DicomValue::to_bytes(self).context(ConvertValueSnafu)
664    }
665}
666
667/// An error which may occur when loading a DICOM object
668#[derive(Debug, Snafu)]
669#[non_exhaustive]
670pub enum ReadError {
671    #[snafu(display("Could not open file '{}'", filename.display()))]
672    OpenFile {
673        filename: std::path::PathBuf,
674        backtrace: Backtrace,
675        source: std::io::Error,
676    },
677    #[snafu(display("Could not read from file '{}'", filename.display()))]
678    ReadFile {
679        filename: std::path::PathBuf,
680        backtrace: Backtrace,
681        source: std::io::Error,
682    },
683    /// Could not read preamble bytes
684    ReadPreambleBytes {
685        backtrace: Backtrace,
686        source: std::io::Error,
687    },
688    #[snafu(display("Could not parse meta group data set"))]
689    ParseMetaDataSet {
690        #[snafu(backtrace)]
691        source: crate::meta::Error,
692    },
693    #[snafu(display("Could not parse sop attribute"))]
694    ParseSopAttribute {
695        #[snafu(source(from(dicom_core::value::ConvertValueError, Box::from)))]
696        source: Box<dicom_core::value::ConvertValueError>,
697        backtrace: Backtrace,
698    },
699    #[snafu(display("Could not create data set parser"))]
700    CreateParser {
701        #[snafu(backtrace)]
702        source: dicom_parser::dataset::read::Error,
703    },
704    #[snafu(display("Could not read data set token"))]
705    ReadToken {
706        #[snafu(backtrace)]
707        source: dicom_parser::dataset::read::Error,
708    },
709    #[snafu(display("Missing element value after header token"))]
710    MissingElementValue { backtrace: Backtrace },
711    #[snafu(display("Unrecognized transfer syntax `{}`", uid))]
712    ReadUnrecognizedTransferSyntax { uid: String, backtrace: Backtrace },
713    #[snafu(display("Unsupported reading for transfer syntax `{}` ({})", uid, name))]
714    ReadUnsupportedTransferSyntax {
715        uid: &'static str,
716        name: &'static str,
717        backtrace: Backtrace,
718    },
719    #[snafu(display(
720        "Unsupported reading for transfer syntax `{uid}` ({name}, try enabling feature `{feature_name}`)"
721    ))]
722    ReadUnsupportedTransferSyntaxWithSuggestion {
723        uid: &'static str,
724        name: &'static str,
725        feature_name: &'static str,
726        backtrace: Backtrace,
727    },
728    #[snafu(display("Unexpected token {:?}", token))]
729    UnexpectedToken {
730        token: Box<dicom_parser::dataset::DataToken>,
731        backtrace: Backtrace,
732    },
733    #[snafu(display("Premature data set end"))]
734    PrematureEnd { backtrace: Backtrace },
735}
736
737/// An error which may occur when writing a DICOM object
738#[derive(Debug, Snafu)]
739#[non_exhaustive]
740pub enum WriteError {
741    #[snafu(display("Could not write to file '{}'", filename.display()))]
742    WriteFile {
743        filename: std::path::PathBuf,
744        backtrace: Backtrace,
745        source: std::io::Error,
746    },
747    #[snafu(display("Could not write object preamble"))]
748    WritePreamble {
749        backtrace: Backtrace,
750        source: std::io::Error,
751    },
752    #[snafu(display("Could not write magic code"))]
753    WriteMagicCode {
754        backtrace: Backtrace,
755        source: std::io::Error,
756    },
757    #[snafu(display("Could not create data set printer"))]
758    CreatePrinter {
759        #[snafu(backtrace)]
760        source: dicom_parser::dataset::write::Error,
761    },
762    #[snafu(display("Could not print meta group data set"))]
763    PrintMetaDataSet {
764        #[snafu(backtrace)]
765        source: crate::meta::Error,
766    },
767    #[snafu(display("Could not print data set"))]
768    PrintDataSet {
769        #[snafu(backtrace)]
770        source: dicom_parser::dataset::write::Error,
771    },
772    #[snafu(display("Unrecognized transfer syntax `{uid}`"))]
773    WriteUnrecognizedTransferSyntax { uid: String, backtrace: Backtrace },
774    #[snafu(display("Unsupported transfer syntax `{uid}` ({name})"))]
775    WriteUnsupportedTransferSyntax {
776        uid: &'static str,
777        name: &'static str,
778        backtrace: Backtrace,
779    },
780    #[snafu(display(
781        "Unsupported transfer syntax `{uid}` ({name}, try enabling feature `{feature_name}`)"
782    ))]
783    WriteUnsupportedTransferSyntaxWithSuggestion {
784        uid: &'static str,
785        name: &'static str,
786        feature_name: &'static str,
787        backtrace: Backtrace,
788    },
789}
790
791/// An error which may occur during private element look-up or insertion
792#[derive(Debug, Snafu)]
793#[non_exhaustive]
794pub enum PrivateElementError {
795    /// Group number must be odd
796    #[snafu(display("Group number must be odd, found {:#06x}", group))]
797    InvalidGroup { group: GroupNumber },
798    /// Private creator not found in group
799    #[snafu(display("Private creator {} not found in group {:#06x}", creator, group))]
800    PrivateCreatorNotFound { creator: String, group: GroupNumber },
801    /// Element not found in group
802    #[snafu(display(
803        "Private Creator {} found in group {:#06x}, but elem {:#06x} not found",
804        creator,
805        group,
806        elem
807    ))]
808    ElementNotFound {
809        creator: String,
810        group: GroupNumber,
811        elem: u8,
812    },
813    /// No space available for more private elements in the group
814    #[snafu(display("No space available in group {:#06x}", group))]
815    NoSpace { group: GroupNumber },
816}
817
818/// An error which may occur when looking up a DICOM object's attributes.
819#[derive(Debug, Snafu)]
820#[non_exhaustive]
821pub enum AccessError {
822    #[snafu(display("No such data element with tag {}", tag))]
823    NoSuchDataElementTag { tag: Tag, backtrace: Backtrace },
824}
825
826impl AccessError {
827    pub fn into_access_by_name(self, alias: impl Into<String>) -> AccessByNameError {
828        match self {
829            AccessError::NoSuchDataElementTag { tag, backtrace } => {
830                AccessByNameError::NoSuchDataElementAlias {
831                    tag,
832                    alias: alias.into(),
833                    backtrace,
834                }
835            }
836        }
837    }
838}
839
840/// An error which may occur when looking up a DICOM object's attributes
841/// at an arbitrary depth,
842/// such as through [`value_at`](crate::InMemDicomObject::value_at).
843#[derive(Debug, Snafu)]
844#[non_exhaustive]
845#[snafu(visibility(pub(crate)))]
846pub enum AtAccessError {
847    /// Missing intermediate sequence for {selector} at step {step_index}
848    MissingSequence {
849        selector: AttributeSelector,
850        step_index: u32,
851    },
852    /// Step {step_index} for {selector} is not a data set sequence
853    NotASequence {
854        selector: AttributeSelector,
855        step_index: u32,
856    },
857    /// Missing element at last step for {selector}
858    MissingLeafElement { selector: AttributeSelector },
859}
860
861/// An error which may occur when looking up a DICOM object's attributes
862/// by a keyword (or alias) instead of by tag.
863///
864/// These accesses incur a look-up at the data element dictionary,
865/// which may fail if no such entry exists.
866#[derive(Debug, Snafu)]
867pub enum AccessByNameError {
868    #[snafu(display("No such data element {} (with tag {})", alias, tag))]
869    NoSuchDataElementAlias {
870        tag: Tag,
871        alias: String,
872        backtrace: Backtrace,
873    },
874
875    /// Could not resolve attribute name from the data dictionary
876    #[snafu(display("Unknown data attribute named `{}`", name))]
877    NoSuchAttributeName { name: String, backtrace: Backtrace },
878}
879
880#[derive(Debug, Snafu)]
881#[non_exhaustive]
882pub enum WithMetaError {
883    /// Could not build file meta table
884    BuildMetaTable {
885        #[snafu(backtrace)]
886        source: crate::meta::Error,
887    },
888    /// Could not prepare file meta table
889    PrepareMetaTable {
890        #[snafu(source(from(dicom_core::value::ConvertValueError, Box::from)))]
891        source: Box<dicom_core::value::ConvertValueError>,
892        backtrace: Backtrace,
893    },
894}
895
896/// A root DICOM object retrieved from a standard DICOM file,
897/// containing additional information from the file meta group
898/// in a separate table value.
899///
900/// **Note:** This type implements `Deref` towards the inner object,
901/// meaning that most method calls will be directed
902/// as if the call was performed directly on the main data set.
903/// While this is convenient,
904/// it also means that data elements from the file meta group
905/// cannot be accessed without explicitly taking a reference
906/// first through [`meta()`](FileDicomObject::meta).
907/// To abstract this away,
908/// see the [`DicomObject`] trait.
909#[derive(Debug, Clone, PartialEq)]
910pub struct FileDicomObject<O> {
911    meta: FileMetaTable,
912    obj: O,
913}
914
915impl<O> FileDicomObject<O> {
916    /// Retrieve the processed meta header table.
917    pub fn meta(&self) -> &FileMetaTable {
918        &self.meta
919    }
920
921    /// Retrieve a mutable reference to the processed meta header table.
922    ///
923    /// Considerable care should be taken when modifying this table,
924    /// as it may influence object reading and writing operations.
925    /// When modifying the table through this method,
926    /// the user is responsible for updating the meta information group length as well,
927    /// which can be done by calling
928    /// [`update_information_group_length`](FileMetaTable::update_information_group_length).
929    ///
930    /// See also [`update_meta`](Self::update_meta).
931    pub fn meta_mut(&mut self) -> &mut FileMetaTable {
932        &mut self.meta
933    }
934
935    /// Update the processed meta header table through a function.
936    ///
937    /// Considerable care should be taken when modifying this table,
938    /// as it may influence object reading and writing operations.
939    /// The meta information group length is updated automatically.
940    pub fn update_meta(&mut self, f: impl FnOnce(&mut FileMetaTable)) {
941        f(&mut self.meta);
942        self.meta.update_information_group_length();
943    }
944
945    /// Retrieve the inner DICOM object structure, discarding the meta table.
946    pub fn into_inner(self) -> O {
947        self.obj
948    }
949}
950
951impl<O> FileDicomObject<O>
952where
953    for<'a> &'a O: IntoTokens,
954{
955    /// Write the entire object as a DICOM file
956    /// into the given file path.
957    /// Preamble, magic code, and file meta group will be included
958    /// before the inner object.
959    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), WriteError> {
960        let path = path.as_ref();
961        let file = File::create(path).context(WriteFileSnafu { filename: path })?;
962        let mut to = BufWriter::new(file);
963
964        // write preamble
965        to.write_all(&[0_u8; 128][..])
966            .context(WriteFileSnafu { filename: path })?;
967
968        // write magic sequence
969        to.write_all(b"DICM")
970            .context(WriteFileSnafu { filename: path })?;
971
972        // write meta group
973        self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
974
975        self.write_dataset_impl(to)
976    }
977
978    /// Write the entire object as a DICOM file
979    /// into the given writer.
980    /// Preamble, magic code, and file meta group will be included
981    /// before the inner object.
982    pub fn write_all(&self, to: impl Write) -> Result<(), WriteError> {
983        let mut to = BufWriter::new(to);
984
985        // write preamble
986        to.write_all(&[0_u8; 128][..]).context(WritePreambleSnafu)?;
987
988        // write magic sequence
989        to.write_all(b"DICM").context(WriteMagicCodeSnafu)?;
990
991        // write meta group
992        self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
993
994        self.write_dataset_impl(to)
995    }
996
997    /// Write the file meta group set into the given writer.
998    ///
999    /// This is equivalent to `self.meta().write(to)`.
1000    pub fn write_meta<W: Write>(&self, to: W) -> Result<(), WriteError> {
1001        self.meta.write(to).context(PrintMetaDataSetSnafu)
1002    }
1003
1004    /// Write the inner data set into the given writer,
1005    /// without preamble, magic code, nor file meta group.
1006    ///
1007    /// The transfer syntax is selected from the file meta table.
1008    pub fn write_dataset<W: Write>(&self, to: W) -> Result<(), WriteError> {
1009        let to = BufWriter::new(to);
1010
1011        self.write_dataset_impl(to)
1012    }
1013
1014    /// Helper function for writing the DICOM data set in this file DICOM object
1015    /// with the right transfer syntax.
1016    /// Automatically retrieves a data set adapter if required and available,
1017    /// returns an error if the transfer syntax is not supported for data set writing.
1018    fn write_dataset_impl(&self, to: impl Write) -> Result<(), WriteError> {
1019        let ts_uid = self.meta.transfer_syntax();
1020        // prepare encoder
1021        let ts = if let Some(ts) = TransferSyntaxRegistry.get(ts_uid) {
1022            ts
1023        } else {
1024            return WriteUnrecognizedTransferSyntaxSnafu {
1025                uid: ts_uid.to_string(),
1026            }
1027            .fail();
1028        };
1029        match ts.codec() {
1030            Codec::Dataset(Some(adapter)) => {
1031                let adapter = adapter.adapt_writer(Box::new(to));
1032                let mut dset_writer =
1033                    DataSetWriter::with_ts(adapter, ts).context(CreatePrinterSnafu)?;
1034
1035                // write object
1036                dset_writer
1037                    .write_sequence((&self.obj).into_tokens())
1038                    .context(PrintDataSetSnafu)?;
1039
1040                dset_writer.flush().context(PrintDataSetSnafu)?;
1041
1042                Ok(())
1043            }
1044            Codec::Dataset(None) => {
1045                // dataset adapter needed, but not provided
1046                if ts_uid == uids::DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN
1047                    || ts_uid == uids::JPIP_REFERENCED_DEFLATE
1048                    || ts_uid == uids::JPIPHTJ2K_REFERENCED_DEFLATE
1049                {
1050                    return WriteUnsupportedTransferSyntaxWithSuggestionSnafu {
1051                        uid: ts.uid(),
1052                        name: ts.name(),
1053                        feature_name: "dicom-transfer-syntax-registry/deflate",
1054                    }
1055                    .fail();
1056                }
1057                WriteUnsupportedTransferSyntaxSnafu {
1058                    uid: ts.uid(),
1059                    name: ts.name(),
1060                }
1061                .fail()
1062            }
1063            Codec::None | Codec::EncapsulatedPixelData(..) => {
1064                // no dataset adapter needed
1065                let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
1066
1067                // write object
1068                dset_writer
1069                    .write_sequence((&self.obj).into_tokens())
1070                    .context(PrintDataSetSnafu)?;
1071                dset_writer.flush().context(PrintDataSetSnafu)?;
1072
1073                Ok(())
1074            }
1075        }
1076    }
1077}
1078
1079impl<O> ::std::ops::Deref for FileDicomObject<O> {
1080    type Target = O;
1081
1082    fn deref(&self) -> &Self::Target {
1083        &self.obj
1084    }
1085}
1086
1087impl<O> ::std::ops::DerefMut for FileDicomObject<O> {
1088    fn deref_mut(&mut self) -> &mut Self::Target {
1089        &mut self.obj
1090    }
1091}
1092
1093impl<L, R> DicomAttribute for Either<L, R>
1094where
1095    L: DicomAttribute,
1096    R: DicomAttribute,
1097{
1098    type Item<'a>
1099        = Either<L::Item<'a>, R::Item<'a>>
1100    where
1101        Self: 'a;
1102    type PixelData<'a>
1103        = Either<L::PixelData<'a>, R::PixelData<'a>>
1104    where
1105        Self: 'a;
1106
1107    fn to_primitive_value(&self) -> Result<PrimitiveValue, AttributeError> {
1108        match self {
1109            Either::Left(l) => l.to_primitive_value(),
1110            Either::Right(r) => r.to_primitive_value(),
1111        }
1112    }
1113
1114    fn item(&self, index: u32) -> Result<Self::Item<'_>, AttributeError> {
1115        match self {
1116            Either::Left(l) => l.item(index).map(Either::Left),
1117            Either::Right(r) => r.item(index).map(Either::Right),
1118        }
1119    }
1120
1121    fn num_items(&self) -> Option<u32> {
1122        match self {
1123            Either::Left(l) => l.num_items(),
1124            Either::Right(r) => r.num_items(),
1125        }
1126    }
1127
1128    fn fragment(&self, index: u32) -> Result<Self::PixelData<'_>, AttributeError> {
1129        match self {
1130            Either::Left(l) => l.fragment(index).map(Either::Left),
1131            Either::Right(r) => r.fragment(index).map(Either::Right),
1132        }
1133    }
1134}
1135
1136impl<O> DicomObject for FileDicomObject<O>
1137where
1138    O: DicomObject,
1139{
1140    type Attribute<'a>
1141        = Either<FileMetaAttribute<'a>, O::Attribute<'a>>
1142    where
1143        Self: 'a,
1144        O: 'a;
1145
1146    type LeafAttribute<'a>
1147        = Either<FileMetaAttribute<'a>, O::LeafAttribute<'a>>
1148    where
1149        Self: 'a,
1150        O: 'a;
1151
1152    #[inline]
1153    fn attr_opt(&self, tag: Tag) -> Result<Option<Self::Attribute<'_>>, AccessError> {
1154        match tag {
1155            Tag(0x0002, _) => {
1156                let attr = self.meta.attr_opt(tag)?;
1157                Ok(attr.map(Either::Left))
1158            }
1159            _ => {
1160                let attr = self.obj.attr_opt(tag)?;
1161                Ok(attr.map(Either::Right))
1162            }
1163        }
1164    }
1165
1166    #[inline]
1167    fn attr_by_name_opt(
1168        &self,
1169        name: &str,
1170    ) -> Result<Option<Self::Attribute<'_>>, AccessByNameError> {
1171        match name {
1172            "FileMetaInformationGroupLength"
1173            | "FileMetaInformationVersion"
1174            | "MediaStorageSOPClassUID"
1175            | "MediaStorageSOPInstanceUID"
1176            | "TransferSyntaxUID"
1177            | "ImplementationClassUID"
1178            | "ImplementationVersionName"
1179            | "SourceApplicationEntityTitle"
1180            | "SendingApplicationEntityTitle"
1181            | "ReceivingApplicationEntityTitle"
1182            | "PrivateInformationCreatorUID"
1183            | "PrivateInformation" => {
1184                let attr = self.meta.attr_by_name_opt(name)?;
1185                Ok(attr.map(Either::Left))
1186            }
1187            _ => {
1188                let attr = self.obj.attr_by_name_opt(name)?;
1189                Ok(attr.map(Either::Right))
1190            }
1191        }
1192    }
1193
1194    #[inline]
1195    fn attr(&self, tag: Tag) -> Result<Self::Attribute<'_>, AccessError> {
1196        match tag {
1197            Tag(0x0002, _) => {
1198                let attr = self.meta.attr(tag)?;
1199                Ok(Either::Left(attr))
1200            }
1201            _ => {
1202                let attr = self.obj.attr(tag)?;
1203                Ok(Either::Right(attr))
1204            }
1205        }
1206    }
1207
1208    fn at(
1209        &self,
1210        selector: impl Into<AttributeSelector>,
1211    ) -> Result<Self::LeafAttribute<'_>, AtAccessError> {
1212        let selector: AttributeSelector = selector.into();
1213        match selector.first_step() {
1214            AttributeSelectorStep::Tag(tag @ Tag(0x0002, _)) => {
1215                let attr = self
1216                    .meta
1217                    .attr(*tag)
1218                    .map_err(|_| AtAccessError::MissingLeafElement { selector })?;
1219                Ok(Either::Left(attr))
1220            }
1221            _ => {
1222                let attr = self.obj.at(selector)?;
1223                Ok(Either::Right(attr))
1224            }
1225        }
1226    }
1227}
1228
1229impl<'s, O: 's> DicomObject for &'s FileDicomObject<O>
1230where
1231    O: DicomObject,
1232{
1233    type Attribute<'a>
1234        = Either<FileMetaAttribute<'a>, O::Attribute<'a>>
1235    where
1236        Self: 'a,
1237        O: 'a;
1238
1239    type LeafAttribute<'a>
1240        = Either<FileMetaAttribute<'a>, O::LeafAttribute<'a>>
1241    where
1242        Self: 'a,
1243        O: 'a;
1244
1245    #[inline]
1246    fn attr_opt(&self, tag: Tag) -> Result<Option<Self::Attribute<'_>>, AccessError> {
1247        (**self).attr_opt(tag)
1248    }
1249
1250    #[inline]
1251    fn attr_by_name_opt(
1252        &self,
1253        name: &str,
1254    ) -> Result<Option<Self::Attribute<'_>>, AccessByNameError> {
1255        (**self).attr_by_name_opt(name)
1256    }
1257
1258    #[inline]
1259    fn attr(&self, tag: Tag) -> Result<Self::Attribute<'_>, AccessError> {
1260        (**self).attr(tag)
1261    }
1262
1263    #[inline]
1264    fn attr_by_name(&self, name: &str) -> Result<Self::Attribute<'_>, AccessByNameError> {
1265        (**self).attr_by_name(name)
1266    }
1267
1268    #[inline]
1269    fn at(
1270        &self,
1271        selector: impl Into<AttributeSelector>,
1272    ) -> Result<Self::LeafAttribute<'_>, AtAccessError> {
1273        (**self).at(selector)
1274    }
1275}
1276
1277/// This implementation creates an iterator
1278/// to the elements of the underlying data set,
1279/// consuming the whole object.
1280/// The attributes in the file meta group are _not_ included.
1281///
1282/// To obtain an iterator over the meta elements,
1283/// use [`meta().to_element_iter()`](FileMetaTable::to_element_iter).
1284impl<O> IntoIterator for FileDicomObject<O>
1285where
1286    O: IntoIterator,
1287{
1288    type Item = <O as IntoIterator>::Item;
1289    type IntoIter = <O as IntoIterator>::IntoIter;
1290
1291    fn into_iter(self) -> Self::IntoIter {
1292        self.obj.into_iter()
1293    }
1294}
1295
1296/// This implementation creates an iterator
1297/// to the elements of the underlying data set.
1298/// The attributes in the file meta group are _not_ included.
1299///
1300/// To obtain an iterator over the meta elements,
1301/// use [`meta().to_element_iter()`](FileMetaTable::to_element_iter).
1302impl<'a, O> IntoIterator for &'a FileDicomObject<O>
1303where
1304    &'a O: IntoIterator,
1305{
1306    type Item = <&'a O as IntoIterator>::Item;
1307    type IntoIter = <&'a O as IntoIterator>::IntoIter;
1308
1309    fn into_iter(self) -> Self::IntoIter {
1310        (&self.obj).into_iter()
1311    }
1312}
1313
1314/// Implement basic pixeldata encoder/decoder functionality
1315impl<D> PixelDataObject for FileDicomObject<InMemDicomObject<D>>
1316where
1317    D: DataDictionary + Clone,
1318{
1319    fn transfer_syntax_uid(&self) -> &str {
1320        self.meta.transfer_syntax()
1321    }
1322
1323    /// Return the Rows attribute or None if it is not found
1324    fn rows(&self) -> Option<u16> {
1325        (**self)
1326            .get(dicom_dictionary_std::tags::ROWS)?
1327            .uint16()
1328            .ok()
1329    }
1330
1331    /// Return the Columns attribute or None if it is not found
1332    fn cols(&self) -> Option<u16> {
1333        (**self)
1334            .get(dicom_dictionary_std::tags::COLUMNS)?
1335            .uint16()
1336            .ok()
1337    }
1338
1339    /// Return the SamplesPerPixel attribute or None if it is not found
1340    fn samples_per_pixel(&self) -> Option<u16> {
1341        (**self)
1342            .get(dicom_dictionary_std::tags::SAMPLES_PER_PIXEL)?
1343            .uint16()
1344            .ok()
1345    }
1346
1347    /// Return the BitsAllocated attribute or None if it is not set
1348    fn bits_allocated(&self) -> Option<u16> {
1349        (**self)
1350            .get(dicom_dictionary_std::tags::BITS_ALLOCATED)?
1351            .uint16()
1352            .ok()
1353    }
1354
1355    /// Return the BitsStored attribute or None if it is not set
1356    fn bits_stored(&self) -> Option<u16> {
1357        (**self)
1358            .get(dicom_dictionary_std::tags::BITS_STORED)?
1359            .uint16()
1360            .ok()
1361    }
1362
1363    fn photometric_interpretation(&self) -> Option<&str> {
1364        (**self)
1365            .get(dicom_dictionary_std::tags::PHOTOMETRIC_INTERPRETATION)?
1366            .string()
1367            .ok()
1368            .map(|s| s.trim_end())
1369    }
1370
1371    /// Return the NumberOfFrames attribute or None if it is not set
1372    fn number_of_frames(&self) -> Option<u32> {
1373        (**self)
1374            .get(dicom_dictionary_std::tags::NUMBER_OF_FRAMES)?
1375            .to_int()
1376            .ok()
1377    }
1378
1379    /// Returns the number of fragments or None for native pixel data
1380    fn number_of_fragments(&self) -> Option<u32> {
1381        let pixel_data = (**self).get(dicom_dictionary_std::tags::PIXEL_DATA)?;
1382        match pixel_data.value() {
1383            DicomValue::Primitive(_p) => Some(1),
1384            DicomValue::PixelSequence(v) => Some(v.fragments().len() as u32),
1385            DicomValue::Sequence(..) => None,
1386        }
1387    }
1388
1389    /// Return a specific encoded pixel fragment by index as a `Vec<u8>`
1390    /// or `None` if no pixel data is found.
1391    ///
1392    /// Non-encapsulated pixel data can be retrieved by requesting fragment #0.
1393    ///
1394    /// Panics if `fragment` is out of bounds for the encapsulated pixel data fragments.
1395    fn fragment(&self, fragment: usize) -> Option<Cow<'_, [u8]>> {
1396        let pixel_data = (**self).get(dicom_dictionary_std::tags::PIXEL_DATA)?;
1397        match pixel_data.value() {
1398            DicomValue::PixelSequence(v) => Some(Cow::Borrowed(v.fragments()[fragment].as_ref())),
1399            DicomValue::Primitive(p) if fragment == 0 => Some(p.to_bytes()),
1400            _ => None,
1401        }
1402    }
1403
1404    fn offset_table(&self) -> Option<Cow<'_, [u32]>> {
1405        let pixel_data = (**self).get(dicom_dictionary_std::tags::PIXEL_DATA)?;
1406        match pixel_data.value() {
1407            DicomValue::Primitive(_) => None,
1408            DicomValue::Sequence(_) => None,
1409            DicomValue::PixelSequence(seq) => Some(Cow::from(seq.offset_table())),
1410        }
1411    }
1412
1413    /// Should return either a byte slice/vector if native pixel data
1414    /// or byte fragments if encapsulated.
1415    /// Returns None if no pixel data is found
1416    fn raw_pixel_data(&self) -> Option<RawPixelData> {
1417        let pixel_data = (**self).get(dicom_dictionary_std::tags::PIXEL_DATA)?;
1418        match pixel_data.value() {
1419            DicomValue::Primitive(p) => {
1420                // Create 1 fragment with all bytes
1421                let fragment = p.to_bytes().to_vec();
1422                let mut fragments = SmallVec::new();
1423                fragments.push(fragment);
1424                Some(RawPixelData {
1425                    fragments,
1426                    offset_table: SmallVec::new(),
1427                })
1428            }
1429            DicomValue::PixelSequence(v) => {
1430                let (offset_table, fragments) = v.clone().into_parts();
1431                Some(RawPixelData {
1432                    fragments,
1433                    offset_table,
1434                })
1435            }
1436            DicomValue::Sequence(..) => None,
1437        }
1438    }
1439}
1440
1441#[cfg(test)]
1442mod tests {
1443    use dicom_core::{DataElement, PrimitiveValue, VR};
1444    use dicom_dictionary_std::{tags, uids};
1445
1446    use crate::meta::FileMetaTableBuilder;
1447    use crate::{AccessError, DicomAttribute as _, DicomObject, FileDicomObject, InMemDicomObject};
1448
1449    fn assert_type_not_too_large<T>(max_size: usize) {
1450        let size = std::mem::size_of::<T>();
1451        if size > max_size {
1452            panic!(
1453                "Type {} of byte size {} exceeds acceptable size {}",
1454                std::any::type_name::<T>(),
1455                size,
1456                max_size
1457            );
1458        }
1459    }
1460
1461    #[test]
1462    fn errors_not_too_large() {
1463        assert_type_not_too_large::<AccessError>(64);
1464    }
1465
1466    #[test]
1467    fn smoke_test() {
1468        const FILE_NAME: &str = ".smoke-test.dcm";
1469
1470        let meta = FileMetaTableBuilder::new()
1471            .transfer_syntax(
1472                dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.uid(),
1473            )
1474            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
1475            .media_storage_sop_instance_uid("1.2.3.456")
1476            .implementation_class_uid("1.2.345.6.7890.1.234")
1477            .build()
1478            .unwrap();
1479        let obj = FileDicomObject::new_empty_with_meta(meta);
1480
1481        obj.write_to_file(FILE_NAME).unwrap();
1482
1483        let obj2 = FileDicomObject::open_file(FILE_NAME).unwrap();
1484
1485        assert_eq!(obj, obj2);
1486
1487        let _ = std::fs::remove_file(FILE_NAME);
1488    }
1489
1490    /// A FileDicomObject<InMemDicomObject>
1491    /// can be used like a DICOM object.
1492    #[test]
1493    fn file_dicom_object_can_use_inner() {
1494        let mut obj = InMemDicomObject::new_empty();
1495
1496        obj.put(DataElement::new(
1497            dicom_dictionary_std::tags::PATIENT_NAME,
1498            VR::PN,
1499            PrimitiveValue::from("John Doe"),
1500        ));
1501
1502        let mut obj = obj
1503            .with_meta(
1504                FileMetaTableBuilder::new()
1505                    .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7")
1506                    .media_storage_sop_instance_uid("1.2.23456789")
1507                    .transfer_syntax("1.2.840.10008.1.2.1"),
1508            )
1509            .unwrap();
1510
1511        // contains patient name
1512        assert_eq!(
1513            obj.element(tags::PATIENT_NAME)
1514                .unwrap()
1515                .value()
1516                .to_str()
1517                .unwrap(),
1518            "John Doe",
1519        );
1520
1521        // same result using DicomObject
1522        assert_eq!(
1523            DicomObject::attr(&obj, tags::PATIENT_NAME)
1524                .unwrap()
1525                .to_str()
1526                .unwrap(),
1527            "John Doe",
1528        );
1529
1530        // can be removed with take
1531        obj.take_element(tags::PATIENT_NAME).unwrap();
1532
1533        assert!(matches!(
1534            obj.element(tags::PATIENT_NAME),
1535            Err(AccessError::NoSuchDataElementTag { .. }),
1536        ));
1537    }
1538
1539    #[test]
1540    fn file_dicom_object_can_iterate_over_elements() {
1541        let mut obj = InMemDicomObject::new_empty();
1542
1543        obj.put(DataElement::new(
1544            dicom_dictionary_std::tags::PATIENT_NAME,
1545            VR::PN,
1546            PrimitiveValue::from("John Doe"),
1547        ));
1548        obj.put(DataElement::new(
1549            dicom_dictionary_std::tags::SOP_INSTANCE_UID,
1550            VR::PN,
1551            PrimitiveValue::from("1.2.987654321"),
1552        ));
1553
1554        let obj = obj
1555            .with_meta(
1556                FileMetaTableBuilder::new()
1557                    .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7")
1558                    .media_storage_sop_instance_uid("1.2.987654321")
1559                    .transfer_syntax("1.2.840.10008.1.2.1"),
1560            )
1561            .unwrap();
1562
1563        // iter
1564        let mut iter = (&obj).into_iter();
1565        assert_eq!(
1566            iter.next().unwrap().header().tag,
1567            dicom_dictionary_std::tags::SOP_INSTANCE_UID
1568        );
1569        assert_eq!(
1570            iter.next().unwrap().header().tag,
1571            dicom_dictionary_std::tags::PATIENT_NAME
1572        );
1573        assert_eq!(iter.next(), None);
1574
1575        // into_iter
1576        let mut iter = obj.into_iter();
1577        assert_eq!(
1578            iter.next().unwrap().header().tag,
1579            dicom_dictionary_std::tags::SOP_INSTANCE_UID
1580        );
1581        assert_eq!(
1582            iter.next().unwrap().header().tag,
1583            dicom_dictionary_std::tags::PATIENT_NAME
1584        );
1585        assert_eq!(iter.next(), None);
1586    }
1587
1588    /// Can access file meta properties via the DicomObject trait
1589    #[test]
1590    pub fn file_dicom_can_update_meta() {
1591        let meta = FileMetaTableBuilder::new()
1592            .transfer_syntax(
1593                dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.uid(),
1594            )
1595            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
1596            .media_storage_sop_instance_uid("2.25.280986007517028771599125034987786349815")
1597            .implementation_class_uid("1.2.345.6.7890.1.234")
1598            .build()
1599            .unwrap();
1600        let mut obj = FileDicomObject::new_empty_with_meta(meta);
1601
1602        obj.update_meta(|meta| {
1603            meta.receiving_application_entity_title = Some("SOMETHING".to_string());
1604        });
1605
1606        assert_eq!(
1607            obj.meta().receiving_application_entity_title.as_deref(),
1608            Some("SOMETHING"),
1609        );
1610    }
1611
1612    #[test]
1613    fn dicom_object_api_on_file_dicom_object() {
1614        use crate::{DicomAttribute as _, DicomObject as _};
1615
1616        let meta = FileMetaTableBuilder::new()
1617            .transfer_syntax(uids::RLE_LOSSLESS)
1618            .media_storage_sop_class_uid(uids::ENHANCED_MR_IMAGE_STORAGE)
1619            .media_storage_sop_instance_uid("2.25.94766187067244888884745908966163363746")
1620            .build()
1621            .unwrap();
1622        let obj = FileDicomObject::new_empty_with_meta(meta);
1623
1624        assert_eq!(
1625            obj.attr(tags::TRANSFER_SYNTAX_UID)
1626                .unwrap()
1627                .to_str()
1628                .unwrap(),
1629            uids::RLE_LOSSLESS
1630        );
1631
1632        let sop_class_uid = obj.attr_opt(tags::MEDIA_STORAGE_SOP_CLASS_UID).unwrap();
1633        let sop_class_uid = sop_class_uid.as_ref().map(|v| v.to_str().unwrap());
1634        assert_eq!(
1635            sop_class_uid.as_deref(),
1636            Some(uids::ENHANCED_MR_IMAGE_STORAGE)
1637        );
1638
1639        assert_eq!(
1640            obj.attr_by_name("MediaStorageSOPInstanceUID")
1641                .unwrap()
1642                .to_str()
1643                .unwrap(),
1644            "2.25.94766187067244888884745908966163363746"
1645        );
1646
1647        assert_eq!(
1648            obj.at(tags::MEDIA_STORAGE_SOP_INSTANCE_UID)
1649                .unwrap()
1650                .to_str()
1651                .unwrap(),
1652            "2.25.94766187067244888884745908966163363746"
1653        );
1654    }
1655
1656    #[test]
1657    fn operations_api_on_primitive_values() {
1658        use crate::DicomAttribute;
1659        use dicom_dictionary_std::tags;
1660
1661        let obj = InMemDicomObject::from_element_iter([
1662            DataElement::new(tags::PATIENT_NAME, VR::PN, PrimitiveValue::from("Doe^John")),
1663            DataElement::new(tags::INSTANCE_NUMBER, VR::IS, PrimitiveValue::from("5")),
1664        ]);
1665
1666        let patient_name = obj.get(tags::PATIENT_NAME).unwrap().value();
1667
1668        // can get string using DICOM attribute API
1669
1670        assert_eq!(&DicomAttribute::to_str(patient_name).unwrap(), "Doe^John");
1671
1672        // can get integer from Instance Number
1673
1674        let instance_number = obj.get(tags::INSTANCE_NUMBER).unwrap().value();
1675        assert_eq!(DicomAttribute::to_u32(instance_number).unwrap(), 5);
1676
1677        // cannot get items
1678
1679        assert_eq!(patient_name.num_items(), None);
1680        assert_eq!(patient_name.num_fragments(), None);
1681        assert!(matches!(
1682            patient_name.item(0),
1683            Err(crate::AttributeError::NotDataSet)
1684        ));
1685
1686        assert_eq!(instance_number.num_items(), None);
1687        assert_eq!(instance_number.num_fragments(), None);
1688        assert!(matches!(
1689            patient_name.item(0),
1690            Err(crate::AttributeError::NotDataSet)
1691        ));
1692        assert!(matches!(
1693            instance_number.item(0),
1694            Err(crate::AttributeError::NotDataSet)
1695        ));
1696    }
1697}