dicom 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -97,7 +97,14 @@ module DICOM
97
97
  # @param [String] file_name the path to be used for storing key/value pairs on disk
98
98
  #
99
99
  def write(file_name)
100
+ # Encode json string:
100
101
  str = JSON.pretty_generate(@dictionary)
102
+ # Create directory if needed:
103
+ unless File.directory?(File.dirname(file_name))
104
+ require 'fileutils'
105
+ FileUtils.mkdir_p(File.dirname(file_name))
106
+ end
107
+ # Write to file:
101
108
  File.open(file_name, 'w') {|f| f.write(str) }
102
109
  end
103
110
 
@@ -6,7 +6,7 @@ module DICOM
6
6
  # Ruby DICOM's registered DICOM UID root (Implementation Class UID).
7
7
  UID_ROOT = "1.2.826.0.1.3680043.8.641"
8
8
  # Ruby DICOM name & version (max 16 characters).
9
- NAME = "RUBY_DCM_" + DICOM::VERSION
9
+ NAME = "RUBY-DCM_" + DICOM::VERSION
10
10
 
11
11
  # Item tag.
12
12
  ITEM_TAG = "FFFE,E000"
@@ -24,15 +24,23 @@ module DICOM
24
24
  @methods_from_names = Hash.new
25
25
  @names_from_methods = Hash.new
26
26
  # Load the elements dictionary:
27
- File.open("#{ROOT_DIR}/dictionary/elements.txt").each do |record|
28
- load_element(record)
29
- end
27
+ add_element_dictionary("#{ROOT_DIR}/dictionary/elements.txt")
30
28
  # Load the unique identifiers dictionary:
31
- File.open("#{ROOT_DIR}/dictionary/uids.txt").each do |record|
32
- fields = record.split("\t")
33
- # Store the uids in a hash with uid-value as key and the uid instance as value:
34
- @uids[fields[0]] = UID.new(fields[0], fields[1], fields[2].rstrip, fields[3].rstrip)
35
- end
29
+ add_uid_dictionary("#{ROOT_DIR}/dictionary/uids.txt")
30
+ end
31
+
32
+ # Adds a custom DictionaryElement to the ruby-dicom element dictionary.
33
+ #
34
+ # @param [DictionaryElement] element the custom dictionary element to be added
35
+ #
36
+ def add_element(element)
37
+ raise ArgumentError, "Invalid argument 'element'. Expected DictionaryElement, got #{element.class}" unless element.is_a?(DictionaryElement)
38
+ # We store the elements in a hash with tag as key and the element instance as value:
39
+ @elements[element.tag] = element
40
+ # Populate the method conversion hashes with element data:
41
+ method = element.name.to_element_method
42
+ @methods_from_names[element.name] = method
43
+ @names_from_methods[method] = element.name
36
44
  end
37
45
 
38
46
  # Adds a custom dictionary file to the ruby-dicom element dictionary.
@@ -40,11 +48,36 @@ module DICOM
40
48
  # @note The format of the dictionary is a tab-separated text file with 5 columns:
41
49
  # * Tag, Name, VR, VM & Retired status
42
50
  # * For samples check out ruby-dicom's element dictionaries in the git repository
43
- # @param [String] file The path to the dictionary file to be added
51
+ # @param [String] file the path to the dictionary file to be added
44
52
  #
45
53
  def add_element_dictionary(file)
