dicom_anonymization/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//! Anonymizes a DICOM object based on the configured actions.
//!
//! This module provides functionality to anonymize DICOM (Digital Imaging and Communications in Medicine) objects
//! by applying various actions to specific DICOM tags. The anonymization process can remove, empty, or change
//! the content of certain data elements based on the configuration.
//!
//! The main components of this module are:
//! - [`ConfigBuilder`]: Struct for building the configuration.
//! - [`DataElementProcessor`]: Processes the various data elements based on the configuration.
//! - [`Anonymizer`]: The main struct that performs the anonymization process.
//! - [`AnonymizationResult`]: The result of the anonymization process.
//!
//! # Example
//!
//! ```
//! use std::fs::File;
//! use dicom_anonymization::Anonymizer;
//! use dicom_anonymization::config::ConfigBuilder;
//! use dicom_anonymization::processor::DataElementProcessor;
//!
//! let config_builder = ConfigBuilder::default();
//! let config = config_builder.build();
//!
//! let processor = DataElementProcessor::new(config);
//! let anonymizer = Anonymizer::new(processor);
//!
//! let file = File::open("tests/data/test.dcm").unwrap();
//! let result = anonymizer.anonymize(file).unwrap();
//! ```
//!
//! This module is designed to be flexible, allowing users to customize the anonymization process
//! according to their specific requirements and privacy regulations.

pub mod actions;
pub mod config;
mod functions;
mod hashing;
pub mod processor;
mod vr;

use std::io::{Read, Write};

use crate::config::ConfigBuilder;
use crate::processor::{DataElementProcessor, Error as ProcessingError};
use dicom_dictionary_std::tags;
use dicom_object::{DefaultDicomObject, FileDicomObject, OpenFileOptions, ReadError, WriteError};
use processor::Processor;
use thiserror::Error;

/// Represents the result of a DICOM anonymization process.
///
/// This struct contains both the original and anonymized DICOM objects after processing.
/// It allows access to both versions for comparison or verification purposes.
///
/// # Fields
///
/// * `original` - The original, unmodified DICOM object before anonymization
/// * `anonymized` - The resulting DICOM object after anonymization
#[derive(Debug, Clone, PartialEq)]
pub struct AnonymizationResult {
    pub original: DefaultDicomObject,
    pub anonymized: DefaultDicomObject,
}

#[derive(Error, Debug, PartialEq)]
pub enum AnonymizationError {
    #[error("Read error: {}", .0.to_lowercase())]
    ReadError(String),

    #[error("Write error: {}", .0.to_lowercase())]
    WriteError(String),

    #[error("{0}")]
    ProcessingError(String),
}

impl From<ReadError> for AnonymizationError {
    fn from(err: ReadError) -> Self {
        AnonymizationError::ReadError(format!("{err}"))
    }
}

impl From<WriteError> for AnonymizationError {
    fn from(err: WriteError) -> Self {
        AnonymizationError::WriteError(format!("{err}"))
    }
}

impl From<ProcessingError> for AnonymizationError {
    fn from(err: ProcessingError) -> Self {
        AnonymizationError::ProcessingError(format!("{err}"))
    }
}

pub type Result<T, E = AnonymizationError> = std::result::Result<T, E>;

impl AnonymizationResult {
    /// Writes the anonymized DICOM object to the provided writer.
    ///
    /// # Arguments
    ///
    /// * `to` - A writer implementing the `Write` trait where the anonymized DICOM object will be written to.
    ///
    /// # Returns
    ///
    /// Returns a `Result<()>` indicating success or an error if the write operation fails.
    ///
    /// # Example
    ///
    /// ```
    /// use std::fs::File;
    /// use dicom_anonymization::Anonymizer;
    ///
    /// let anonymizer = Anonymizer::default();
    /// let file = File::open("tests/data/test.dcm").unwrap();
    /// let result = anonymizer.anonymize(file).unwrap();
    ///
    /// // output can be a file or anything else that implements the `Write` trait
    /// let mut output = Vec::new();
    /// result.write(&mut output).unwrap();
    /// ```
    pub fn write<W: Write>(&self, to: W) -> Result<()> {
        self.anonymized.write_all(to)?;
        Ok(())
    }
}

/// A struct for performing the anonymization process on DICOM objects.
///
/// The [`Anonymizer`] contains a `Box<dyn Processor>` which performs the actual anonymization by applying
/// processor-defined transformations to DICOM data elements. The processor must implement both the `Processor`
/// trait and be `Sync`.
pub struct Anonymizer {
    processor: Box<dyn Processor + Sync>,
}

