Skip to main content

dicom_toolkit_data/
file_format.rs

1//! High-level DICOM file API.
2//!
3//! `FileFormat` is the top-level object representing a DICOM Part 10 file:
4//! it bundles the File Meta Information with the dataset.
5
6use crate::dataset::DataSet;
7use crate::meta_info::FileMetaInformation;
8use dicom_toolkit_core::error::DcmResult;
9use std::path::Path;
10
11/// A DICOM Part 10 file: File Meta Information + dataset.
12#[derive(Debug, Clone)]
13pub struct FileFormat {
14    pub meta: FileMetaInformation,
15    pub dataset: DataSet,
16}
17
18impl FileFormat {
19    pub fn new(meta: FileMetaInformation, dataset: DataSet) -> Self {
20        Self { meta, dataset }
21    }
22
23    /// Create from a dataset, auto-generating File Meta Information.
24    ///
25    /// Uses Explicit VR Little Endian as the default transfer syntax.
26    pub fn from_dataset(sop_class_uid: &str, sop_instance_uid: &str, dataset: DataSet) -> Self {
27        let meta = FileMetaInformation::new(sop_class_uid, sop_instance_uid, "1.2.840.10008.1.2.1");
28        Self { meta, dataset }
29    }
30
31    /// Open a DICOM file from disk.
32    pub fn open(path: impl AsRef<Path>) -> DcmResult<Self> {
33        let data = std::fs::read(path.as_ref())?;
34        crate::io::reader::parse_file(&data)
35    }
36
37    /// Save this file to disk using its current transfer syntax.
38    pub fn save(&self, path: impl AsRef<Path>) -> DcmResult<()> {
39        let bytes = crate::io::writer::encode_file(self)?;
40        std::fs::write(path.as_ref(), &bytes)?;
41        Ok(())
42    }
43
44    /// Save to disk using a specific transfer syntax.
45    ///
46    /// Only the transfer syntax UID in the meta is updated; no pixel data
47    /// transcoding is performed.
48    pub fn save_as(&self, path: impl AsRef<Path>, ts_uid: &str) -> DcmResult<()> {
49        let mut ff = self.clone();
50        ff.meta.transfer_syntax_uid = ts_uid.to_string();
51        let bytes = crate::io::writer::encode_file(&ff)?;
52        std::fs::write(path.as_ref(), &bytes)?;
53        Ok(())
54    }
55}
56
57// ── Tests ─────────────────────────────────────────────────────────────────────
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::element::Element;
63    use crate::io::reader::DicomReader;
64    use crate::io::writer::DicomWriter;
65    use dicom_toolkit_dict::{tags, Vr};
66
67    fn make_test_dataset() -> DataSet {
68        let mut ds = DataSet::new();
69        ds.insert(Element::string(tags::PATIENT_NAME, Vr::PN, "Smith^John"));
70        ds.insert(Element::u16(tags::ROWS, 512));
71        ds.insert(Element::u16(tags::COLUMNS, 256));
72        ds
73    }
74
75    #[test]
76    fn file_format_roundtrip_explicit_vr_le() {
77        let ds = make_test_dataset();
78        let ff = FileFormat::from_dataset("1.2.840.10008.5.1.4.1.1.2", "1.2.3.4.5", ds.clone());
79
80        let mut buf = Vec::new();
81        DicomWriter::new(&mut buf).write_file(&ff).unwrap();
82
83        let ff2 = DicomReader::new(buf.as_slice()).read_file().unwrap();
84        assert_eq!(ff2.dataset.get_u16(tags::ROWS), Some(512));
85        assert_eq!(ff2.dataset.get_u16(tags::COLUMNS), Some(256));
86        assert_eq!(
87            ff2.dataset.get_string(tags::PATIENT_NAME),
88            Some("Smith^John")
89        );
90    }
91
92    #[test]
93    fn file_format_roundtrip_implicit_vr_le() {
94        let mut ds = DataSet::new();
95        ds.insert(Element::u16(tags::ROWS, 128));
96        ds.insert(Element::u16(tags::COLUMNS, 128));
97
98        let mut buf = Vec::new();
99        DicomWriter::new(&mut buf)
100            .write_dataset(&ds, "1.2.840.10008.1.2")
101            .unwrap();
102
103        let ds2 = DicomReader::new(buf.as_slice())
104            .read_dataset("1.2.840.10008.1.2")
105            .unwrap();
106        assert_eq!(ds2.get_u16(tags::ROWS), Some(128));
107        assert_eq!(ds2.get_u16(tags::COLUMNS), Some(128));
108    }
109
110    #[test]
111    fn file_format_roundtrip_with_sequence() {
112        let mut item = DataSet::new();
113        item.insert(Element::string(tags::PATIENT_ID, Vr::LO, "PID001"));
114
115        let mut ds = DataSet::new();
116        ds.insert(Element::sequence(tags::REFERENCED_SOP_SEQUENCE, vec![item]));
117
118        let ff = FileFormat::from_dataset("", "", ds);
119        let mut buf = Vec::new();
120        DicomWriter::new(&mut buf).write_file(&ff).unwrap();
121
122        let ff2 = DicomReader::new(buf.as_slice()).read_file().unwrap();
123        let items = ff2
124            .dataset
125            .get_items(tags::REFERENCED_SOP_SEQUENCE)
126            .unwrap();
127        assert_eq!(items.len(), 1);
128        assert_eq!(items[0].get_string(tags::PATIENT_ID), Some("PID001"));
129    }
130
131    #[test]
132    fn file_format_from_disk_roundtrip() {
133        let dir = tempfile::tempdir().unwrap();
134        let path = dir.path().join("test.dcm");
135
136        let ds = make_test_dataset();
137        let ff = FileFormat::from_dataset("", "1.2.3", ds);
138        ff.save(&path).unwrap();
139
140        let loaded = FileFormat::open(&path).unwrap();
141        assert_eq!(loaded.dataset.get_u16(tags::ROWS), Some(512));
142        assert_eq!(
143            loaded.dataset.get_string(tags::PATIENT_NAME),
144            Some("Smith^John")
145        );
146    }
147
148    #[test]
149    fn file_format_save_as_different_ts() {
150        let dir = tempfile::tempdir().unwrap();
151        let path1 = dir.path().join("explicit.dcm");
152
153        let ds = make_test_dataset();
154        let ff = FileFormat::from_dataset("", "1.2.3", ds);
155        ff.save_as(&path1, "1.2.840.10008.1.2").unwrap();
156
157        let loaded = FileFormat::open(&path1).unwrap();
158        assert_eq!(loaded.meta.transfer_syntax_uid, "1.2.840.10008.1.2");
159    }
160
161    #[test]
162    fn file_format_meta_generated() {
163        let ff = FileFormat::from_dataset("1.2.840.10008.5.1.4.1.1.2", "9.8.7.6.5", DataSet::new());
164        assert_eq!(ff.meta.transfer_syntax_uid, "1.2.840.10008.1.2.1");
165        assert_eq!(
166            ff.meta.media_storage_sop_class_uid,
167            "1.2.840.10008.5.1.4.1.1.2"
168        );
169        assert_eq!(ff.meta.media_storage_sop_instance_uid, "9.8.7.6.5");
170    }
171}