46
- File.open(file).each do |record|
47
- load_element(record)
54
+ File.open(file, :encoding => 'utf-8').each do |record|
55
+ fields = record.split("\t")
56
+ add_element(DictionaryElement.new(fields[0], fields[1], fields[2].split(","), fields[3].rstrip, fields[4].rstrip))
57
+ end
58
+ end
59
+
60
+ # Adds a custom uid (e.g. SOP Class, Transfer Syntax) to the ruby-dicom uid dictionary.
61
+ #
62
+ # @param [UID] uid the custom uid instance to be added
63
+ #
64
+ def add_uid(uid)
65
+ raise ArgumentError, "Invalid argument 'uid'. Expected UID, got #{uid.class}" unless uid.is_a?(UID)
66
+ # We store the uids in a hash with uid-value as key and the uid instance as value:
67
+ @uids[uid.value] = uid
68
+ end
69
+
70
+ # Adds a custom dictionary file to the ruby-dicom uid dictionary.
71
+ #
72
+ # @note The format of the dictionary is a tab-separated text file with 4 columns:
73
+ # * Value, Name, Type & Retired status
74
+ # * For samples check out ruby-dicom's uid dictionaries in the git repository
75
+ # @param [String] file the path to the dictionary file to be added
76
+ #
77
+ def add_uid_dictionary(file)
78
+ File.open(file, :encoding => 'utf-8').each do |record|
79
+ fields = record.split("\t")
80
+ add_uid(UID.new(fields[0], fields[1], fields[2].rstrip, fields[3].rstrip))
48
81
  end
49
82
  end
50
83
 
@@ -197,24 +230,6 @@ module DICOM
197
230
  @uids[value]
198
231
  end
199
232
 
200
-
201
- private
202
-
203
-
204
- # Loads an element to the dictionary from an element string record.
205
- #
206
- # @param [String] record a tab-separated string line as extracted from a dictionary file
207
- #
208
- def load_element(record)
209
- fields = record.split("\t")
210
- # Store the elements in a hash with tag as key and the element instance as value:
211
- element = DictionaryElement.new(fields[0], fields[1], fields[2].split(","), fields[3].rstrip, fields[4].rstrip)
212
- @elements[fields[0]] = element
213
- # Populate the method conversion hashes with element data:
214
- method = element.name.to_element_method
215
- @methods_from_names[element.name] = method
216
- @names_from_methods[method] = element.name
217
- end
218
-
219
233
  end
220
- end
234
+
235
+ end
@@ -1,18 +1,4 @@
1
- # === TODO:
2
- #
3
- # * The retrieve file network functionality (#get_image in DClient class) has not been tested.
4
- # * Make the networking code more intelligent in its handling of unexpected network communication.
5
- # * Full support for compressed image data.
6
- # * Read/Write 12 bit image data.
7
- # * Full color support (RGB and PALETTE COLOR with #image already implemented).
8
- # * Support for extraction of multiple encapsulated pixel data frames in #pixels and #narray.
9
- # * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
10
- # * More robust and flexible options for reorienting extracted pixel arrays?
11
- # * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
12
- # * Add these as github issues and remove this list!
13
-
14
-
15
- # Copyright 2008-2012 Christoffer Lervag
1
+ # Copyright 2008-2013 Christoffer Lervag
16
2
  #
17
3
  # This program is free software: you can redistribute it and/or modify
18
4
  # it under the terms of the GNU General Public License as published by
@@ -44,8 +30,12 @@ module DICOM
44
30
  attr_reader :parent
45
31
  # A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
46
32
  attr_accessor :read_success
33
+ # The source of the DObject (nil, :str or file name string).
34
+ attr_accessor :source
47
35
  # The Stream instance associated with this DObject instance (this attribute is mostly used internally).
48
36
  attr_reader :stream
37
+ # An attribute (used by e.g. DICOM.load) to indicate that a DObject-type instance was given to the load method (instead of e.g. a file).
38
+ attr_accessor :was_dcm_on_input
49
39
  # A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
50
40
  attr_reader :write_success
51
41
 
@@ -91,6 +81,7 @@ module DICOM
91
81
  dcm = self.new
92
82
  dcm.read_success = false
93
83
  end
84
+ dcm.source = link
94
85
  return dcm
95
86
  end
96
87
 
@@ -98,6 +89,7 @@ module DICOM
98
89
  #
99
90
  # @param [String] string an encoded binary string containing DICOM information
100
91
  # @param [Hash] options the options to use for parsing the DICOM string
92
+ # @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
101
93
  # @option options [Boolean] :signature if set as false, the parsing algorithm will not be looking for the DICOM header signature (defaults to true)
102
94
  # @option options [String] :syntax if a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the binary string
103
95
  # @example Parse a DICOM file that has already been loaded to a binary string
