dicom 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,320 +0,0 @@
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
-
320
- end
@@ -1,156 +1,156 @@
1
- module DICOM
2
-
3
- # This module handles logging functionality.
4
- #
5
- # Logging functionality uses the Standard library's Logger class.
6
- # To properly handle progname, which inside the DICOM module is simply
7
- # "DICOM", in all cases, we use an implementation with a proxy class.
8
- #
9
- # @note For more information, please read the Standard library Logger documentation.
10
- #
11
- # @example Various logger use cases:
12
- # require 'dicom'
13
- # include DICOM
14
- #
15
- # # Logging to STDOUT with DEBUG level:
16
- # DICOM.logger = Logger.new(STDOUT)
17
- # DICOM.logger.level = Logger::DEBUG
18
- #
19
- # # Logging to a file:
20
- # DICOM.logger = Logger.new('my_logfile.log')
21
- #
22
- # # Combine an external logger with DICOM:
23
- # logger = Logger.new(STDOUT)
24
- # logger.progname = "MY_APP"
25
- # DICOM.logger = logger
26
- # # Now you can call the logger in the following ways:
27
- # DICOM.logger.info "Message" # => "DICOM: Message"
28
- # DICOM.logger.info("MY_MODULE) {"Message"} # => "MY_MODULE: Message"
29
- # logger.info "Message" # => "MY_APP: Message"
30
- #
31
- module Logging
32
-
33
- require 'logger'
34
-
35
- # Inclusion hook to make the ClassMethods available to whatever
36
- # includes the Logging module, i.e. the DICOM module.
37
- #
38
- def self.included(base)
39
- base.extend(ClassMethods)
40
- end
41
-
42
- # Class methods which the Logging module is extended with.
43
- #
44
- module ClassMethods
45
-
46
- # We use our own ProxyLogger to achieve the features wanted for DICOM logging,
47
- # e.g. using DICOM as progname for messages logged within the DICOM module
48
- # (for both the Standard logger as well as the Rails logger), while still allowing
49
- # a custom progname to be used when the logger is called outside the DICOM module.
50
- #
51
- class ProxyLogger
52
-
53
- # Creating the ProxyLogger instance.
54
- #
55
- # @param [Logger] target a logger instance (e.g. Standard Logger or ActiveSupport::BufferedLogger)
56
- #
57
- def initialize(target)
58
- @target = target
59
- end
60
-
61
- # Catches missing methods.
62
- #
63
- # In our case, the methods of interest are the typical logger methods,
64
- # i.e. log, info, fatal, error, debug, where the arguments/block are
65
- # redirected to the logger in a specific way so that our stated logger
66
- # features are achieved (this behaviour depends on the logger
67
- # (Rails vs Standard) and in the case of Standard logger,
68
- # whether or not a block is given).
69
- #
70
- # @example Inside the DICOM module or an external class with 'include DICOM::Logging':
71
- # logger.info "message"
72
- #
73
- # @example Calling from outside the DICOM module:
74
- # DICOM.logger.info "message"
75
- #
76
- def method_missing(method_name, *args, &block)
77
- if method_name.to_s =~ /(log|debug|info|warn|error|fatal)/
78
- # Rails uses it's own buffered logger which does not
79
- # work with progname + block as the standard logger does:
80
- if defined?(Rails)
81
- @target.send(method_name, "DICOM: #{args.first}")
82
- elsif block_given?
83
- @target.send(method_name, *args) { yield }
84
- else
85
- @target.send(method_name, "DICOM") { args.first }
86
- end
87
- else
88
- @target.send(method_name, *args, &block)
89
- end
90
- end
91
-
92
- end
93
-
94
- # The logger class variable (must be initialized
95
- # before it is referenced by the object setter).
96
- #
97
- @@logger = nil
98
-
99
- # The logger object getter.
100
- #
101
- # If a logger instance is not pre-defined, it sets up a Standard
102
- # logger or (if in a Rails environment) the Rails logger.
103
- #
104
- # @example Inside the DICOM module (or a class with 'include DICOM::Logging'):
105
- # logger # => Logger instance
106
- #
107
- # @example Accessing from outside the DICOM module:
108
- # DICOM.logger # => Logger instance
109
- #
110
- # @return [ProxyLogger] the logger class variable
111
- #
112
- def logger
113
- @@logger ||= lambda {
114
- if defined?(Rails)
115
- ProxyLogger.new(Rails.logger)
116
- else
117
- l = Logger.new(STDOUT)
118
- l.level = Logger::INFO
119
- ProxyLogger.new(l)
120
- end
121
- }.call
122
- end
123
-
124
- # The logger object setter.
125
- #
126
- # This method is used to replace the default logger instance with
127
- # a custom logger of your own.
128
- #
129
- # @param [Logger] l a logger instance
130
- #
131
- # @example Multiple log files
132
- # # Create a logger which ages logfile once it reaches a certain size,
133
- # # leaving 10 "old log files" with each file being about 1,024,000 bytes:
134
- # DICOM.logger = Logger.new('foo.log', 10, 1024000)
135
- #
136
- def logger=(l)
137
- @@logger = ProxyLogger.new(l)
138
- end
139
-
140
- end
141
-
142
- # A logger object getter.
143
- # Forwards the call to the logger class method of the Logging module.
144
- #
145
- # @return [ProxyLogger] the logger class variable
146
- #
147
- def logger
148
- self.class.logger
149
- end
150
-
151
- end
152
-
153
- # Include the Logging module so we can use DICOM.logger.
154
- include Logging
155
-
1
+ module DICOM
2
+
3
+ # This module handles logging functionality.
4
+ #
5
+ # Logging functionality uses the Standard library's Logger class.
6
+ # To properly handle progname, which inside the DICOM module is simply
7
+ # "DICOM", in all cases, we use an implementation with a proxy class.
8
+ #
9
+ # @note For more information, please read the Standard library Logger documentation.
10
+ #
11
+ # @example Various logger use cases:
12
+ # require 'dicom'
13
+ # include DICOM
14
+ #
15
+ # # Logging to STDOUT with DEBUG level:
16
+ # DICOM.logger = Logger.new(STDOUT)
17
+ # DICOM.logger.level = Logger::DEBUG
18
+ #
19
+ # # Logging to a file:
20
+ # DICOM.logger = Logger.new('my_logfile.log')
21
+ #
22
+ # # Combine an external logger with DICOM:
23
+ # logger = Logger.new(STDOUT)
24
+ # logger.progname = "MY_APP"
25
+ # DICOM.logger = logger
26
+ # # Now you can call the logger in the following ways:
27
+ # DICOM.logger.info "Message" # => "DICOM: Message"
28
+ # DICOM.logger.info("MY_MODULE) {"Message"} # => "MY_MODULE: Message"
29
+ # logger.info "Message" # => "MY_APP: Message"
30
+ #
31
+ module Logging
32
+
33
+ require 'logger'
34
+
35
+ # Inclusion hook to make the ClassMethods available to whatever
36
+ # includes the Logging module, i.e. the DICOM module.
37
+ #
38
+ def self.included(base)
39
+ base.extend(ClassMethods)
40
+ end
41
+
42
+ # Class methods which the Logging module is extended with.
43
+ #
44
+ module ClassMethods
45
+
46
+ # We use our own ProxyLogger to achieve the features wanted for DICOM logging,
47
+ # e.g. using DICOM as progname for messages logged within the DICOM module
48
+ # (for both the Standard logger as well as the Rails logger), while still allowing
49
+ # a custom progname to be used when the logger is called outside the DICOM module.
50
+ #
51
+ class ProxyLogger
52
+
53
+ # Creating the ProxyLogger instance.
54
+ #
55
+ # @param [Logger] target a logger instance (e.g. Standard Logger or ActiveSupport::BufferedLogger)
56
+ #
57
+ def initialize(target)
58
+ @target = target
59
+ end
60
+
61
+ # Catches missing methods.
62
+ #
63
+ # In our case, the methods of interest are the typical logger methods,
64
+ # i.e. log, info, fatal, error, debug, where the arguments/block are
65
+ # redirected to the logger in a specific way so that our stated logger
66
+ # features are achieved (this behaviour depends on the logger
67
+ # (Rails vs Standard) and in the case of Standard logger,
68
+ # whether or not a block is given).
69
+ #
70
+ # @example Inside the DICOM module or an external class with 'include DICOM::Logging':
71
+ # logger.info "message"
72
+ #
73
+ # @example Calling from outside the DICOM module:
74
+ # DICOM.logger.info "message"
75
+ #
76
+ def method_missing(method_name, *args, &block)
77
+ if method_name.to_s =~ /(log|debug|info|warn|error|fatal)/
78
+ # Rails uses it's own buffered logger which does not
79
+ # work with progname + block as the standard logger does:
80
+ if defined?(Rails)
81
+ @target.send(method_name, "DICOM: #{args.first}")
82
+ elsif block_given?
83
+ @target.send(method_name, *args) { yield }
84
+ else
85
+ @target.send(method_name, "DICOM") { args.first }
86
+ end
87
+ else
88
+ @target.send(method_name, *args, &block)
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ # The logger class variable (must be initialized
95
+ # before it is referenced by the object setter).
96
+ #
97
+ @@logger = nil
98
+
99
+ # The logger object getter.
100
+ #
101
+ # If a logger instance is not pre-defined, it sets up a Standard
102
+ # logger or (if in a Rails environment) the Rails logger.
103
+ #
104
+ # @example Inside the DICOM module (or a class with 'include DICOM::Logging'):
105
+ # logger # => Logger instance
106
+ #
107
+ # @example Accessing from outside the DICOM module:
108
+ # DICOM.logger # => Logger instance
109
+ #
110
+ # @return [ProxyLogger] the logger class variable
111
+ #
112
+ def logger
113
+ @@logger ||= lambda {
114
+ if defined?(Rails)
115
+ ProxyLogger.new(Rails.logger)
116
+ else
117
+ l = Logger.new(STDOUT)
118
+ l.level = Logger::INFO
119
+ ProxyLogger.new(l)
120
+ end
121
+ }.call
122
+ end
123
+
124
+ # The logger object setter.
125
+ #
126
+ # This method is used to replace the default logger instance with
127
+ # a custom logger of your own.
128
+ #
129
+ # @param [Logger] l a logger instance
130
+ #
131
+ # @example Multiple log files
132
+ # # Create a logger which ages logfile once it reaches a certain size,
133
+ # # leaving 10 "old log files" with each file being about 1,024,000 bytes:
134
+ # DICOM.logger = Logger.new('foo.log', 10, 1024000)
135
+ #
136
+ def logger=(l)
137
+ @@logger = ProxyLogger.new(l)
138
+ end
139
+
140
+ end
141
+
142
+ # A logger object getter.
143
+ # Forwards the call to the logger class method of the Logging module.
144
+ #
145
+ # @return [ProxyLogger] the logger class variable
146
+ #
147
+ def logger
148
+ self.class.logger
149
+ end
150
+
151
+ end
152
+
153
+ # Include the Logging module so we can use DICOM.logger.
154
+ include Logging
155
+
156
156
  end