impl<'a> Anonymizer {
    pub fn new<T>(processor: T) -> Self
    where
        T: Processor + 'a + Sync + 'static,
    {
        Self {
            processor: Box::new(processor),
        }
    }

    /// Performs the anonymization process on the given DICOM object.
    ///
    /// This function takes a source implementing the `Read` trait and returns an [`AnonymizationResult`]
    /// containing both the original and anonymized DICOM objects.
    ///
    /// # Arguments
    ///
    /// * `src` - A source implementing the `Read` trait containing a DICOM object
    ///
    /// # Returns
    ///
    /// Returns a `Result` containing the [`AnonymizationResult`] if successful, or an
    /// [`AnonymizationError`] if the anonymization process fails in some way.
    ///
    /// # Example
    ///
    /// ```
    /// use std::fs::File;
    /// use dicom_anonymization::Anonymizer;
    ///
    /// let anonymizer = Anonymizer::default();
    /// let file = File::open("tests/data/test.dcm").unwrap();
    /// let result = anonymizer.anonymize(file).unwrap();
    /// ```
    pub fn anonymize(&self, src: impl Read) -> Result<AnonymizationResult> {
        let obj = OpenFileOptions::new().from_reader(src)?;
        let mut new_obj = FileDicomObject::new_empty_with_meta(obj.meta().clone());

        for elem in &obj {
            let result = self.processor.process_element(&obj, elem);
            match result {
                Ok(None) => continue,
                Ok(Some(processed_elem)) => {
                    new_obj.put(processed_elem.into_owned());
                }
                Err(err) => return Err(err.into()),
            }
        }

        // Make `MediaStorageSOPInstanceUID` the same as `SOPInstanceUID`
        if let Ok(elem) = new_obj.element(tags::SOP_INSTANCE_UID) {
            let sop_instance_uid = elem.value().clone();
            let meta = new_obj.meta_mut();
            if let Ok(sop_instance_uid_str) = sop_instance_uid.to_str() {
                meta.media_storage_sop_instance_uid = sop_instance_uid_str.into_owned();
                meta.update_information_group_length();
            }
        }

        // Make `MediaStorageSOPClassUID` the same as `SOPClassUID`
        if let Ok(elem) = new_obj.element(tags::SOP_CLASS_UID) {
            let sop_class_uid = elem.value().clone();
            let meta = new_obj.meta_mut();
            if let Ok(sop_class_uid_str) = sop_class_uid.to_str() {
                meta.media_storage_sop_class_uid = sop_class_uid_str.into_owned();
                meta.update_information_group_length();
            }
        }

        Ok(AnonymizationResult {
            original: obj,
            anonymized: new_obj,
        })
    }
}

impl Default for Anonymizer {
    /// Returns a default instance of [`Anonymizer`] with standard anonymization settings.
    ///
    /// This creates an [`Anonymizer`] with a [`DataElementProcessor`] that uses the default
    /// configuration from the [`ConfigBuilder`].
    ///
    /// # Returns
    ///
    /// A new [`Anonymizer`] instance with default settings.
    ///
    /// # Example
    ///
    /// ```
    /// use dicom_anonymization::Anonymizer;
    ///
    /// let anonymizer = Anonymizer::default();
    /// ```
    fn default() -> Self {
        let config = ConfigBuilder::default().build();
        let processor = DataElementProcessor::new(config);
        Self::new(processor)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::ConfigBuilder;
    use crate::processor::DataElementProcessor;
    use dicom_core::value::Value;
    use dicom_core::{PrimitiveValue, Tag, VR};
    use dicom_dictionary_std::tags;
    use dicom_object::mem::InMemElement;
    use dicom_object::meta::FileMetaTableBuilder;
    use dicom_object::{FileMetaTable, InMemDicomObject};

    fn make_file_meta() -> FileMetaTable {
        FileMetaTableBuilder::new()
            .media_storage_sop_class_uid("1.2.3")
            .media_storage_sop_instance_uid("2.3.4")
            .transfer_syntax("1.2.840.10008.1.2.1") // Explicit VR Little Endian
            .build()
            .unwrap()
    }

    #[test]
    fn test_anonymizer() {
        let meta = make_file_meta();
        let mut obj: FileDicomObject<InMemDicomObject> = FileDicomObject::new_empty_with_meta(meta);

        obj.put(InMemElement::new(
            tags::PATIENT_NAME,
            VR::PN,
            Value::from("John Doe"),
        ));

        obj.put(InMemElement::new(
            tags::PATIENT_ID,
            VR::LO,
            Value::from("12345"),
        ));

        obj.put(InMemElement::new(
            Tag::from([0x0033, 0x1010]),
            VR::LO,
            Value::from("I am a private tag and should be removed"),
        ));

        let mut file = Vec::new();
        obj.write_all(&mut file).unwrap();

        let config = ConfigBuilder::default().build();
        let processor = DataElementProcessor::new(config);
        let anonymizer = Anonymizer::new(processor);
        let result = anonymizer.anonymize(file.as_slice()).unwrap();

        assert!(result.anonymized.element(tags::PATIENT_NAME).is_ok());
        assert_eq!(
            result
                .anonymized
                .element(tags::PATIENT_NAME)
                .unwrap()
                .value(),
            &Value::Primitive(PrimitiveValue::Str("6652061665".to_string()))
        );

        assert!(result.anonymized.element(tags::PATIENT_ID).is_ok());
        assert_eq!(
            result.anonymized.element(tags::PATIENT_ID).unwrap().value(),
            &Value::Primitive(PrimitiveValue::from("6662505961"))
        );

        // private tag should be removed after anonymization
        assert!(result.original.element(Tag::from([0x0033, 0x1010])).is_ok());
        assert!(result
            .anonymized
            .element(Tag::from([0x0033, 0x1010]))
            .is_err());
    }
}