@@ -111,23 +103,26 @@ module DICOM
111
103
  raise ArgumentError, "Invalid option :syntax. Expected String, got #{options[:syntax].class}." if options[:syntax] && !options[:syntax].is_a?(String)
112
104
  signature = options[:signature].nil? ? true : options[:signature]
113
105
  dcm = self.new
114
- dcm.send(:read, string, signature, :syntax => options[:syntax])
106
+ dcm.send(:read, string, signature, :overwrite => options[:overwrite], :syntax => options[:syntax])
115
107
  if dcm.read?
116
108
  logger.debug("DICOM string successfully parsed.")
117
109
  else
118
110
  logger.warn("Failed to parse this string as DICOM.")
119
111
  end
112
+ dcm.source = :str
120
113
  return dcm
121
114
  end
122
115
 
123
116
  # Creates a DObject instance by reading and parsing a DICOM file.
124
117
  #
125
118
  # @param [String] file a string which specifies the path of the DICOM file to be loaded
119
+ # @param [Hash] options the options to use for reading the DICOM file
120
+ # @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
126
121
  # @example Load a DICOM file
127
122
  # require 'dicom'
128
123
  # dcm = DICOM::DObject.read('test.dcm')
129
124
  #
130
- def self.read(file)
125
+ def self.read(file, options={})
131
126
  raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
132
127
  # Read the file content:
133
128
  bin = nil
@@ -150,12 +145,13 @@ module DICOM
150
145
  end
151
146
  # Parse the file contents and create the DICOM object:
152
147
  if bin
153
- dcm = self.parse(bin)
148
+ dcm = self.parse(bin, options)
154
149
  # If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
155
150
  # This will help for some rare cases where the DICOM file is saved (erroneously, Im sure) with explicit encoding without specifying the transfer syntax tag.
156
151
  if !dcm.read? and !dcm.exists?("0002,0010")
157
152
  logger.info("Attempting a second decode pass (assuming Explicit Little Endian transfer syntax).")
158
- dcm = self.parse(bin, :syntax => EXPLICIT_LITTLE_ENDIAN)
153
+ options[:syntax] = EXPLICIT_LITTLE_ENDIAN
154
+ dcm = self.parse(bin, options)
159
155
  end
160
156
  else
161
157
  dcm = self.new
@@ -165,6 +161,7 @@ module DICOM
165
161
  else
166
162
  logger.warn("Reading DICOM file failed: #{file}")
167
163
  end
164
+ dcm.source = file
168
165
  return dcm
169
166
  end
170
167
 
@@ -212,6 +209,14 @@ module DICOM
212
209
 
213
210
  alias_method :eql?, :==
214
211
 
212
+ # Performs de-identification (anonymization) on the DICOM object.
213
+ #
214
+ # @param [Anonymizer] a an Anonymizer instance to use for the anonymization
215
+ #
216
+ def anonymize(a=Anonymizer.new)
217
+ a.to_anonymizer.anonymize(self)
218
+ end
219
+
215
220
  # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
216
221
  #
217
222
  # Returns the encoded binary strings in an array.
@@ -263,8 +268,17 @@ module DICOM
263
268
  # System endian:
264
269
  cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
265
270
  sys_info << "Byte Order (CPU): #{cpu}"
266
- # File path/name:
267
- info << "File: #{@file}"
271
+ # Source (file name):
272
+ if @source
273
+ if @source == :str
274
+ source = "Binary string #{@read_success ? '(successfully parsed)' : '(failed to parse)'}"
275
+ else
276
+ source = "File #{@read_success ? '(successfully read)' : '(failed to read)'}: #{@source}"
277
+ end
278
+ else
279
+ source = 'Created from scratch'
280
+ end
281
+ info << "Source: #{source}"
268
282
  # Modality:
269
283
  modality = (LIBRARY.uid(value('0008,0016')) ? LIBRARY.uid(value('0008,0016')).name : "SOP Class unknown or not specified!")
270
284
  info << "Modality: #{modality}"
@@ -376,15 +390,15 @@ module DICOM
376
390
  #
