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}