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//! Loading a DICOM file can be done with ease via the function [`open_file`].
10//! For additional file reading options, use [`OpenFileOptions`].
11//! New DICOM instances can be built from scratch using [`InMemDicomObject`]
12//! (see the [`mem`] module for more details).
13//! The current default file DICOM object implementation
14//! loads the file entirely in memory.
15//! To read DICOM data sets in smaller portions,
16//! use the [DICOM collector API](collector).
17//!
18//! # Examples
19//!
20//! ### Reading DICOM
21//!
22//! Read an object and fetch some attributes:
23//!
24//! ```no_run
25//! use dicom_dictionary_std::tags;
26//! use dicom_object::open_file;
27//! # fn foo() -> Result<(), Box<dyn std::error::Error>> {
28//! let obj = open_file("0001.dcm")?;
29//!
30//! let patient_name = obj.element(tags::PATIENT_NAME)?.to_str()?;
31//! let modality = obj.element_by_name("Modality")?.to_str()?;
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! Elements can be fetched by tag,
37//! either by creating a [`Tag`]
38//! or by using one of the [readily available constants][const]
39//! from the [`dicom-dictionary-std`][dictionary-std] crate.
40//!
41//! [const]: dicom_dictionary_std::tags
42//! [dictionary-std]: https://docs.rs/dicom-dictionary-std
43//!
44//! By default, the entire data set is fully loaded into memory.
45//! The pixel data and following elements can be ignored
46//! by using [`OpenFileOptions`]:
47//!
48//! ```no_run
49//! use dicom_object::OpenFileOptions;
50//!
51//! let obj = OpenFileOptions::new()
52//!     .read_until(dicom_dictionary_std::tags::PIXEL_DATA)
53//!     .open_file("0002.dcm")?;
54//! # Result::<(), dicom_object::ReadError>::Ok(())
55//! ```
56//!
57//! Once a data set element is looked up,
58//! one will typically wish to inspect the value within.
59//! Methods are available for converting the element's DICOM value
60//! into something more usable in Rust.
61//!
62//! ```
63//! # use dicom_dictionary_std::tags;
64//! # use dicom_object::{DefaultDicomObject, Tag};
65//! # fn something(obj: DefaultDicomObject) -> Result<(), Box<dyn std::error::Error>> {
66//! let patient_date = obj.element(tags::PATIENT_BIRTH_DATE)?.to_date()?;
67//! let pixel_data_bytes = obj.element(tags::PIXEL_DATA)?.to_bytes()?;
68//! # Ok(())
69//! # }
70//! ```
71//! 
72//! ### Writing DICOM
73//!
74//! **Note:** if you need to decode the pixel data first,
75//! see the [dicom-pixeldata] crate.
76//!
77//! [dicom-pixeldata]: https://docs.rs/dicom-pixeldata
78//!
79//! Finally, DICOM objects can be serialized back into DICOM encoded bytes.
80//! A method is provided for writing a file DICOM object into a new DICOM file.
81//!
82//! ```no_run
83//! # use dicom_object::{DefaultDicomObject, Tag};
84//! # fn something(obj: DefaultDicomObject) -> Result<(), Box<dyn std::error::Error>> {
85//! obj.write_to_file("0001_new.dcm")?;
86//! # Ok(())
87//! # }
88//! ```
89//!
90//! This method requires you to write a [file meta table] first.
91//! When creating a new DICOM object from scratch,
92//! use a [`FileMetaTableBuilder`] to construct the file meta group,
93//! then use [`with_meta`] or [`with_exact_meta`]:
94//!
95//! [file meta table]: crate::meta::FileMetaTable
96//! [`FileMetaTableBuilder`]: crate::meta::FileMetaTableBuilder
97//! [`with_meta`]: crate::InMemDicomObject::with_meta
98//! [`with_exact_meta`]: crate::InMemDicomObject::with_exact_meta
99//!
100//! ```no_run
101//! # use dicom_object::{InMemDicomObject, FileMetaTableBuilder};
102//! # fn something(obj: InMemDicomObject) -> Result<(), Box<dyn std::error::Error>> {
103//! use dicom_dictionary_std::uids;
104//!
105//! let file_obj = obj.with_meta(
106//!     FileMetaTableBuilder::new()
107//!         // Implicit VR Little Endian
108//!         .transfer_syntax(uids::IMPLICIT_VR_LITTLE_ENDIAN)
109//!         // Computed Radiography image storage
110//!         .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
111//! )?;
112//! file_obj.write_to_file("0001_new.dcm")?;
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! In order to write a plain DICOM data set,
118//! use one of the various data set writing methods
119//! such as [`write_dataset_with_ts`]:
120//!
121//! [`write_dataset_with_ts`]: crate::InMemDicomObject::write_dataset_with_ts
122//! ```
123//! # use dicom_object::InMemDicomObject;
124//! # use dicom_core::{DataElement, Tag, VR};
125//! # fn run() -> Result<(), Box<dyn std::error::Error>> {
126//! // build your object
127//! let mut obj = InMemDicomObject::new_empty();
128//! let patient_name = DataElement::new(
129//!     Tag(0x0010, 0x0010),
130//!     VR::PN,
131//!     "Doe^John",
132//! );
133//! obj.put(patient_name);
134//!
135//! // write the object's data set
136//! let mut serialized = Vec::new();
137//! let ts = dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.erased();
138//! obj.write_dataset_with_ts(&mut serialized, &ts)?;
139//! assert!(!serialized.is_empty());
140//! # Ok(())
141//! # }
142//! # run().unwrap();
143//! ```
144pub mod file;
145pub mod mem;
146pub mod meta;
147pub mod ops;
148pub mod collector;
149pub mod tokens;
150
151pub use crate::file::{from_reader, open_file, OpenFileOptions};
152pub use crate::mem::InMemDicomObject;
153pub use crate::collector::{DicomCollectorOptions, DicomCollector};
154pub use crate::meta::{FileMetaTable, FileMetaTableBuilder};
155use dicom_core::ops::AttributeSelector;
156use dicom_core::DataDictionary;
157pub use dicom_core::Tag;
158pub use dicom_dictionary_std::StandardDataDictionary;
159
160/// The default implementation of a root DICOM object.
161pub type DefaultDicomObject<D = StandardDataDictionary> = FileDicomObject<mem::InMemDicomObject<D>>;
162
163use dicom_core::header::{GroupNumber, Header};
164use dicom_encoding::adapters::{PixelDataObject, RawPixelData};
165use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
166use dicom_parser::dataset::{DataSetWriter, IntoTokens};
167use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
168use smallvec::SmallVec;
169use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
170use std::borrow::Cow;
171use std::fs::File;
172use std::io::{BufWriter, Write};
173use std::path::Path;
174
175/// The current implementation class UID generically referring to DICOM-rs.
176///
177/// Automatically generated as per the standard, part 5, section B.2.
178///
179/// This UID may change in future versions,
180/// even between patch versions.
181pub const IMPLEMENTATION_CLASS_UID: &str = "2.25.262086406829110419931297894772577063974";
182
183/// The current implementation version name generically referring to DICOM-rs.
184///
185/// This name may change in future versions,
186/// even between patch versions.
187pub const IMPLEMENTATION_VERSION_NAME: &str = "DICOM-rs 0.8.1";
188
189/// Trait type for a DICOM object.
190/// This is a high-level abstraction where an object is accessed and
191/// manipulated as dictionary of entries indexed by tags, which in
192/// turn may contain a DICOM object.
193///
194/// This trait interface is experimental and prone to sudden changes.
195pub trait DicomObject {
196    type Element: Header;
197
198    /// Retrieve a particular DICOM element by its tag.
199    fn element(&self, tag: Tag) -> Result<Self::Element, AccessError>;
200
201    /// Retrieve a particular DICOM element by its name.
202    fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError>;
203
204    /// Retrieve the processed meta information table, if available.
205    ///
206    /// This table will generally not be reachable from children objects
207    /// in another object with a valid meta table. As such, it is recommended
208    /// for this method to be called at the root of a DICOM object.
209    fn meta(&self) -> Option<&FileMetaTable> {
210        None
211    }
212}
213
214/// An error which may occur when loading a DICOM object
215#[derive(Debug, Snafu)]
216#[non_exhaustive]
217pub enum ReadError {
218    #[snafu(display("Could not open file '{}'", filename.display()))]
219    OpenFile {
220        filename: std::path::PathBuf,
221        backtrace: Backtrace,
222        source: std::io::Error,
223    },
224    #[snafu(display("Could not read from file '{}'", filename.display()))]
225    ReadFile {
226        filename: std::path::PathBuf,
227        backtrace: Backtrace,
228        source: std::io::Error,
229    },
230    /// Could not read preamble bytes
231    ReadPreambleBytes {
232        backtrace: Backtrace,
233        source: std::io::Error,
234    },
235    #[snafu(display("Could not parse meta group data set"))]
236    ParseMetaDataSet {
237        #[snafu(backtrace)]
238        source: crate::meta::Error,
239    },
240    #[snafu(display("Could not parse sop attribute"))]
241    ParseSopAttribute {
242        #[snafu(source(from(dicom_core::value::ConvertValueError, Box::from)))]
243        source: Box<dicom_core::value::ConvertValueError>,
244        backtrace: Backtrace,
245    },
246    #[snafu(display("Could not create data set parser"))]
247    CreateParser {
248        #[snafu(backtrace)]
249        source: dicom_parser::dataset::read::Error,
250    },
251    #[snafu(display("Could not read data set token"))]
252    ReadToken {
253        #[snafu(backtrace)]
254        source: dicom_parser::dataset::read::Error,
255    },
256    #[snafu(display("Missing element value after header token"))]
257    MissingElementValue { backtrace: Backtrace },
258    #[snafu(display("Unsupported transfer syntax `{}`", uid))]
259    ReadUnsupportedTransferSyntax { uid: String, backtrace: Backtrace },
260    #[snafu(display("Unexpected token {:?}", token))]
261    UnexpectedToken {
262        token: Box<dicom_parser::dataset::DataToken>,
263        backtrace: Backtrace,
264    },
265    #[snafu(display("Premature data set end"))]
266    PrematureEnd { backtrace: Backtrace },
267}
268
269/// An error which may occur when writing a DICOM object
270#[derive(Debug, Snafu)]
271#[non_exhaustive]
272pub enum WriteError {
273    #[snafu(display("Could not write to file '{}'", filename.display()))]
274    WriteFile {
275        filename: std::path::PathBuf,
276        backtrace: Backtrace,
277        source: std::io::Error,
278    },
279    #[snafu(display("Could not write object preamble"))]
280    WritePreamble {
281        backtrace: Backtrace,
282        source: std::io::Error,
283    },
284    #[snafu(display("Could not write magic code"))]
285    WriteMagicCode {
286        backtrace: Backtrace,
287        source: std::io::Error,
288    },
289    #[snafu(display("Could not create data set printer"))]
290    CreatePrinter {
291        #[snafu(backtrace)]
292        source: dicom_parser::dataset::write::Error,
293    },
294    #[snafu(display("Could not print meta group data set"))]
295    PrintMetaDataSet {
296        #[snafu(backtrace)]
297        source: crate::meta::Error,
298    },
299    #[snafu(display("Could not print data set"))]
300    PrintDataSet {
301        #[snafu(backtrace)]
302        source: dicom_parser::dataset::write::Error,
303    },
304    #[snafu(display("Unsupported transfer syntax `{}`", uid))]
305    WriteUnsupportedTransferSyntax { uid: String, backtrace: Backtrace },
306}
307
308/// An error which may occur during private element look-up or insertion
309#[derive(Debug, Snafu)]
310#[non_exhaustive]
311pub enum PrivateElementError {
312    /// Group number must be odd
313    #[snafu(display("Group number must be odd, found {:#06x}", group))]
314    InvalidGroup { group: GroupNumber },
315    /// Private creator not found in group
316    #[snafu(display("Private creator {} not found in group {:#06x}", creator, group))]
317    PrivateCreatorNotFound { creator: String, group: GroupNumber },
318    /// Element not found in group
319    #[snafu(display(
320        "Private Creator {} found in group {:#06x}, but elem {:#06x} not found",
321        creator,
322        group,
323        elem
324    ))]
325    ElementNotFound {
326        creator: String,
327        group: GroupNumber,
328        elem: u8,
329    },
330    /// No space available for more private elements in the group
331    #[snafu(display("No space available in group {:#06x}", group))]
332    NoSpace { group: GroupNumber },
333}
334
335/// An error which may occur when looking up a DICOM object's attributes.
336#[derive(Debug, Snafu)]
337#[non_exhaustive]
338pub enum AccessError {
339    #[snafu(display("No such data element with tag {}", tag))]
340    NoSuchDataElementTag { tag: Tag, backtrace: Backtrace },
341}
342
343impl AccessError {
344    pub fn into_access_by_name(self, alias: impl Into<String>) -> AccessByNameError {
345        match self {
346            AccessError::NoSuchDataElementTag { tag, backtrace } => {
347                AccessByNameError::NoSuchDataElementAlias {
348                    tag,
349                    alias: alias.into(),
350                    backtrace,
351                }
352            }
353        }
354    }
355}
356
357/// An error which may occur when looking up a DICOM object's attributes
358/// at an arbitrary depth,
359/// such as through [`value_at`](crate::InMemDicomObject::value_at).
360#[derive(Debug, Snafu)]
361#[non_exhaustive]
362#[snafu(visibility(pub(crate)))]
363pub enum AtAccessError {
364    /// Missing intermediate sequence for {selector} at step {step_index}
365    MissingSequence {
366        selector: AttributeSelector,
367        step_index: u32,
368    },
369    /// Step {step_index} for {selector} is not a data set sequence
370    NotASequence {
371        selector: AttributeSelector,
372        step_index: u32,
373    },
374    /// Missing element at last step for {selector}
375    MissingLeafElement { selector: AttributeSelector },
376}
377
378/// An error which may occur when looking up a DICOM object's attributes
379/// by a keyword (or alias) instead of by tag.
380///
381/// These accesses incur a look-up at the data element dictionary,
382/// which may fail if no such entry exists.
383#[derive(Debug, Snafu)]
384pub enum AccessByNameError {
385    #[snafu(display("No such data element {} (with tag {})", alias, tag))]
386    NoSuchDataElementAlias {
387        tag: Tag,
388        alias: String,
389        backtrace: Backtrace,
390    },
391
392    /// Could not resolve attribute name from the data dictionary
393    #[snafu(display("Unknown data attribute named `{}`", name))]
394    NoSuchAttributeName { name: String, backtrace: Backtrace },
395}
396
397#[derive(Debug, Snafu)]
398#[non_exhaustive]
399pub enum WithMetaError {
400    /// Could not build file meta table
401    BuildMetaTable {
402        #[snafu(backtrace)]
403        source: crate::meta::Error,
404    },
405    /// Could not prepare file meta table
406    PrepareMetaTable {
407        #[snafu(source(from(dicom_core::value::ConvertValueError, Box::from)))]
408        source: Box<dicom_core::value::ConvertValueError>,
409        backtrace: Backtrace,
410    },
411}
412
413/// A root DICOM object retrieved from a standard DICOM file,
414/// containing additional information from the file meta group
415/// in a separate table value.
416#[derive(Debug, Clone, PartialEq)]
417pub struct FileDicomObject<O> {
418    meta: FileMetaTable,
419    obj: O,
420}
421
422impl<O> FileDicomObject<O> {
423    /// Retrieve the processed meta header table.
424    pub fn meta(&self) -> &FileMetaTable {
425        &self.meta
426    }
427
428    /// Retrieve a mutable reference to the processed meta header table.
429    ///
430    /// Considerable care should be taken when modifying this table,
431    /// as it may influence object reading and writing operations.
432    /// When modifying the table through this method,
433    /// the user is responsible for updating the meta information group length as well,
434    /// which can be done by calling
435    /// [`update_information_group_length`](FileMetaTable::update_information_group_length).
436    ///
437    /// See also [`update_meta`](Self::update_meta).
438    pub fn meta_mut(&mut self) -> &mut FileMetaTable {
439        &mut self.meta
440    }
441
442    /// Update the processed meta header table through a function.
443    ///
444    /// Considerable care should be taken when modifying this table,
445    /// as it may influence object reading and writing operations.
446    /// The meta information group length is updated automatically.
447    pub fn update_meta(&mut self, f: impl FnOnce(&mut FileMetaTable)) {
448        f(&mut self.meta);
449        self.meta.update_information_group_length();
450    }
451
452    /// Retrieve the inner DICOM object structure, discarding the meta table.
453    pub fn into_inner(self) -> O {
454        self.obj
455    }
456}
457
458impl<O> FileDicomObject<O>
459where
460    for<'a> &'a O: IntoTokens,
461{
462    /// Write the entire object as a DICOM file
463    /// into the given file path.
464    /// Preamble, magic code, and file meta group will be included
465    /// before the inner object.
466    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), WriteError> {
467        let path = path.as_ref();
468        let file = File::create(path).context(WriteFileSnafu { filename: path })?;
469        let mut to = BufWriter::new(file);
470
471        // write preamble
472        to.write_all(&[0_u8; 128][..])
473            .context(WriteFileSnafu { filename: path })?;
474
475        // write magic sequence
476        to.write_all(b"DICM")
477            .context(WriteFileSnafu { filename: path })?;
478
479        // write meta group
480        self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
481
482        // prepare encoder
483        let ts = TransferSyntaxRegistry
484            .get(&self.meta.transfer_syntax)
485            .with_context(|| WriteUnsupportedTransferSyntaxSnafu {
486                uid: self.meta.transfer_syntax.clone(),
487            })?;
488        let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
489
490        // We use the default options, because only the inner object knows if something needs to change
491        dset_writer
492            .write_sequence((&self.obj).into_tokens())
493            .context(PrintDataSetSnafu)?;
494
495        Ok(())
496    }
497
498    /// Write the entire object as a DICOM file
499    /// into the given writer.
500    /// Preamble, magic code, and file meta group will be included
501    /// before the inner object.
502    pub fn write_all<W: Write>(&self, to: W) -> Result<(), WriteError> {
503        let mut to = BufWriter::new(to);
504
505        // write preamble
506        to.write_all(&[0_u8; 128][..]).context(WritePreambleSnafu)?;
507
508        // write magic sequence
509        to.write_all(b"DICM").context(WriteMagicCodeSnafu)?;
510
511        // write meta group
512        self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
513
514        // prepare encoder
515        let ts = TransferSyntaxRegistry
516            .get(&self.meta.transfer_syntax)
517            .with_context(|| WriteUnsupportedTransferSyntaxSnafu {
518                uid: self.meta.transfer_syntax.clone(),
519            })?;
520        let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
521
522        // We use the default options, because only the inner object knows if something needs to change
523        dset_writer
524            .write_sequence((&self.obj).into_tokens())
525            .context(PrintDataSetSnafu)?;
526
527        Ok(())
528    }
529
530    /// Write the file meta group set into the given writer.
531    ///
532    /// This is equivalent to `self.meta().write(to)`.
533    pub fn write_meta<W: Write>(&self, to: W) -> Result<(), WriteError> {
534        self.meta.write(to).context(PrintMetaDataSetSnafu)
535    }
536
537    /// Write the inner data set into the given writer,
538    /// without preamble, magic code, nor file meta group.
539    ///
540    /// The transfer syntax is selected from the file meta table.
541    pub fn write_dataset<W: Write>(&self, to: W) -> Result<(), WriteError> {
542        let to = BufWriter::new(to);
543
544        // prepare encoder
545        let ts = TransferSyntaxRegistry
546            .get(&self.meta.transfer_syntax)
547            .with_context(|| WriteUnsupportedTransferSyntaxSnafu {
548                uid: self.meta.transfer_syntax.clone(),
549            })?;
550        let mut dset_writer = DataSetWriter::with_ts(to, ts).context(CreatePrinterSnafu)?;
551
552        // write object
553        dset_writer
554            .write_sequence((&self.obj).into_tokens())
555            .context(PrintDataSetSnafu)?;
556
557        Ok(())
558    }
559}
560
561impl<O> ::std::ops::Deref for FileDicomObject<O> {
562    type Target = O;
563
564    fn deref(&self) -> &Self::Target {
565        &self.obj
566    }
567}
568
569impl<O> ::std::ops::DerefMut for FileDicomObject<O> {
570    fn deref_mut(&mut self) -> &mut Self::Target {
571        &mut self.obj
572    }
573}
574
575impl<O> DicomObject for FileDicomObject<O>
576where
577    O: DicomObject,
578{
579    type Element = <O as DicomObject>::Element;
580
581    fn element(&self, tag: Tag) -> Result<Self::Element, AccessError> {
582        self.obj.element(tag)
583    }
584
585    fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError> {
586        self.obj.element_by_name(name)
587    }
588
589    fn meta(&self) -> Option<&FileMetaTable> {
590        Some(&self.meta)
591    }
592}
593
594impl<'a, O: 'a> DicomObject for &'a FileDicomObject<O>
595where
596    O: DicomObject,
597{
598    type Element = <O as DicomObject>::Element;
599
600    fn element(&self, tag: Tag) -> Result<Self::Element, AccessError> {
601        self.obj.element(tag)
602    }
603
604    fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError> {
605        self.obj.element_by_name(name)
606    }
607}
608
609/// This implementation creates an iterator
610/// to the elements of the underlying data set,
611/// consuming the whole object.
612/// The attributes in the file meta group are _not_ included.
613///
614/// To obtain an iterator over the meta elements,
615/// use [`meta().to_element_iter()`](FileMetaTable::to_element_iter).
616impl<O> IntoIterator for FileDicomObject<O>
617where
618    O: IntoIterator,
619{
620    type Item = <O as IntoIterator>::Item;
621    type IntoIter = <O as IntoIterator>::IntoIter;
622
623    fn into_iter(self) -> Self::IntoIter {
624        self.obj.into_iter()
625    }
626}
627
628/// This implementation creates an iterator
629/// to the elements of the underlying data set.
630/// The attributes in the file meta group are _not_ included.
631///
632/// To obtain an iterator over the meta elements,
633/// use [`meta().to_element_iter()`](FileMetaTable::to_element_iter).
634impl<'a, O> IntoIterator for &'a FileDicomObject<O>
635where
636    &'a O: IntoIterator,
637{
638    type Item = <&'a O as IntoIterator>::Item;
639    type IntoIter = <&'a O as IntoIterator>::IntoIter;
640
641    fn into_iter(self) -> Self::IntoIter {
642        (&self.obj).into_iter()
643    }
644}
645
646/// Implement basic pixeldata encoder/decoder functionality
647impl<D> PixelDataObject for FileDicomObject<InMemDicomObject<D>>
648where
649    D: DataDictionary + Clone,
650{
651    fn transfer_syntax_uid(&self) -> &str {
652        self.meta.transfer_syntax()
653    }
654
655    /// Return the Rows attribute or None if it is not found
656    fn rows(&self) -> Option<u16> {
657        self.get(dicom_dictionary_std::tags::ROWS)?.uint16().ok()
658    }
659
660    /// Return the Columns attribute or None if it is not found
661    fn cols(&self) -> Option<u16> {
662        self.get(dicom_dictionary_std::tags::COLUMNS)?.uint16().ok()
663    }
664
665    /// Return the SamplesPerPixel attribute or None if it is not found
666    fn samples_per_pixel(&self) -> Option<u16> {
667        self.get(dicom_dictionary_std::tags::SAMPLES_PER_PIXEL)?
668            .uint16()
669            .ok()
670    }
671
672    /// Return the BitsAllocated attribute or None if it is not set
673    fn bits_allocated(&self) -> Option<u16> {
674        self.get(dicom_dictionary_std::tags::BITS_ALLOCATED)?
675            .uint16()
676            .ok()
677    }
678
679    /// Return the BitsStored attribute or None if it is not set
680    fn bits_stored(&self) -> Option<u16> {
681        self.get(dicom_dictionary_std::tags::BITS_STORED)?
682            .uint16()
683            .ok()
684    }
685
686    fn photometric_interpretation(&self) -> Option<&str> {
687        self.get(dicom_dictionary_std::tags::PHOTOMETRIC_INTERPRETATION)?
688            .string()
689            .ok()
690            .map(|s| s.trim_end())
691    }
692
693    /// Return the NumberOfFrames attribute or None if it is not set
694    fn number_of_frames(&self) -> Option<u32> {
695        self.get(dicom_dictionary_std::tags::NUMBER_OF_FRAMES)?
696            .to_int()
697            .ok()
698    }
699
700    /// Returns the number of fragments or None for native pixel data
701    fn number_of_fragments(&self) -> Option<u32> {
702        let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
703        match pixel_data.value() {
704            dicom_core::DicomValue::Primitive(_p) => Some(1),
705            dicom_core::DicomValue::PixelSequence(v) => Some(v.fragments().len() as u32),
706            dicom_core::DicomValue::Sequence(..) => None,
707        }
708    }
709
710    /// Return a specific encoded pixel fragment by index as a `Vec<u8>`
711    /// or `None` if no pixel data is found.
712    ///
713    /// Non-encapsulated pixel data can be retrieved by requesting fragment #0.
714    ///
715    /// Panics if `fragment` is out of bounds for the encapsulated pixel data fragments.
716    fn fragment(&self, fragment: usize) -> Option<Cow<'_, [u8]>> {
717        let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
718        match pixel_data.value() {
719            dicom_core::DicomValue::PixelSequence(v) => {
720                Some(Cow::Borrowed(v.fragments()[fragment].as_ref()))
721            }
722            dicom_core::DicomValue::Primitive(p) if fragment == 0 => Some(p.to_bytes()),
723            _ => None,
724        }
725    }
726
727    fn offset_table(&self) -> Option<Cow<'_, [u32]>> {
728        let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
729        match pixel_data.value() {
730            dicom_core::DicomValue::Primitive(_) => None,
731            dicom_core::DicomValue::Sequence(_) => None,
732            dicom_core::DicomValue::PixelSequence(seq) => Some(Cow::from(seq.offset_table())),
733        }
734    }
735
736    /// Should return either a byte slice/vector if native pixel data
737    /// or byte fragments if encapsulated.
738    /// Returns None if no pixel data is found
739    fn raw_pixel_data(&self) -> Option<RawPixelData> {
740        let pixel_data = self.get(dicom_dictionary_std::tags::PIXEL_DATA)?;
741        match pixel_data.value() {
742            dicom_core::DicomValue::Primitive(p) => {
743                // Create 1 fragment with all bytes
744                let fragment = p.to_bytes().to_vec();
745                let mut fragments = SmallVec::new();
746                fragments.push(fragment);
747                Some(RawPixelData {
748                    fragments,
749                    offset_table: SmallVec::new(),
750                })
751            }
752            dicom_core::DicomValue::PixelSequence(v) => {
753                let (offset_table, fragments) = v.clone().into_parts();
754                Some(RawPixelData {
755                    fragments,
756                    offset_table,
757                })
758            }
759            dicom_core::DicomValue::Sequence(..) => None,
760        }
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use dicom_core::{DataElement, PrimitiveValue, VR};
767
768    use crate::meta::FileMetaTableBuilder;
769    use crate::{AccessError, FileDicomObject, InMemDicomObject};
770
771    fn assert_type_not_too_large<T>(max_size: usize) {
772        let size = std::mem::size_of::<T>();
773        if size > max_size {
774            panic!(
775                "Type {} of byte size {} exceeds acceptable size {}",
776                std::any::type_name::<T>(),
777                size,
778                max_size
779            );
780        }
781    }
782
783    #[test]
784    fn errors_not_too_large() {
785        assert_type_not_too_large::<AccessError>(64);
786    }
787
788    #[test]
789    fn smoke_test() {
790        const FILE_NAME: &str = ".smoke-test.dcm";
791
792        let meta = FileMetaTableBuilder::new()
793            .transfer_syntax(
794                dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.uid(),
795            )
796            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
797            .media_storage_sop_instance_uid("1.2.3.456")
798            .implementation_class_uid("1.2.345.6.7890.1.234")
799            .build()
800            .unwrap();
801        let obj = FileDicomObject::new_empty_with_meta(meta);
802
803        obj.write_to_file(FILE_NAME).unwrap();
804
805        let obj2 = FileDicomObject::open_file(FILE_NAME).unwrap();
806
807        assert_eq!(obj, obj2);
808
809        let _ = std::fs::remove_file(FILE_NAME);
810    }
811
812    /// A FileDicomObject<InMemDicomObject>
813    /// can be used like a DICOM object.
814    #[test]
815    fn file_dicom_object_can_use_inner() {
816        let mut obj = InMemDicomObject::new_empty();
817
818        obj.put(DataElement::new(
819            dicom_dictionary_std::tags::PATIENT_NAME,
820            VR::PN,
821            PrimitiveValue::from("John Doe"),
822        ));
823
824        let mut obj = obj
825            .with_meta(
826                FileMetaTableBuilder::new()
827                    .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7")
828                    .media_storage_sop_instance_uid("1.2.23456789")
829                    .transfer_syntax("1.2.840.10008.1.2.1"),
830            )
831            .unwrap();
832
833        // contains patient name
834        assert_eq!(
835            obj.element(dicom_dictionary_std::tags::PATIENT_NAME)
836                .unwrap()
837                .value()
838                .to_str()
839                .unwrap(),
840            "John Doe",
841        );
842
843        // can be removed with take
844        obj.take_element(dicom_dictionary_std::tags::PATIENT_NAME)
845            .unwrap();
846
847        assert!(matches!(
848            obj.element(dicom_dictionary_std::tags::PATIENT_NAME),
849            Err(AccessError::NoSuchDataElementTag { .. }),
850        ));
851    }
852
853    #[test]
854    fn file_dicom_object_can_iterate_over_elements() {
855        let mut obj = InMemDicomObject::new_empty();
856
857        obj.put(DataElement::new(
858            dicom_dictionary_std::tags::PATIENT_NAME,
859            VR::PN,
860            PrimitiveValue::from("John Doe"),
861        ));
862        obj.put(DataElement::new(
863            dicom_dictionary_std::tags::SOP_INSTANCE_UID,
864            VR::PN,
865            PrimitiveValue::from("1.2.987654321"),
866        ));
867
868        let obj = obj
869            .with_meta(
870                FileMetaTableBuilder::new()
871                    .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7")
872                    .media_storage_sop_instance_uid("1.2.987654321")
873                    .transfer_syntax("1.2.840.10008.1.2.1"),
874            )
875            .unwrap();
876
877        // iter
878        let mut iter = (&obj).into_iter();
879        assert_eq!(
880            iter.next().unwrap().header().tag,
881            dicom_dictionary_std::tags::SOP_INSTANCE_UID
882        );
883        assert_eq!(
884            iter.next().unwrap().header().tag,
885            dicom_dictionary_std::tags::PATIENT_NAME
886        );
887        assert_eq!(iter.next(), None);
888
889        // into_iter
890        let mut iter = obj.into_iter();
891        assert_eq!(
892            iter.next().unwrap().header().tag,
893            dicom_dictionary_std::tags::SOP_INSTANCE_UID
894        );
895        assert_eq!(
896            iter.next().unwrap().header().tag,
897            dicom_dictionary_std::tags::PATIENT_NAME
898        );
899        assert_eq!(iter.next(), None);
900    }
901
902    #[test]
903    pub fn file_dicom_can_update_meta() {
904        let meta = FileMetaTableBuilder::new()
905            .transfer_syntax(
906                dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.uid(),
907            )
908            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
909            .media_storage_sop_instance_uid("2.25.280986007517028771599125034987786349815")
910            .implementation_class_uid("1.2.345.6.7890.1.234")
911            .build()
912            .unwrap();
913        let mut obj = FileDicomObject::new_empty_with_meta(meta);
914
915        obj.update_meta(|meta| {
916            meta.receiving_application_entity_title = Some("SOMETHING".to_string());
917        });
918
919        assert_eq!(
920            obj.meta().receiving_application_entity_title.as_deref(),
921            Some("SOMETHING"),
922        );
923    }
924}