377
391
  # @param [String] file_name the path of the DICOM file which is to be written to disk
378
392
  # @param [Hash] options the options to use for writing the DICOM file
379
- # @option options [Boolean] :add_meta <DEPRECATED> if set to false, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file
380
393
  # @option options [Boolean] :ignore_meta if true, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file
394
+ # @option options [Boolean] :include_empty_parents if true, childless parents (sequences & items) are written to the DICOM file
381
395
  # @example Encode a DICOM file from a DObject
382
396
  # dcm.write('C:/dicom/test.dcm')
383
397
  #
384
398
  def write(file_name, options={})
385
- logger.warn("Option :add_meta => false is deprecated. Use option :ignore_meta => true instead.") if options[:add_meta] == false
386
399
  raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
387
- insert_missing_meta unless options[:add_meta] == false or options[:ignore_meta]
400
+ @include_empty_parents = options[:include_empty_parents]
401
+ insert_missing_meta unless options[:ignore_meta]
388
402
  write_elements(:file_name => file_name, :signature => true, :syntax => transfer_syntax)
389
403
  end
390
404
 
@@ -88,8 +88,17 @@ module DICOM
88
88
  # Create an Element from the gathered data:
89
89
  if level_vr == "SQ" or tag == ITEM_TAG
90
90
  if level_vr == "SQ"
91
- # Create a Sequence:
92
- @current_element = Sequence.new(tag, :length => length, :name => name, :parent => @current_parent, :vr => vr)
91
+ # Check for duplicate and create sequence:
92
+ logger.warn("Duplicate Sequence (#{tag}) detected at level #{@current_parent.parent.is_a?(DObject) ? 'DObject' : @current_parent.parent.tag + ' => ' if @current_parent.parent}#{@current_parent.is_a?(DObject) ? 'DObject' : @current_parent.tag}") if @current_parent[tag]
93
+ unless @current_parent[tag] and !@overwrite
94
+ @current_element = Sequence.new(tag, :length => length, :name => name, :parent => @current_parent, :vr => vr)
95
+ else
96
+ # We have skipped a sequence. This means that any following children
97
+ # of this sequence must be skipped as well. We solve this by creating an 'orphaned'
98
+ # sequence that has a parent defined, but does not add itself to this parent:
99
+ @current_element = Sequence.new(tag, :length => length, :name => name, :vr => vr)
100
+ @current_element.set_parent(@current_parent)
101
+ end
93
102
  elsif tag == ITEM_TAG
94
103
  # Create an Item:
95
104
  if @enc_image
@@ -118,10 +127,13 @@ module DICOM
118
127
  # The occurance of such a tag indicates that a sequence or item has ended, and the parent must be changed:
119
128
  @current_parent = @current_parent.parent
120
129
  else
121
- # Create an ordinary Data Element:
122
- @current_element = Element.new(tag, value, :bin => bin, :name => name, :parent => @current_parent, :vr => vr)
123
- # Check that the data stream didnt end abruptly:
124
- raise "The actual length of the value (#{@current_element.bin.length}) does not match its specified length (#{length}) for Data Element #{@current_element.tag}" if length != @current_element.bin.length
130
+ # Check for duplicate and create an ordinary data element:
131
+ logger.warn("Duplicate Element (#{tag}) detected at level #{@current_parent.parent.is_a?(DObject) ? 'DObject' : @current_parent.parent.tag + ' => ' if @current_parent.parent}#{@current_parent.is_a?(DObject) ? 'DObject' : @current_parent.tag}") if @current_parent[tag]
132
+ unless @current_parent[tag] and !@overwrite
133
+ @current_element = Element.new(tag, value, :bin => bin, :name => name, :parent => @current_parent, :vr => vr)
134
+ # Check that the data stream didn't end abruptly:
135
+ raise "The actual length of the value (#{@current_element.bin.length}) does not match its specified length (#{length}) for Data Element #{@current_element.tag}" if length != @current_element.bin.length
136
+ end
125
137
  end
126
138
  # Return true to indicate success:
127
139
  return true
