1#![allow(clippy::derive_partial_eq_without_eq)]
2pub 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
150pub 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
165pub const IMPLEMENTATION_CLASS_UID: &str = "2.25.262086406829110419931297894772577063974";
172
173pub const IMPLEMENTATION_VERSION_NAME: &str = "DICOM-rs 0.8.1";
178
179pub trait DicomObject {
186 type Element: Header;
187
188 fn element(&self, tag: Tag) -> Result<Self::Element, AccessError>;
190
191 fn element_by_name(&self, name: &str) -> Result<Self::Element, AccessByNameError>;
193
194 fn meta(&self) -> Option<&FileMetaTable> {
200 None
201 }
202}
203
204#[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 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#[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#[derive(Debug, Snafu)]
300#[non_exhaustive]
301pub enum PrivateElementError {
302 #[snafu(display("Group number must be odd, found {:#06x}", group))]
304 InvalidGroup { group: GroupNumber },
305 #[snafu(display("Private creator {} not found in group {:#06x}", creator, group))]
307 PrivateCreatorNotFound { creator: String, group: GroupNumber },
308 #[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 #[snafu(display("No space available in group {:#06x}", group))]
322 NoSpace { group: GroupNumber },
323}
324
325#[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#[derive(Debug, Snafu)]
351#[non_exhaustive]
352#[snafu(visibility(pub(crate)))]
353pub enum AtAccessError {
354 MissingSequence {
356 selector: AttributeSelector,
357 step_index: u32,
358 },
359 NotASequence {
361 selector: AttributeSelector,
362 step_index: u32,
363 },
364 MissingLeafElement { selector: AttributeSelector },
366}
367
368#[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 #[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 BuildMetaTable {
392 #[snafu(backtrace)]
393 source: crate::meta::Error,
394 },
395 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#[derive(Debug, Clone, PartialEq)]
407pub struct FileDicomObject<O> {
408 meta: FileMetaTable,
409 obj: O,
410}
411
412impl<O> FileDicomObject<O> {
413 pub fn meta(&self) -> &FileMetaTable {
415 &self.meta
416 }
417
418 pub fn meta_mut(&mut self) -> &mut FileMetaTable {
429 &mut self.meta
430 }
431
432 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 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 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 to.write_all(&[0_u8; 128][..])
463 .context(WriteFileSnafu { filename: path })?;
464
465 to.write_all(b"DICM")
467 .context(WriteFileSnafu { filename: path })?;
468
469 self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
471
472 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 dset_writer
482 .write_sequence((&self.obj).into_tokens())
483 .context(PrintDataSetSnafu)?;
484
485 Ok(())
486 }
487
488 pub fn write_all<W: Write>(&self, to: W) -> Result<(), WriteError> {
493 let mut to = BufWriter::new(to);
494
495 to.write_all(&[0_u8; 128][..]).context(WritePreambleSnafu)?;
497
498 to.write_all(b"DICM").context(WriteMagicCodeSnafu)?;
500
501 self.meta.write(&mut to).context(PrintMetaDataSetSnafu)?;
503
504 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 dset_writer
514 .write_sequence((&self.obj).into_tokens())
515 .context(PrintDataSetSnafu)?;
516
517 Ok(())
518 }
519
520 pub fn write_meta<W: Write>(&self, to: W) -> Result<(), WriteError> {
524 self.meta.write(to).context(PrintMetaDataSetSnafu)
525 }
526
527 pub fn write_dataset<W: Write>(&self, to: W) -> Result<(), WriteError> {
532 let to = BufWriter::new(to);
533
534 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 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
599impl<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
618impl<'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
636impl<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 fn rows(&self) -> Option<u16> {
647 self.get(dicom_dictionary_std::tags::ROWS)?.uint16().ok()
648 }
649
650 fn cols(&self) -> Option<u16> {
652 self.get(dicom_dictionary_std::tags::COLUMNS)?.uint16().ok()
653 }
654
655 fn samples_per_pixel(&self) -> Option<u16> {
657 self.get(dicom_dictionary_std::tags::SAMPLES_PER_PIXEL)?
658 .uint16()
659 .ok()
660 }
661
662 fn bits_allocated(&self) -> Option<u16> {
664 self.get(dicom_dictionary_std::tags::BITS_ALLOCATED)?
665 .uint16()
666 .ok()
667 }
668
669 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 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 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 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 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 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 #[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 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 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 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 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}