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