@@ -132,11 +144,13 @@ module DICOM
132
144
  # @param [String] string a binary DICOM string to be parsed
133
145
  # @param [Boolean] signature if true (default), the parsing algorithm will look for the DICOM header signature
134
146
  # @param [Hash] options the options to use for parsing the DICOM string
147
+ # @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
135
148
  # @option options [String] :syntax if a syntax string is specified, the parsing algorithm is forced to use this transfer syntax when decoding the string
136
149
  #
137
150
  def read(string, signature=true, options={})
138
151
  # (Re)Set variables:
139
152
  @str = string
153
+ @overwrite = options[:overwrite]
140
154
  # Presence of the official DICOM signature:
141
155
  @signature = false
142
156
  # Default explicitness of start of DICOM string:
@@ -52,7 +52,7 @@ module DICOM
52
52
  # @param [Integer] port the network port to be used
53
53
  # @param [Hash] options the options to use for the DICOM server
54
54
  # @option options [String] :file_handler a customized FileHandler class to use instead of the default FileHandler
55
- # @option options [String] :host the hostname that the TCPServer binds to (defaults to '127.0.0.1')
55
+ # @option options [String] :host the hostname that the TCPServer binds to (defaults to '0.0.0.0')
56
56
  # @option options [String] :host_ae the name of the server (application entity)
57
57
  # @option options [String] :max_package_size the maximum allowed size of network packages (in bytes)
58
58
  # @option options [String] :timeout the number of seconds the server will wait on an answer from a client before aborting the communication
@@ -60,7 +60,7 @@ module DICOM
60
60
  # @example Create a server using default settings
61
61
  # s = DICOM::DServer.new
62
62
  # @example Create a server with a specific host name and a custom buildt file handler
63
- # require 'MyFileHandler'
63
+ # require_relative 'my_file_handler'
64
64
  # server = DICOM::DServer.new(104, :host_ae => "RUBY_SERVER", :file_handler => DICOM::MyFileHandler)
65
65
  #
66
66
  def initialize(port=104, options={})
@@ -69,7 +69,7 @@ module DICOM
69
69
  @port = port
70
70
  # Optional parameters (and default values):
71
71
  @file_handler = options[:file_handler] || FileHandler
72
- @host = options[:host] || '127.0.0.1'
72
+ @host = options[:host] || '0.0.0.0'
73
73
  @host_ae = options[:host_ae] || "RUBY_DICOM"
74
74
  @max_package_size = options[:max_package_size] || 32768 # 16384
75
75
  @timeout = options[:timeout] || 10 # seconds
@@ -155,14 +155,19 @@ module DICOM
155
155
  write_data_element(element)
156
156
  write_data_elements(element.children)
157
157
  if @enc_image
158
- write_delimiter(element) if element.tag == PIXEL_TAG # (Write a delimiter for the pixel tag, but not for it's items)
158
+ # Write a delimiter for the pixel tag, but not for its items:
159
+ write_delimiter(element) if element.tag == PIXEL_TAG
159
160
  else
160
161
  write_delimiter(element)
161
162
  end
162
163
  else
163
- # Empty sequence/item or item with binary data (We choose not to write empty, childless parents):
164
+ # Parent is childless:
164
165
  if element.bin
165
166
  write_data_element(element) if element.bin.length > 0
167
+ elsif @include_empty_parents
168
+ # Only write empty/childless parents if specifically indicated:
169
+ write_data_element(element)
170
+ write_delimiter(element)
166
171
  end
167
172
  end
168
173
  else
@@ -1,2 +1,320 @@
1
1
  module DICOM
