dicom_anonymization/
lib.rs

1//! Anonymizes a DICOM object based on the configured actions.
2//!
3//! This module provides functionality to anonymize DICOM (Digital Imaging and Communications in Medicine) objects
4//! by applying various actions to specific DICOM tags. The anonymization process can remove, empty, or change
5//! the content of certain data elements based on the configuration.
6//!
7//! The main components of this module are:
8//! - [`ConfigBuilder`]: Struct for building the configuration.
9//! - [`DefaultProcessor`]: Processes the various data elements based on the configuration.
10//! - [`Anonymizer`]: The main struct that performs the anonymization process.
11//! - [`AnonymizationResult`]: The result of the anonymization process.
12//!
13//! # Example
14//!
15//! ```
16//! use std::fs::File;
17//! use dicom_anonymization::Anonymizer;
18//! use dicom_anonymization::config::builder::ConfigBuilder;
19//! use dicom_anonymization::processor::DefaultProcessor;
20//!
21//! let config_builder = ConfigBuilder::default();
22//! let config = config_builder.build();
23//!
24//! let processor = DefaultProcessor::new(config);
25//! let anonymizer = Anonymizer::new(processor);
26//!
27//! let file = File::open("tests/data/test.dcm").unwrap();
28//! let result = anonymizer.anonymize(file).unwrap();
29//! ```
30//!
31//! This module is designed to be flexible, allowing users to customize the anonymization process
32//! according to their specific requirements and privacy regulations.
33
34pub mod actions;
35pub mod config;
36mod dicom;
37pub mod hasher;
38pub mod processor;
39mod test_utils;
40
41use std::io::{Read, Write};
42
43use crate::config::builder::ConfigBuilder;
44use crate::processor::{DefaultProcessor, Error as ProcessingError};
45pub use dicom_core::Tag;
46pub use dicom_dictionary_std::tags;
47use dicom_object::{DefaultDicomObject, FileDicomObject, OpenFileOptions, ReadError, WriteError};
48use processor::Processor;
49use thiserror::Error;
50
51/// Represents the result of a DICOM anonymization process.
52///
53/// This struct contains both the original and anonymized DICOM objects after processing.
54/// It allows access to both versions for comparison or verification purposes.
55///
56/// # Fields
57///
58/// * `original` - The original, unmodified DICOM object before anonymization
59/// * `anonymized` - The resulting DICOM object after anonymization
60#[derive(Debug, Clone, PartialEq)]
61pub struct AnonymizationResult {
62    pub original: DefaultDicomObject,
63    pub anonymized: DefaultDicomObject,
64}
65
66#[derive(Error, Debug, PartialEq)]
67pub enum AnonymizationError {
68    #[error("Read error: {}", .0.to_lowercase())]
69    ReadError(String),
70
71    #[error("Write error: {}", .0.to_lowercase())]
72    WriteError(String),
73
74    #[error("{0}")]
75    ProcessingError(String),
76}
77
78impl From<ReadError> for AnonymizationError {
79    fn from(err: ReadError) -> Self {
80        AnonymizationError::ReadError(format!("{err}"))
81    }
82}
83
84impl From<WriteError> for AnonymizationError {
85    fn from(err: WriteError) -> Self {
86        AnonymizationError::WriteError(format!("{err}"))
87    }
88}
89
90impl From<ProcessingError> for AnonymizationError {
91    fn from(err: ProcessingError) -> Self {
92        AnonymizationError::ProcessingError(format!("{err}"))
93    }
94}
95
96pub type Result<T, E = AnonymizationError> = std::result::Result<T, E>;
97
98impl AnonymizationResult {
99    /// Writes the anonymized DICOM object to the provided writer.
100    ///
101    /// # Arguments
102    ///
103    /// * `to` - A writer implementing the `Write` trait where the anonymized DICOM object will be written to.
104    ///
105    /// # Returns
106    ///
107    /// Returns a `Result<()>` indicating success or an error if the write operation fails.
108    ///
109    /// # Example
110    ///
111    /// ```
112    /// use std::fs::File;
113    /// use dicom_anonymization::Anonymizer;
114    ///
115    /// let anonymizer = Anonymizer::default();
116    /// let file = File::open("tests/data/test.dcm").unwrap();
117    /// let result = anonymizer.anonymize(file).unwrap();
118    ///
119    /// // output can be a file or anything else that implements the `Write` trait, like this one:
120    /// let mut output = Vec::<u8>::new();
121    /// result.write(&mut output).unwrap();
122    /// ```
123    pub fn write<W: Write>(&self, to: W) -> Result<()> {
124        self.anonymized.write_all(to)?;
125        Ok(())
126    }
127}
128
129/// A struct for performing the anonymization process on DICOM objects.
130///
131/// The [`Anonymizer`] contains a `Box<dyn Processor>` which performs the actual anonymization by applying
132/// processor-defined transformations to DICOM data elements. The processor must implement both the `Processor`
133/// trait and be `Sync`.
134pub struct Anonymizer {
135    processor: Box<dyn Processor + Send + Sync>,
136}
137
138impl Anonymizer {
139    pub fn new<T>(processor: T) -> Self
140    where
141        T: Processor + Send + Sync + 'static,
142    {
143        Self {
144            processor: Box::new(processor),
145        }
146    }
147
148    /// Performs the anonymization process on the given DICOM object.
149    ///
150    /// This function takes a source implementing the `Read` trait and returns an [`AnonymizationResult`]
151    /// containing both the original and anonymized DICOM objects.
152    ///
153    /// # Arguments
154    ///
155    /// * `src` - A source implementing the `Read` trait containing a DICOM object
156    ///
157    /// # Returns
158    ///
159    /// Returns a `Result` containing the [`AnonymizationResult`] if successful, or an
160    /// [`AnonymizationError`] if the anonymization process fails in some way.
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use std::fs::File;
166    /// use dicom_anonymization::Anonymizer;
167    ///
168    /// let anonymizer = Anonymizer::default();
169    /// let file = File::open("tests/data/test.dcm").unwrap();
170    /// let result = anonymizer.anonymize(file).unwrap();
171    /// ```
172    pub fn anonymize(&self, src: impl Read) -> Result<AnonymizationResult> {
173        let obj = OpenFileOptions::new().from_reader(src)?;
174        let mut new_obj = FileDicomObject::new_empty_with_meta(obj.meta().clone());
175
176        for elem in &obj {
177            let result = self.processor.process_element(&obj, elem);
178            match result {
179                Ok(None) => continue,
180                Ok(Some(processed_elem)) => {
181                    new_obj.put(processed_elem.into_owned());
182                }
183                Err(err) => return Err(err.into()),
184            }
185        }
186
187        // Make `MediaStorageSOPInstanceUID` the same as `SOPInstanceUID`
188        if let Ok(elem) = new_obj.element(tags::SOP_INSTANCE_UID) {
189            let sop_instance_uid = elem.value().clone();
190            let meta = new_obj.meta_mut();
191            if let Ok(sop_instance_uid_str) = sop_instance_uid.to_str() {
192                meta.media_storage_sop_instance_uid = sop_instance_uid_str.into_owned();
193                meta.update_information_group_length();
194            }
195        }
196
197        // Make `MediaStorageSOPClassUID` the same as `SOPClassUID`
198        if let Ok(elem) = new_obj.element(tags::SOP_CLASS_UID) {
199            let sop_class_uid = elem.value().clone();
200            let meta = new_obj.meta_mut();
201            if let Ok(sop_class_uid_str) = sop_class_uid.to_str() {
202                meta.media_storage_sop_class_uid = sop_class_uid_str.into_owned();
203                meta.update_information_group_length();
204            }
205        }
206
207        Ok(AnonymizationResult {
208            original: obj,
209            anonymized: new_obj,
210        })
211    }
212}
213
214impl Default for Anonymizer {
215    /// Returns a default instance of [`Anonymizer`] with standard anonymization settings.
216    ///
217    /// This creates an [`Anonymizer`] with a [`DefaultProcessor`] that uses the default
218    /// configuration from the [`ConfigBuilder`].
219    ///
220    /// # Returns
221    ///
222    /// A new [`Anonymizer`] instance with default settings.
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// use dicom_anonymization::Anonymizer;
228    ///
229    /// let anonymizer = Anonymizer::default();
230    /// ```
231    fn default() -> Self {
232        let config = ConfigBuilder::default().build();
233        let processor = DefaultProcessor::new(config);
234        Self::new(processor)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    use crate::tags;
243    use dicom_core::value::Value;
244    use dicom_core::{PrimitiveValue, VR};
245    use dicom_object::mem::InMemElement;
246    use dicom_object::InMemDicomObject;
247
248    use crate::config::builder::ConfigBuilder;
249    use crate::processor::DefaultProcessor;
250    use crate::test_utils::make_file_meta;
251    use crate::Tag;
252
253    #[test]
254    fn test_anonymizer() {
255        let meta = make_file_meta();
256        let mut obj: FileDicomObject<InMemDicomObject> = FileDicomObject::new_empty_with_meta(meta);
257
258        obj.put(InMemElement::new(
259            tags::PATIENT_NAME,
260            VR::PN,
261            Value::from("John Doe"),
262        ));
263
264        obj.put(InMemElement::new(
265            tags::PATIENT_ID,
266            VR::LO,
267            Value::from("12345"),
268        ));
269
270        obj.put(InMemElement::new(
271            Tag::from([0x0033, 0x1010]),
272            VR::LO,
273            Value::from("I am a private tag and should be removed"),
274        ));
275
276        let mut file = Vec::new();
277        obj.write_all(&mut file).unwrap();
278
279        let config = ConfigBuilder::default().build();
280        let processor = DefaultProcessor::new(config);
281        let anonymizer = Anonymizer::new(processor);
282        let result = anonymizer.anonymize(file.as_slice()).unwrap();
283
284        assert!(result.anonymized.element(tags::PATIENT_NAME).is_ok());
285        assert_eq!(
286            result
287                .anonymized
288                .element(tags::PATIENT_NAME)
289                .unwrap()
290                .value(),
291            &Value::Primitive(PrimitiveValue::Str("6652061665".to_string()))
292        );
293
294        assert!(result.anonymized.element(tags::PATIENT_ID).is_ok());
295        assert_eq!(
296            result.anonymized.element(tags::PATIENT_ID).unwrap().value(),
297            &Value::Primitive(PrimitiveValue::from("6662505961"))
298        );
299
300        // private tag should be removed after anonymization
301        assert!(result.original.element(Tag::from([0x0033, 0x1010])).is_ok());
302        assert!(result
303            .anonymized
304            .element(Tag::from([0x0033, 0x1010]))
305            .is_err());
306    }
307}