2
+
3
+ class Anonymizer
4
+
5
+ # Adds an exception folder which will be avoided when anonymizing.
6
+ #
7
+ # @deprecated Use Anonymizer#anonymize instead.
8
+ # @param [String] path a path that will be avoided
9
+ # @example Adding a folder
10
+ # a.add_exception("/home/dicom/tutorials/")
11
+ #
12
+ def add_exception(path)
13
+ # Deprecation warning:
14
+ logger.warn("The '#add_exception' method of the Anonymization class has been deprecated! Please use the '#anonymize' method with a dataset argument instead.")
15
+ raise ArgumentError, "Expected String, got #{path.class}." unless path.is_a?(String)
16
+ if path
17
+ # Remove last character if the path ends with a file separator:
18
+ path.chop! if path[-1..-1] == File::SEPARATOR
19
+ @exceptions << path
20
+ end
21
+ end
22
+
23
+ # Adds a folder who's files will be anonymized.
24
+ #
25
+ # @deprecated Use Anonymizer#anonymize instead.
26
+ # @param [String] path a path that will be included in the anonymization
27
+ # @example Adding a folder
28
+ # a.add_folder("/home/dicom")
29
+ #
30
+ def add_folder(path)
31
+ # Deprecation warning:
32
+ logger.warn("The '#add_exception' method of the Anonymization class has been deprecated! Please use the '#anonymize' method with a dataset argument instead.")
33
+ raise ArgumentError, "Expected String, got #{path.class}." unless path.is_a?(String)
34
+ @folders << path
35
+ end
36
+
37
+ # Executes the anonymization process.
38
+ #
39
+ # This method is run when all settings have been finalized for the Anonymization instance.
40
+ #
41
+ # @deprecated Use Anonymizer#anonymize instead.
42
+ #
43
+ def execute
44
+ # Deprecation warning:
45
+ logger.warn("The '#execute' method of the Anonymization class has been deprecated! Please use the '#anonymize' method instead.")
46
+ # FIXME: This method has grown way too lengthy. It needs to be refactored one of these days.
47
+ # Search through the folders to gather all the files to be anonymized:
48
+ logger.info("Initiating anonymization process.")
49
+ start_time = Time.now.to_f
50
+ logger.info("Searching for files...")
51
+ load_files
52
+ logger.info("Done.")
53
+ if @files.length > 0
54
+ if @tags.length > 0
55
+ logger.info(@files.length.to_s + " files have been identified in the specified folder(s).")
56
+ if @write_path
57
+ # Determine the write paths, as anonymized files will be written to a separate location:
58
+ logger.info("Processing write paths...")
59
+ process_write_paths
60
+ logger.info("Done")
61
+ else
62
+ # Overwriting old files:
63
+ logger.warn("Separate write folder not specified. Existing DICOM files will be overwritten.")
64
+ @write_paths = @files
65
+ end
66
+ # If the user wants enumeration, we need to prepare variables for storing
67
+ # existing information associated with each tag:
68
+ create_enum_hash if @enumeration
69
+ # Start the read/update/write process:
70
+ logger.info("Initiating read/update/write process. This may take some time...")
71
+ # Monitor whether every file read/write was successful:
72
+ all_read = true
73
+ all_write = true
74
+ files_written = 0
75
+ files_failed_read = 0
76
+ begin
77
+ require 'progressbar'
78
+ pbar = ProgressBar.new("Anonymizing", @files.length)
79
+ rescue LoadError
80
+ pbar = nil
81
+ end
82
+ # Temporarily increase the log threshold to suppress messages from the DObject class:
83
+ anonymizer_level = logger.level
84
+ logger.level = Logger::FATAL
85
+ @files.each_index do |i|
86
+ pbar.inc if pbar
87
+ # Read existing file to DICOM object:
88
+ dcm = DObject.read(@files[i])
89
+ if dcm.read?
90
+ # Extract the data element parents to investigate for this DICOM object:
91
+ parents = element_parents(dcm)
92
+ parents.each do |parent|
93
+ # Anonymize the desired tags:
94
+ @tags.each_index do |j|
95
+ if parent.exists?(@tags[j])
96
+ element = parent[@tags[j]]
97
+ if element.is_a?(Element)
98
+ if @blank
99
+ value = ""
100
+ elsif @enumeration
101
+ old_value = element.value
102
+ # Only launch enumeration logic if there is an actual value to the data element:
103
+ if old_value
104
+ value = enumerated_value(old_value, j)
105
+ else
106
+ value = ""
107
+ end
108
+ else
109
+ # Use the value that has been set for this tag:
110
+ value = @values[j]
111
+ end
112
+ element.value = value
113
+ end
114
+ end
115
+ end
116
+ # Delete elements marked for deletion:
117
+ @delete.each_key do |tag|
118
+ parent.delete(tag) if parent.exists?(tag)
119
+ end
120
+ end
121
+ # General DICOM object manipulation:
122
+ # Add a Patient Identity Removed attribute (as per
123
+ # DICOM PS 3.15, Annex E, E.1.1 De-Identifier, point 6):
124
+ dcm.add(Element.new('0012,0062', 'YES'))
125
+ # Delete (and replace) the File Meta Information (as per
126
+ # DICOM PS 3.15, Annex E, E.1.1 De-Identifier, point 7):
127
+ dcm.delete_group('0002')
128
+ # Handle UIDs if requested:
129
+ replace_uids(parents) if @uid
130
+ # Delete private tags?
131
+ dcm.delete_private if @delete_private
132
+ # Write DICOM file:
133
+ dcm.write(@write_paths[i])
134
+ if dcm.written?
135
+ files_written += 1
136
+ else
137
+ all_write = false
138
+ end
139
+ else
140
+ all_read = false
141
+ files_failed_read += 1
142
+ end
143
+ end
144
+ pbar.finish if pbar
145
+ # Finished anonymizing files. Reset the log threshold:
146
+ logger.level = anonymizer_level
147
+ # Print elapsed time and status of anonymization:
148
+ end_time = Time.now.to_f
149
+ logger.info("Anonymization process completed!")
150
+ if all_read
151
+ logger.info("All files in the specified folder(s) were SUCCESSFULLY read to DICOM objects.")
152
+ else
153
+ logger.warn("Some files were NOT successfully read (#{files_failed_read} files). If some folder(s) contain non-DICOM files, this is expected.")
154
+ end
155
+ if all_write
156
+ logger.info("All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files).")
157
+ else
158
+ logger.warn("Some DICOM objects were NOT succesfully written to file. You are advised to investigate the result (#{files_written} files succesfully written).")
159
+ end
160
+ @audit_trail.write(@audit_trail_file) if @audit_trail
161
+ elapsed = (end_time-start_time).to_s
162
+ logger.info("Elapsed time: #{elapsed[0..elapsed.index(".")+1]} seconds")
163
+ else
164
+ logger.warn("No tags were selected for anonymization. Aborting.")
165
+ end
166
+ else
167
+ logger.warn("No files were found in specified folders. Aborting.")
168
+ end
169
+ end
170
+
171
+ # Prints to screen a list of which tags are currently selected for anonymization along with
172
+ # the replacement values that will be used and enumeration status.
173
+ #
174
+ def print
175
+ logger.warn("Anonymizer#print is deprecated.")
176
+ # Extract the string lengths which are needed to make the formatting nice:
177
+ names = Array.new
178
+ types = Array.new
179
+ tag_lengths = Array.new
180
+ name_lengths = Array.new
181
+ type_lengths = Array.new
182
+ value_lengths = Array.new
183
+ @tags.each_index do |i|
184
+ name, vr = LIBRARY.name_and_vr(@tags[i])
185
+ names << name
186
+ types << vr
187
+ tag_lengths[i] = @tags[i].length
188
+ name_lengths[i] = names[i].length
189
+ type_lengths[i] = types[i].length
190
+ value_lengths[i] = @values[i].to_s.length unless @blank
191
+ value_lengths[i] = '' if @blank
192
+ end
193
+ # To give the printed output a nice format we need to check the string lengths of some of these arrays:
194
+ tag_maxL = tag_lengths.max
195
+ name_maxL = name_lengths.max
196
+ type_maxL = type_lengths.max
197
+ value_maxL = value_lengths.max
198
+ # Format string array for print output:
199
+ lines = Array.new
200
+ @tags.each_index do |i|
201
+ # Configure empty spaces:
202
+ s = ' '
203
+ f1 = ' '*(tag_maxL-@tags[i].length+1)
204
+ f2 = ' '*(name_maxL-names[i].length+1)
205
+ f3 = ' '*(type_maxL-types[i].length+1)
206
+ f4 = ' ' if @blank
207
+ f4 = ' '*(value_maxL-@values[i].to_s.length+1) unless @blank
208
+ if @enumeration
209
+ enum = @enumerations[i]
210
+ else
211
+ enum = ''
212
+ end
213
+ if @blank
214
+ value = ''
215
+ else
216
+ value = @values[i]
217
+ end
218
+ tag = @tags[i]
219
+ lines << tag + f1 + names[i] + f2 + types[i] + f3 + value.to_s + f4 + enum.to_s
220
+ end
221
+ # Print to screen:
222
+ lines.each do |line|
223
+ puts line
224
+ end
225
+ end
226
+
227
+
228
+ private
229
+
230
+
231
+ # Finds the common path (if any) in the instance file path array, by performing a recursive search
232
+ # on the folders that make up the path of one such file.
233
+ #
234
+ # @param [Array<String>] str_arr an array of folder strings from the path of a select file
235
+ # @param [Fixnum] index the index of the folder in str_arr to check against all file paths
236
+ # @return [Fixnum] the index of the last folder in the path of the selected file that is common for all file paths
237
+ #
238
+ def common_path(str_arr, index=0)
239
+ common_folders = Array.new
240
+ # Find out how much of the path is similar for all files in @files array:
241
+ folder = str_arr[index]
242
+ all_match = true
243
+ @files.each do |f|
244
+ all_match = false unless f.include?(folder)
245
+ end
246
+ if all_match
247
+ # Need to check the next folder in the array:
248
+ result = common_path(str_arr, index + 1)
249
+ else
250
+ # Current folder did not match, which means last possible match is current index -1.
251
+ result = index - 1
252
+ end
253
+ return result
254
+ end
255
+
256
+ # Discovers all the files contained in the specified directory (all its sub-directories),
257
+ # and adds these files to the instance file array.
258
+ #
259
+ def load_files
260
+ # Load find library:
261
+ require 'find'
262
+ # Iterate through the folders (and its subfolders) to extract all files:
263
+ for dir in @folders
264
+ Find.find(dir) do |path|
265
+ if FileTest.directory?(path)
266
+ proceed = true
267
+ @exceptions.each do |e|
268
+ proceed = false if e == path
269
+ end
270
+ if proceed
271
+ next
272
+ else
273
+ Find.prune # Don't look any further into this directory.
274
+ end
275
+ else
276
+ @files << path # Store the file in our array
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ # Analyzes the write_path and the 'read' file path to determine if they have some common root.
283
+ # If there are parts of the file path that exists also in the write path, the common parts will
284
+ # not be added to the write_path. The processed paths are put in a write_path instance array.
285
+ #
286
+ def process_write_paths
287
+ @write_paths = Array.new
288
+ # First make sure @write_path ends with a file separator character:
289
+ last_character = @write_path[-1..-1]
290
+ @write_path = @write_path + File::SEPARATOR unless last_character == File::SEPARATOR
291
+ # Differing behaviour if we have one, or several files in our array:
292
+ if @files.length == 1
293
+ # Write path is requested write path + old file name:
294
+ str_arr = @files[0].split(File::SEPARATOR)
295
+ @write_paths << @write_path + str_arr.last
296
+ else
297
+ # Several files.
298
+ # Find out how much of the path they have in common, remove that and
299
+ # add the remaining to the @write_path:
300
+ str_arr = @files[0].split(File::SEPARATOR)
301
+ last_match_index = common_path(str_arr)
302
+ if last_match_index >= 0
303
+ # Remove the matching folders from the path that will be added to @write_path:
304
+ @files.each do |file|
305
+ arr = file.split(File::SEPARATOR)
306
+ part_to_write = arr[(last_match_index+1)..(arr.length-1)].join(File::SEPARATOR)
307
+ @write_paths << @write_path + part_to_write
308
+ end
309
+ else
310
+ # No common folders. Add all of original path to write path:
311
+ @files.each do |file|
312
+ @write_paths << @write_path + file
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ end
319
+
2
320
  end