dicom 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,158 +1,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
- # === Examples
10
- #
11
- # require 'dicom'
12
- # include DICOM
13
- #
14
- # # Logging to STDOUT with DEBUG level:
15
- # DICOM.logger = Logger.new(STDOUT)
16
- # DICOM.logger.level = Logger::DEBUG
17
- #
18
- # # Logging to a file:
19
- # DICOM.logger = Logger.new('my_logfile.log')
20
- #
21
- # # Combine an external logger with DICOM:
22
- # logger = Logger.new(STDOUT)
23
- # logger.progname = "MY_APP"
24
- # DICOM.logger = logger
25
- # # Now you can call the logger in the following ways:
26
- # DICOM.logger.info "Message" # => "DICOM: Message"
27
- # DICOM.logger.info("MY_MODULE) {"Message"} # => "MY_MODULE: Message"
28
- # logger.info "Message" # => "MY_APP: Message"
29
- #
30
- # For more information, please read the Standard library Logger documentation.
31
- #
32
- module Logging
33
-
34
- require 'logger'
35
-
36
- # Inclusion hook to make the ClassMethods available to whatever
37
- # includes the Logging module, i.e. the DICOM module.
38
- #
39
- def self.included(base)
40
- base.extend(ClassMethods)
41
- end
42
-
43
- module ClassMethods
44
-
45
- # We use our own ProxyLogger to achieve the features wanted for DICOM logging,
46
- # e.g. using DICOM as progname for messages logged within the DICOM module
47
- # (for both the Standard logger as well as the Rails logger), while still allowing
48
- # a custom progname to be used when the logger is called outside the DICOM module.
49
- #
50
- class ProxyLogger
51
-
52
- # Creating the ProxyLogger instance.
53
- #
54
- # === Parameters
55
- #
56
- # * <tt>target</tt> -- A Logger instance (e.g. Standard Logger or ActiveSupport::BufferedLogger).
57
- #
58
- def initialize(target)
59
- @target = target
60
- end
61
-
62
- # Catches missing methods.
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
- # === Examples
71
- #
72
- # # Inside the DICOM module or an external class with 'include DICOM::Logging':
73
- # logger.info "message"
74
- #
75
- # # Calling from outside the DICOM module:
76
- # DICOM.logger.info "message"
77
- #
78
- def method_missing(method_name, *args, &block)
79
- if method_name.to_s =~ /(log|debug|info|warn|error|fatal)/
80
- # Rails uses it's own buffered logger which does not
81
- # work with progname + block as the standard logger does:
82
- if defined?(Rails)
83
- @target.send(method_name, "DICOM: #{args.first}")
84
- elsif block_given?
85
- @target.send(method_name, *args) { yield }
86
- else
87
- @target.send(method_name, "DICOM") { args.first }
88
- end
89
- else
90
- @target.send(method_name, *args, &block)
91
- end
92
- end
93
-
94
- end
95
-
96
- # The logger class variable (must be initialized
97
- # before it is referenced by the object setter).
98
- #
99
- @@logger = nil
100
-
101
- # The logger object setter.
102
- # This method is used to replace the default logger instance with
103
- # a custom logger of your own.
104
- #
105
- # === Parameters
106
- #
107
- # * <tt>l</tt> -- A Logger instance (e.g. a custom standard Logger).
108
- #
109
- # === Examples
110
- #
111
- # # Create a logger which ages logfile once it reaches a certain size,
112
- # # leaves 10 "old log files" with each file being about 1,024,000 bytes:
113
- # DICOM.logger = Logger.new('foo.log', 10, 1024000)
114
- #
115
- def logger=(l)
116
- @@logger = ProxyLogger.new(l)
117
- end
118
-
119
- # The logger object getter.
120
- # Returns the logger class variable, if defined.
121
- # If not defined, sets up the Rails logger (if in a Rails environment),
122
- # or a Standard logger if not.
123
- #
124
- # === Examples
125
- #
126
- # # Inside the DICOM module (or a class with 'include DICOM::Logging'):
127
- # logger # => Logger instance
128
- #
129
- # # Accessing from outside the DICOM module:
130
- # DICOM.logger # => Logger instance
131
- #
132
- def logger
133
- @@logger ||= lambda {
134
- if defined?(Rails)
135
- ProxyLogger.new(Rails.logger)
136
- else
137
- l = Logger.new(STDOUT)
138
- l.level = Logger::INFO
139
- ProxyLogger.new(l)
140
- end
141
- }.call
142
- end
143
-
144
- end
145
-
146
- # A logger object getter.
147
- # Forwards the call to the logger class method of the Logging module.
148
- #
149
- def logger
150
- self.class.logger
151
- end
152
-
153
- end
154
-
155
- # Include the Logging module so we can use DICOM.logger.
156
- include Logging
157
-
158
- end
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
+ # This method is used to replace the default logger instance with
126
+ # a custom logger of your own.
127
+ #
128
+ # @param [Logger] l a logger instance
129
+ #
130
+ # @example Multiple log files
131
+ # # Create a logger which ages logfile once it reaches a certain size,
132
+ # # leaving 10 "old log files" with each file being about 1,024,000 bytes:
133
+ # DICOM.logger = Logger.new('foo.log', 10, 1024000)
134
+ #
135
+ def logger=(l)
136
+ @@logger = ProxyLogger.new(l)
137
+ end
138
+
139
+ end
140
+
141
+ # A logger object getter.
142
+ # Forwards the call to the logger class method of the Logging module.
143
+ #
144
+ # @return [ProxyLogger] the logger class variable
145
+ #
146
+ def logger
147
+ self.class.logger
148
+ end
149
+
150
+ end
151
+
152
+ # Include the Logging module so we can use DICOM.logger.
153
+ include Logging
154
+
155
+ end
@@ -1,847 +1,782 @@
1
- module DICOM
2
-
3
- # Super class which contains common code for all parent elements.
4
- #
5
- # === Inheritance
6
- #
7
- # Since all parents inherit from this class, these methods are available to instances of the following classes:
8
- # * DObject
9
- # * Item
10
- # * Sequence
11
- #
12
- class Parent
13
-
14
- # Returns the specified child element.
15
- # If the requested data element isn't found, nil is returned.
16
- #
17
- # === Notes
18
- #
19
- # * Only immediate children are searched. Grandchildren etc. are not included.
20
- #
21
- # === Parameters
22
- #
23
- # * <tt>tag_or_index</tt> -- A tag string which identifies the data element to be returned (Exception: In the case where an Item is wanted, an index (Fixnum) is used instead).
24
- #
25
- # === Examples
26
- #
27
- # # Extract the "Pixel Data" data element from the DObject instance:
28
- # pixel_data_element = dcm["7FE0,0010"]
29
- # # Extract the first Item from a Sequence:
30
- # first_item = dcm["3006,0020"][1]
31
- #
32
- def [](tag_or_index)
33
- formatted = tag_or_index.is_a?(String) ? tag_or_index.upcase : tag_or_index
34
- return @tags[formatted]
35
- end
36
-
37
- # Adds a Element or Sequence instance to self (where self can be either a DObject or an Item).
38
- #
39
- # === Restrictions
40
- #
41
- # * Items can not be added with this method.
42
- #
43
- # === Parameters
44
- #
45
- # * <tt>element</tt> -- An element (Element or Sequence).
46
- # * <tt>options</tt> -- A hash of parameters.
47
- #
48
- # === Options
49
- #
50
- # * <tt>:no_follow</tt> -- Boolean. If true, the method does not update the parent attribute of the child that is added.
51
- #
52
- # === Examples
53
- #
54
- # # Set a new patient's name to the DICOM object:
55
- # dcm.add(Element.new("0010,0010", "John_Doe"))
56
- # # Add a previously defined element roi_name to the first item in the following sequence:
57
- # dcm["3006,0020"][0].add(roi_name)
58
- #
59
- def add(element, options={})
60
- unless element.is_a?(Item)
61
- unless self.is_a?(Sequence)
62
- # Does the element's binary value need to be reencoded?
63
- reencode = true if element.is_a?(Element) && element.endian != stream.str_endian
64
- # If we are replacing an existing Element, we need to make sure that this Element's parent value is erased before proceeding.
65
- self[element.tag].parent = nil if exists?(element.tag)
66
- # Add the element, and set its parent attribute:
67
- @tags[element.tag] = element
68
- element.parent = self unless options[:no_follow]
69
- # As the element has been moved in place, perform re-encode if indicated:
70
- element.value = element.value if reencode
71
- else
72
- raise "A Sequence is only allowed to have Item elements added to it. Use add_item() instead if the intention is to add an Item."
73
- end
74
- else
75
- raise ArgumentError, "An Item is not allowed as a parameter to the add() method. Use add_item() instead."
76
- end
77
- end
78
-
79
- # Adds a child item to a Sequence (or Item in some cases where pixel data is encapsulated).
80
- # If no existing Item is specified, an empty item will be added.
81
- #
82
- # === Notes
83
- #
84
- # * Items are specified by index (starting at 0) instead of a tag string!
85
- #
86
- # === Parameters
87
- #
88
- # * <tt>item</tt> -- The Item instance that is to be added (defaults to nil, in which case an empty Item will be added).
89
- # * <tt>options</tt> -- A hash of parameters.
90
- #
91
- # === Options
92
- #
93
- # * <tt>:index</tt> -- Fixnum. If the Item is to be inserted at a specific index (Item number), this option parameter needs to set.
94
- # * <tt>:no_follow</tt> -- Boolean. If true, the method does not update the parent attribute of the child that is added.
95
- #
96
- # === Examples
97
- #
98
- # # Add an empty Item to a specific Sequence:
99
- # dcm["3006,0020"].add_item
100
- # # Add an existing Item at the 2nd item position/index in the specific Sequence:
101
- # dcm["3006,0020"].add_item(my_item, :index => 2)
102
- #
103
- def add_item(item=nil, options={})
104
- unless self.is_a?(DObject)
105
- if item
106
- if item.is_a?(Item)
107
- if options[:index]
108
- # This Item will take a specific index, and all existing Items with index higher or equal to this number will have their index increased by one.
109
- # Check if index is valid (must be an existing index):
110
- if options[:index] >= 0
111
- # If the index value is larger than the max index present, we dont need to modify the existing items.
112
- if options[:index] < @tags.length
113
- # Extract existing Hash entries to an array:
114
- pairs = @tags.sort
115
- @tags = Hash.new
116
- # Change the key of those equal or larger than index and put these key,value pairs back in a new Hash:
117
- pairs.each do |pair|
118
- if pair[0] < options[:index]
119
- @tags[pair[0]] = pair[1] # (Item keeps its old index)
120
- else
121
- @tags[pair[0]+1] = pair[1]
122
- pair[1].index = pair[0]+1 # (Item gets updated with its new index)
123
- end
124
- end
125
- else
126
- # Set the index value one higher than the already existing max value:
127
- options[:index] = @tags.length
128
- end
129
- #,Add the new Item and set its index:
130
- @tags[options[:index]] = item
131
- item.index = options[:index]
132
- else
133
- raise ArgumentError, "The specified index (#{options[:index]}) is out of range (Must be a positive integer)."
134
- end
135
- else
136
- # Add the existing Item to this Sequence:
137
- index = @tags.length
138
- @tags[index] = item
139
- # Let the Item know what index key it's got in it's parent's Hash:
140
- item.index = index
141
- end
142
- # Set ourself as this item's new parent:
143
- item.set_parent(self) unless options[:no_follow]
144
- else
145
- raise ArgumentError, "The specified parameter is not an Item. Only Items are allowed to be added to a Sequence."
146
- end
147
- else
148
- # Create an empty Item with self as parent.
149
- index = @tags.length
150
- item = Item.new(:parent => self)
151
- end
152
- else
153
- raise "An Item #{item} was attempted added to a DObject instance #{self}, which is not allowed."
154
- end
155
- end
156
-
157
- # Returns all (immediate) child elements in an array (sorted by element tag).
158
- # If this particular parent doesn't have any children, an empty array is returned
159
- #
160
- # === Examples
161
- #
162
- # # Retrieve all top level data elements in a DICOM object:
163
- # top_level_elements = dcm.children
164
- #
165
- def children
166
- return @tags.sort.transpose[1] || Array.new
167
- end
168
-
169
- # Checks if an element actually has any child elements.
170
- # Returns true if it has and false if it doesn't.
171
- #
172
- # === Notes
173
- #
174
- # Notice the subtle difference between the children? and is_parent? methods. While they
175
- # will give the same result in most real use cases, they differ when used on parent elements
176
- # that do not have any children added yet.
177
- #
178
- # For example, when called on an empty Sequence, the children? method will return false,
179
- # while the is_parent? method still returns true.
180
- #
181
- def children?
182
- if @tags.length > 0
183
- return true
184
- else
185
- return false
186
- end
187
- end
188
-
189
- # Counts and returns the number of elements contained directly in this parent.
190
- # This count does NOT include the number of elements contained in any possible child elements.
191
- #
192
- def count
193
- return @tags.length
194
- end
195
-
196
- # Counts and returns the total number of elements contained in this parent.
197
- # This count includes all the elements contained in any possible child elements.
198
- #
199
- def count_all
200
- # Iterate over all elements, and repeat recursively for all elements which themselves contain children.
201
- total_count = count
202
- @tags.each_value do |value|
203
- total_count += value.count_all if value.children?
204
- end
205
- return total_count
206
- end
207
-
208
- # Iterates the children of this parent, calling <tt>block</tt> for each child.
209
- #
210
- def each(&block)
211
- children.each_with_index(&block)
212
- end
213
-
214
- # Iterates the child elements of this parent, calling <tt>block</tt> for each element.
215
- #
216
- def each_element(&block)
217
- elements.each_with_index(&block) if children?
218
- end
219
-
220
- # Iterates the child items of this parent, calling <tt>block</tt> for each item.
221
- #
222
- def each_item(&block)
223
- items.each_with_index(&block) if children?
224
- end
225
-
226
- # Iterates the child sequences of this parent, calling <tt>block</tt> for each sequence.
227
- #
228
- def each_sequence(&block)
229
- sequences.each_with_index(&block) if children?
230
- end
231
-
232
- # Iterates the child tags of this parent, calling <tt>block</tt> for each tag.
233
- #
234
- def each_tag(&block)
235
- @tags.each_key(&block)
236
- end
237
-
238
- # Returns all child elements of this parent in an array.
239
- # If no child elements exists, returns an empty array.
240
- #
241
- def elements
242
- children.select { |child| child.is_a?(Element)}
243
- end
244
-
245
- # A boolean which indicates whether the parent has any child elements.
246
- #
247
- def elements?
248
- elements.any?
249
- end
250
-
251
- # Re-encodes the binary data strings of all child Element instances.
252
- # This also includes all the elements contained in any possible child elements.
253
- #
254
- # === Notes
255
- #
256
- # This method is not intended for external use, but for technical reasons (the fact that is called between
257
- # instances of different classes), cannot be made private.
258
- #
259
- # === Parameters
260
- #
261
- # * <tt>old_endian</tt> -- The previous endianness of the elements/DObject instance (used for decoding values from binary).
262
- #
263
- def encode_children(old_endian)
264
- # Cycle through all levels of children recursively:
265
- children.each do |element|
266
- if element.children?
267
- element.encode_children(old_endian)
268
- elsif element.is_a?(Element)
269
- encode_child(element, old_endian)
270
- end
271
- end
272
- end
273
-
274
- # Checks whether a specific data element tag is defined for this parent.
275
- # Returns true if the tag is found and false if not.
276
- #
277
- # === Parameters
278
- #
279
- # * <tt>tag</tt> -- A tag string which identifies the data element that is queried (Exception: In the case of an Item query, an index integer is used instead).
280
- #
281
- # === Examples
282
- #
283
- # process_name(dcm["0010,0010"]) if dcm.exists?("0010,0010")
284
- #
285
- def exists?(tag)
286
- if self[tag]
287
- return true
288
- else
289
- return false
290
- end
291
- end
292
-
293
- # Returns an array of all child elements that belongs to the specified group.
294
- # If no matches are found, returns an empty array.
295
- #
296
- # === Parameters
297
- #
298
- # * <tt>group_string</tt> -- A group string (the first 4 characters of a tag string).
299
- #
300
- def group(group_string)
301
- raise ArgumentError, "Expected String, got #{group_string.class}." unless group_string.is_a?(String)
302
- found = Array.new
303
- children.each do |child|
304
- found << child if child.tag.group == group_string.upcase
305
- end
306
- return found
307
- end
308
-
309
- # Gathers the desired information from the selected data elements and processes this information to make
310
- # a text output which is nicely formatted. Returns a text array and an index of the last data element.
311
- #
312
- # === Notes
313
- #
314
- # This method is not intended for external use, but for technical reasons (the fact that is called between
315
- # instances of different classes), cannot be made private.
316
- #
317
- # The method is used by the print() method to construct the text output.
318
- #
319
- # === Parameters
320
- #
321
- # * <tt>index</tt> -- Fixnum. The index which is given to the first child of this parent.
322
- # * <tt>max_digits</tt> -- Fixnum. The maximum number of digits in the index of an element (which is the index of the last element).
323
- # * <tt>max_name</tt> -- Fixnum. The maximum number of characters in the name of any element to be printed.
324
- # * <tt>max_length</tt> -- Fixnum. The maximum number of digits in the length of an element.
325
- # * <tt>max_generations</tt> -- Fixnum. The maximum number of generations of children for this parent.
326
- # * <tt>visualization</tt> -- An array of string symbols which visualizes the tree structure that the children of this particular parent belongs to. For no visualization, an empty array is passed.
327
- # * <tt>options</tt> -- A hash of parameters.
328
- #
329
- # === Options
330
- #
331
- # * <tt>:value_max</tt> -- Fixnum. If a value max length is specified, the data elements who's value exceeds this length will be trimmed to this length.
332
- #
333
- #--
334
- # FIXME: This method is somewhat complex, and some simplification, if possible, wouldn't hurt.
335
- #
336
- def handle_print(index, max_digits, max_name, max_length, max_generations, visualization, options={})
337
- elements = Array.new
338
- s = " "
339
- hook_symbol = "|_"
340
- last_item_symbol = " "
341
- nonlast_item_symbol = "| "
342
- children.each_with_index do |element, i|
343
- n_parents = element.parents.length
344
- # Formatting: Index
345
- i_s = s*(max_digits-(index).to_s.length)
346
- # Formatting: Name (and Tag)
347
- if element.tag == ITEM_TAG
348
- # Add index numbers to the Item names:
349
- name = "#{element.name} (\##{i})"
350
- else
351
- name = element.name
352
- end
353
- n_s = s*(max_name-name.length)
354
- # Formatting: Tag
355
- tag = "#{visualization.join}#{element.tag}"
356
- t_s = s*((max_generations-1)*2+9-tag.length)
357
- # Formatting: Length
358
- l_s = s*(max_length-element.length.to_s.length)
359
- # Formatting Value:
360
- if element.is_a?(Element)
361
- value = element.value.to_s
362
- else
363
- value = ""
364
- end
365
- if options[:value_max]
366
- value = "#{value[0..(options[:value_max]-3)]}.." if value.length > options[:value_max]
367
- end
368
- elements << "#{i_s}#{index} #{tag}#{t_s} #{name}#{n_s} #{element.vr} #{l_s}#{element.length} #{value}"
369
- index += 1
370
- # If we have child elements, print those elements recursively:
371
- if element.children?
372
- if n_parents > 1
373
- child_visualization = Array.new
374
- child_visualization.replace(visualization)
375
- if element == children.first
376
- if children.length == 1
377
- # Last item:
378
- child_visualization.insert(n_parents-2, last_item_symbol)
379
- else
380
- # More items follows:
381
- child_visualization.insert(n_parents-2, nonlast_item_symbol)
382
- end
383
- elsif element == children.last
384
- # Last item:
385
- child_visualization[n_parents-2] = last_item_symbol
386
- child_visualization.insert(-1, hook_symbol)
387
- else
388
- # Neither first nor last (more items follows):
389
- child_visualization.insert(n_parents-2, nonlast_item_symbol)
390
- end
391
- elsif n_parents == 1
392
- child_visualization = Array.new(1, hook_symbol)
393
- else
394
- child_visualization = Array.new
395
- end
396
- new_elements, index = element.handle_print(index, max_digits, max_name, max_length, max_generations, child_visualization, options)
397
- elements << new_elements
398
- end
399
- end
400
- return elements.flatten, index
401
- end
402
-
403
- # Returns a string containing a human-readable hash representation of the element.
404
- #
405
- def inspect
406
- to_hash.inspect
407
- end
408
-
409
- # Checks if an element is a parent.
410
- # Returns true for all parent elements.
411
- #
412
- def is_parent?
413
- return true
414
- end
415
-
416
- # Returns all child items of this parent in an array.
417
- # If no child items exists, returns an empty array.
418
- #
419
- def items
420
- children.select { |child| child.is_a?(Item)}
421
- end
422
-
423
- # A boolean which indicates whether the parent has any child items.
424
- #
425
- def items?
426
- items.any?
427
- end
428
-
429
- # Handles missing methods, which in our case is intended to be dynamic
430
- # method names matching DICOM elements in the dictionary.
431
- #
432
- # === Notes
433
- #
434
- # * When a dynamic method name is matched against a DICOM element, this method:
435
- # * Returns the element if the method name suggests an element retrieval, and the element exists.
436
- # * Returns nil if the method name suggests an element retrieval, but the element doesn't exist.
437
- # * Returns a boolean, if the method name suggests a query (?), based on whether the matched element exists or not.
438
- # * When the method name suggests assignment (=), an element is created with the supplied arguments, or if the argument is nil, the element is deleted.
439
- #
440
- # * When a dynamic method name is not matched against a DICOM element, and the method is not defined by the parent, a NoMethodError is raised.
441
- #
442
- # === Parameters
443
- #
444
- # * <tt>sym</tt> -- Symbol. A method name.
445
- #
446
- def method_missing(sym, *args, &block)
447
- # Try to match the method against a tag from the dictionary:
448
- tag = LIBRARY.as_tag(sym.to_s) || LIBRARY.as_tag(sym.to_s[0..-2])
449
- if tag
450
- if sym.to_s[-1..-1] == '?'
451
- # Query:
452
- return self.exists?(tag)
453
- elsif sym.to_s[-1..-1] == '='
454
- # Assignment:
455
- unless args.length==0 || args[0].nil?
456
- # What kind of element to create?
457
- if tag == "FFFE,E000"
458
- return self.add_item
459
- elsif LIBRARY.tags[tag][0][0] == "SQ"
460
- return self.add(Sequence.new(tag))
461
- else
462
- return self.add(Element.new(tag, *args))
463
- end
464
- else
465
- return self.delete(tag)
466
- end
467
- else
468
- # Retrieval:
469
- return self[tag] rescue nil
470
- end
471
- end
472
- # Forward to Object#method_missing:
473
- super
474
- end
475
-
476
- # Sets the length of a Sequence or Item.
477
- #
478
- # === Notes
479
- #
480
- # Currently, Ruby DICOM does not use sequence/item lengths when writing DICOM files
481
- # (it sets the length to -1, which means UNDEFINED). Therefore, in practice, it isn't
482
- # necessary to use this method, at least as far as writing (valid) DICOM files is concerned.
483
- #
484
- # === Parameters
485
- #
486
- # * <tt>new_length</tt> -- Fixnum. The new length to assign to the Sequence/Item.
487
- #
488
- def length=(new_length)
489
- unless self.is_a?(DObject)
490
- @length = new_length
491
- else
492
- raise "Length can not be set for a DObject instance."
493
- end
494
- end
495
-
496
- # Prints all child elements of this particular parent.
497
- # Information such as tag, parent-child relationship, name, vr, length and value is gathered for each data element
498
- # and processed to produce a nicely formatted output.
499
- # Returns an array of formatted data elements.
500
- #
501
- # === Parameters
502
- #
503
- # * <tt>options</tt> -- A hash of parameters.
504
- #
505
- # === Options
506
- #
507
- # * <tt>:value_max</tt> -- Fixnum. If a value max length is specified, the data elements who's value exceeds this length will be trimmed to this length.
508
- # * <tt>:file</tt> -- String. If a file path is specified, the output will be printed to this file instead of being printed to the screen.
509
- #
510
- # === Examples
511
- #
512
- # # Print a DObject instance to screen
513
- # dcm.print
514
- # # Print the DObject to the screen, but specify a 25 character value cutoff to produce better-looking results:
515
- # dcm.print(:value_max => 25)
516
- # # Print to a text file the elements that belong to a specific Sequence:
517
- # dcm["3006,0020"].print(:file => "dicom.txt")
518
- #
519
- #--
520
- # FIXME: Perhaps a :children => false option would be a good idea (to avoid lengthy printouts in cases where this would be desirable)?
521
- # FIXME: Speed. The new print algorithm may seem to be slower than the old one (observed on complex, hiearchical DICOM files). Perhaps it can be optimized?
522
- #
523
- def print(options={})
524
- elements = Array.new
525
- # We first gather some properties that is necessary to produce a nicely formatted printout (max_lengths, count_all),
526
- # then the actual information is gathered (handle_print),
527
- # and lastly, we pass this information on to the methods which print the output (print_file or print_screen).
528
- if count > 0
529
- max_name, max_length, max_generations = max_lengths
530
- max_digits = count_all.to_s.length
531
- visualization = Array.new
532
- elements, index = handle_print(start_index=1, max_digits, max_name, max_length, max_generations, visualization, options)
533
- if options[:file]
534
- print_file(elements, options[:file])
535
- else
536
- print_screen(elements)
537
- end
538
- else
539
- puts "Notice: Object #{self} is empty (contains no data elements)!"
540
- end
541
- return elements
542
- end
543
-
544
- # Finds and returns the maximum character lengths of name and length which occurs for any child element,
545
- # as well as the maximum number of generations of elements.
546
- #
547
- # === Notes
548
- #
549
- # This method is not intended for external use, but for technical reasons (the fact that is called between
550
- # instances of different classes), cannot be made private.
551
- #
552
- # The method is used by the print() method to achieve a proper format in its output.
553
- #
554
- def max_lengths
555
- max_name = 0
556
- max_length = 0
557
- max_generations = 0
558
- children.each do |element|
559
- if element.children?
560
- max_nc, max_lc, max_gc = element.max_lengths
561
- max_name = max_nc if max_nc > max_name
562
- max_length = max_lc if max_lc > max_length
563
- max_generations = max_gc if max_gc > max_generations
564
- end
565
- n_length = element.name.length
566
- l_length = element.length.to_s.length
567
- generations = element.parents.length
568
- max_name = n_length if n_length > max_name
569
- max_length = l_length if l_length > max_length
570
- max_generations = generations if generations > max_generations
571
- end
572
- return max_name, max_length, max_generations
573
- end
574
-
575
- # Deletes the specified element from this parent.
576
- #
577
- # === Parameters
578
- #
579
- # * <tt>tag</tt> -- A tag string which specifies the element to be deleted (Exception: In the case of an Item removal, an index (Fixnum) is used instead).
580
- # * <tt>options</tt> -- A hash of parameters.
581
- #
582
- # === Options
583
- #
584
- # * <tt>:no_follow</tt> -- Boolean. If true, the method does not update the parent attribute of the child that is deleted.
585
- #
586
- # === Examples
587
- #
588
- # # Delete an Element from a DObject instance:
589
- # dcm.delete("0008,0090")
590
- # # Delete Item 1 from a specific Sequence:
591
- # dcm["3006,0020"].delete(1)
592
- #
593
- def delete(tag, options={})
594
- if tag.is_a?(String) or tag.is_a?(Integer)
595
- raise ArgumentError, "Argument (#{tag}) is not a valid tag string." if tag.is_a?(String) && !tag.tag?
596
- raise ArgumentError, "Negative Integer argument (#{tag}) is not allowed." if tag.is_a?(Integer) && tag < 0
597
- else
598
- raise ArgumentError, "Expected String or Integer, got #{tag.class}."
599
- end
600
- # We need to delete the specified child element's parent reference in addition to removing it from the tag Hash.
601
- element = self[tag]
602
- if element
603
- element.parent = nil unless options[:no_follow]
604
- @tags.delete(tag)
605
- end
606
- end
607
-
608
- # Deletes all child elements from this parent.
609
- #
610
- def delete_children
611
- @tags.each_key do |tag|
612
- delete(tag)
613
- end
614
- end
615
-
616
- # Deletes all data elements of the specified group from this parent.
617
- #
618
- # === Parameters
619
- #
620
- # * <tt>group_string</tt> -- A group string (the first 4 characters of a tag string).
621
- #
622
- # === Examples
623
- #
624
- # # Delete the File Meta Group of a DICOM object:
625
- # dcm.delete_group("0002")
626
- #
627
- def delete_group(group_string)
628
- group_elements = group(group_string)
629
- group_elements.each do |element|
630
- delete(element.tag)
631
- end
632
- end
633
-
634
- # Deletes all private data elements from the child elements of this parent.
635
- #
636
- # === Examples
637
- #
638
- # # Delete all private elements from a DObject instance:
639
- # dcm.delete_private
640
- # # Delete only private elements belonging to a specific Sequence:
641
- # dcm["3006,0020"].delete_private
642
- #
643
- def delete_private
644
- # Iterate all children, and repeat recursively if a child itself has children, to delete all private data elements:
645
- children.each do |element|
646
- delete(element.tag) if element.tag.private?
647
- element.delete_private if element.children?
648
- end
649
- end
650
-
651
- # Resets the length of a Sequence or Item to -1, which is the number used for 'undefined' length.
652
- #
653
- def reset_length
654
- unless self.is_a?(DObject)
655
- @length = -1
656
- @bin = ""
657
- else
658
- raise "Length can not be set for a DObject instance."
659
- end
660
- end
661
-
662
- # Returns true if the parent responds to the given method (symbol) (method is defined).
663
- # Returns false if the method is not defined.
664
- #
665
- # === Parameters
666
- #
667
- # * <tt>method</tt> -- Symbol. A method name who's response is tested.
668
- # * <tt>include_private</tt> -- (Not used by ruby-dicom) Boolean. If true, private methods are included in the search.
669
- #
670
- def respond_to?(method, include_private=false)
671
- # Check the library for a tag corresponding to the given method name symbol:
672
- return true unless LIBRARY.as_tag(method.to_s).nil?
673
- # In case of a query (xxx?) or assign (xxx=), remove last character and try again:
674
- return true unless LIBRARY.as_tag(method.to_s[0..-2]).nil?
675
- # Forward to Object#respond_to?:
676
- super
677
- end
678
-
679
- # Returns all child sequences of this parent in an array.
680
- # If no child sequences exists, returns an empty array.
681
- #
682
- def sequences
683
- children.select { |child| child.is_a?(Sequence) }
684
- end
685
-
686
- # A boolean which indicates whether the parent has any child sequences.
687
- #
688
- def sequences?
689
- sequences.any?
690
- end
691
-
692
- # Builds and returns a nested hash containing all children of this parent.
693
- # Keys are determined by the key_representation attribute, and data element values are used as values.
694
- #
695
- # === Notes
696
- #
697
- # * For private elements, the tag is used for key instead of the key representation, as private tags lacks names.
698
- # * For child-less parents, the key_representation attribute is used as value.
699
- #
700
- def to_hash
701
- as_hash = Hash.new
702
- unless children?
703
- if self.is_a?(DObject)
704
- as_hash = {}
705
- else
706
- as_hash[(self.tag.private?) ? self.tag : self.send(DICOM.key_representation)] = nil
707
- end
708
- else
709
- children.each do |child|
710
- if child.tag.private?
711
- hash_key = child.tag
712
- elsif child.is_a?(Item)
713
- hash_key = "Item #{child.index}"
714
- else
715
- hash_key = child.send(DICOM.key_representation)
716
- end
717
- if child.is_a?(Element)
718
- as_hash[hash_key] = child.to_hash[hash_key]
719
- else
720
- as_hash[hash_key] = child.to_hash
721
- end
722
- end
723
- end
724
- return as_hash
725
- end
726
-
727
- # Returns a json string containing a human-readable representation of the element.
728
- #
729
- def to_json
730
- to_hash.to_json
731
- end
732
-
733
- # Returns a yaml string containing a human-readable representation of the element.
734
- #
735
- def to_yaml
736
- to_hash.to_yaml
737
- end
738
-
739
- # Returns the value of a specific Element child of this parent.
740
- # Returns nil if the child element does not exist.
741
- #
742
- # === Notes
743
- #
744
- # * Only Element instances have values. Parent elements like Sequence and Item have no value themselves.
745
- # If the specified <tt>tag</tt> is that of a parent element, <tt>value()</tt> will raise an exception.
746
- #
747
- # === Parameters
748
- #
749
- # * <tt>tag</tt> -- A tag string which identifies the child Element.
750
- #
751
- # === Examples
752
- #
753
- # # Get the patient's name value:
754
- # name = dcm.value("0010,0010")
755
- # # Get the Frame of Reference UID from the first item in the Referenced Frame of Reference Sequence:
756
- # uid = dcm["3006,0010"][0].value("0020,0052")
757
- #
758
- def value(tag)
759
- if tag.is_a?(String) or tag.is_a?(Integer)
760
- raise ArgumentError, "Argument (#{tag}) is not a valid tag string." if tag.is_a?(String) && !tag.tag?
761
- raise ArgumentError, "Negative Integer argument (#{tag}) is not allowed." if tag.is_a?(Integer) && tag < 0
762
- else
763
- raise ArgumentError, "Expected String or Integer, got #{tag.class}."
764
- end
765
- if exists?(tag)
766
- if self[tag].is_parent?
767
- raise ArgumentError, "Illegal parameter '#{tag}'. Parent elements, like the referenced '#{@tags[tag].class}', have no value. Only Element tags are valid."
768
- else
769
- return self[tag].value
770
- end
771
- else
772
- return nil
773
- end
774
- end
775
-
776
-
777
- # Following methods are private:
778
- private
779
-
780
-
781
- # Re-encodes the value of a child Element (but only if the Element encoding is
782
- # influenced by a shift in endianness).
783
- #
784
- # === Parameters
785
- #
786
- # * <tt>element</tt> -- The Element who's value will be re-encoded.
787
- # * <tt>old_endian</tt> -- The previous endianness of the element binary (used for decoding the value).
788
- #
789
- #--
790
- # FIXME: Tag with VR AT has no re-encoding yet..
791
- #
792
- def encode_child(element, old_endian)
793
- if element.tag == "7FE0,0010"
794
- # As encoding settings of the DObject has already been changed, we need to decode the old pixel values with the old encoding:
795
- stream_old_endian = Stream.new(nil, old_endian)
796
- pixels = decode_pixels(element.bin, stream_old_endian)
797
- encode_pixels(pixels, stream)
798
- else
799
- # Not all types of tags needs to be reencoded when switching endianness:
800
- case element.vr
801
- when "US", "SS", "UL", "SL", "FL", "FD", "OF", "OW", "AT" # Numbers or tag reference
802
- # Re-encode, as long as it is not a group 0002 element (which must always be little endian):
803
- unless element.tag.group == "0002"
804
- stream_old_endian = Stream.new(element.bin, old_endian)
805
- formatted_value = stream_old_endian.decode(element.length, element.vr)
806
- element.value = formatted_value # (the value=() method also encodes a new binary for the element)
807
- end
808
- end
809
- end
810
- end
811
-
812
- # Initializes common variables among the parent elements.
813
- #
814
- def initialize_parent
815
- # All child data elements and sequences are stored in a hash where the tag string is used as key:
816
- @tags = Hash.new
817
- end
818
-
819
- # Prints an array of data element ascii text lines gathered by the print() method to file.
820
- #
821
- # === Parameters
822
- #
823
- # * <tt>elements</tt> -- An array of formatted data element lines.
824
- # * <tt>file</tt> -- A path & file string.
825
- #
826
- def print_file(elements, file)
827
- File.open(file, 'w') do |output|
828
- elements.each do |line|
829
- output.print line + "\n"
830
- end
831
- end
832
- end
833
-
834
- # Prints an array of data element ascii text lines gathered by the print() method to the screen.
835
- #
836
- # === Parameters
837
- #
838
- # * <tt>elements</tt> -- An array of formatted data element lines.
839
- #
840
- def print_screen(elements)
841
- elements.each do |line|
842
- puts line
843
- end
844
- end
845
-
846
- end
847
- end
1
+ module DICOM
2
+
3
+ # Super class which contains common code for all parent elements.
4
+ #
5
+ # === Inheritance
6
+ #
7
+ # Since all parents inherit from this class, these methods are available to instances of the following classes:
8
+ # * DObject
9
+ # * Item
10
+ # * Sequence
11
+ #
12
+ class Parent
13
+
14
+ include Logging
15
+
16
+ # Retrieves the child element matching the specified element tag or item index.
17
+ #
18
+ # Only immediate children are searched. Grandchildren etc. are not included.
19
+ #
20
+ # @param [String, Integer] tag_or_index a ruby-dicom tag string or item index
21
+ # @return [Element, Sequence, Item, NilClass] the matched element (or nil, if no match was made)
22
+ # @example Extract the "Pixel Data" data element from the DObject instance
23
+ # pixel_data_element = dcm["7FE0,0010"]
24
+ # @example Extract the first Item from a Sequence
25
+ # first_item = dcm["3006,0020"][0]
26
+ #
27
+ def [](tag_or_index)
28
+ formatted = tag_or_index.is_a?(String) ? tag_or_index.upcase : tag_or_index
29
+ return @tags[formatted]
30
+ end
31
+
32
+ # Adds an Element or Sequence instance to self (where self can be either a DObject or an Item).
33
+ #
34
+ # @note Items can not be added with this method (use add_item instead).
35
+ #
36
+ # @param [Element, Sequence] element a child element/sequence
37
+ # @param [Hash] options the options used for adding the element/sequence
38
+ # option options [Boolean] :no_follow when true, the method does not update the parent attribute of the child that is added
39
+ # @example Set a new patient's name to the DICOM object
40
+ # dcm.add(Element.new("0010,0010", "John_Doe"))
41
+ # @example Add a previously defined element roi_name to the first item of a sequence
42
+ # dcm["3006,0020"][0].add(roi_name)
43
+ #
44
+ def add(element, options={})
45
+ unless element.is_a?(Item)
46
+ unless self.is_a?(Sequence)
47
+ # Does the element's binary value need to be reencoded?
48
+ reencode = true if element.is_a?(Element) && element.endian != stream.str_endian
49
+ # If we are replacing an existing Element, we need to make sure that this Element's parent value is erased before proceeding.
50
+ self[element.tag].parent = nil if exists?(element.tag)
51
+ # Add the element, and set its parent attribute:
52
+ @tags[element.tag] = element
53
+ element.parent = self unless options[:no_follow]
54
+ # As the element has been moved in place, perform re-encode if indicated:
55
+ element.value = element.value if reencode
56
+ else
57
+ raise "A Sequence is only allowed to have Item elements added to it. Use add_item() instead if the intention is to add an Item."
58
+ end
59
+ else
60
+ raise ArgumentError, "An Item is not allowed as a parameter to the add() method. Use add_item() instead."
61
+ end
62
+ end
63
+
64
+ # Adds a child item to a Sequence (or Item in some cases where pixel data is encapsulated).
65
+ #
66
+ # If no existing Item is given, a new item will be created and added.
67
+ #
68
+ # @note Items are specified by index (starting at 0) instead of a tag string!
69
+ #
70
+ # @param [Item] item the Item instance to be added
71
+ # @param [Hash] options the options used for adding the item
72
+ # option options [Integer] :if specified, forces the item to be inserted at that specific index (Item number)
73
+ # option options [Boolean] :no_follow when true, the method does not update the parent attribute of the child that is added
74
+ # * <tt>options</tt> -- A hash of parameters.
75
+ # @example Add an empty Item to a specific Sequence
76
+ # dcm["3006,0020"].add_item
77
+ # @example Add an existing Item at the 2nd item position/index in the specific Sequence
78
+ # dcm["3006,0020"].add_item(my_item, :index => 1)
79
+ #
80
+ def add_item(item=nil, options={})
81
+ unless self.is_a?(DObject)
82
+ if item
83
+ if item.is_a?(Item)
84
+ if options[:index]
85
+ # This Item will take a specific index, and all existing Items with index higher or equal to this number will have their index increased by one.
86
+ # Check if index is valid (must be an existing index):
87
+ if options[:index] >= 0
88
+ # If the index value is larger than the max index present, we dont need to modify the existing items.
89
+ if options[:index] < @tags.length
90
+ # Extract existing Hash entries to an array:
91
+ pairs = @tags.sort
92
+ @tags = Hash.new
93
+ # Change the key of those equal or larger than index and put these key,value pairs back in a new Hash:
94
+ pairs.each do |pair|
95
+ if pair[0] < options[:index]
96
+ @tags[pair[0]] = pair[1] # (Item keeps its old index)
97
+ else
98
+ @tags[pair[0]+1] = pair[1]
99
+ pair[1].index = pair[0]+1 # (Item gets updated with its new index)
100
+ end
101
+ end
102
+ else
103
+ # Set the index value one higher than the already existing max value:
104
+ options[:index] = @tags.length
105
+ end
106
+ #,Add the new Item and set its index:
107
+ @tags[options[:index]] = item
108
+ item.index = options[:index]
109
+ else
110
+ raise ArgumentError, "The specified index (#{options[:index]}) is out of range (Must be a positive integer)."
111
+ end
112
+ else
113
+ # Add the existing Item to this Sequence:
114
+ index = @tags.length
115
+ @tags[index] = item
116
+ # Let the Item know what index key it's got in it's parent's Hash:
117
+ item.index = index
118
+ end
119
+ # Set ourself as this item's new parent:
120
+ item.set_parent(self) unless options[:no_follow]
121
+ else
122
+ raise ArgumentError, "The specified parameter is not an Item. Only Items are allowed to be added to a Sequence."
123
+ end
124
+ else
125
+ # Create an empty Item with self as parent.
126
+ index = @tags.length
127
+ item = Item.new(:parent => self)
128
+ end
129
+ else
130
+ raise "An Item #{item} was attempted added to a DObject instance #{self}, which is not allowed."
131
+ end
132
+ end
133
+
134
+ # Retrieves all (immediate) child elementals in an array (sorted by element tag).
135
+ #
136
+ # @return [Array<Element, Item, Sequence>] the parent's child elementals (an empty array if childless)
137
+ # @example Retrieve all top level elements in a DICOM object
138
+ # top_level_elements = dcm.children
139
+ #
140
+ def children
141
+ return @tags.sort.transpose[1] || Array.new
142
+ end
143
+
144
+ # Checks if an element actually has any child elementals (elements/items/sequences).
145
+ #
146
+ # Notice the subtle difference between the children? and is_parent? methods. While they
147
+ # will give the same result in most real use cases, they differ when used on parent elements
148
+ # that do not have any children added yet.
149
+ #
150
+ # For example, when called on an empty Sequence, the children? method
151
+ # will return false, whereas the is_parent? method still returns true.
152
+ #
153
+ # @return [Boolean] true if the element has children, and false if not
154
+ #
155
+ def children?
156
+ if @tags.length > 0
157
+ return true
158
+ else
159
+ return false
160
+ end
161
+ end
162
+
163
+ # Gives the number of elements connected directly to this parent.
164
+ #
165
+ # This count does NOT include the number of elements contained in any possible child elements.
166
+ #
167
+ # @return [Integer] The number of child elements belonging to this parent
168
+ #
169
+ def count
170
+ return @tags.length
171
+ end
172
+
173
+ # Gives the total number of elements connected to this parent.
174
+ #
175
+ # This count includes all the elements contained in any possible child elements.
176
+ #
177
+ # @return [Integer] The total number of child elements connected to this parent
178
+ #
179
+ def count_all
180
+ # Iterate over all elements, and repeat recursively for all elements which themselves contain children.
181
+ total_count = count
182
+ @tags.each_value do |value|
183
+ total_count += value.count_all if value.children?
184
+ end
185
+ return total_count
186
+ end
187
+
188
+ # Deletes the specified element from this parent.
189
+ #
190
+ # @param [String, Integer] tag_or_index a ruby-dicom tag string or item index
191
+ # @param [Hash] options the options used for deleting the element
192
+ # option options [Boolean] :no_follow when true, the method does not update the parent attribute of the child that is deleted
193
+ # @example Delete an Element from a DObject instance
194
+ # dcm.delete("0008,0090")
195
+ # @example Delete Item 1 from a Sequence
196
+ # dcm["3006,0020"].delete(1)
197
+ #
198
+ def delete(tag_or_index, options={})
199
+ if tag_or_index.is_a?(String) or tag_or_index.is_a?(Integer)
200
+ raise ArgumentError, "Argument (#{tag_or_index}) is not a valid tag string." if tag_or_index.is_a?(String) && !tag_or_index.tag?
201
+ raise ArgumentError, "Negative Integer argument (#{tag_or_index}) is not allowed." if tag_or_index.is_a?(Integer) && tag_or_index < 0
202
+ else
203
+ raise ArgumentError, "Expected String or Integer, got #{tag_or_index.class}."
204
+ end
205
+ # We need to delete the specified child element's parent reference in addition to removing it from the tag Hash.
206
+ element = self[tag_or_index]
207
+ if element
208
+ element.parent = nil unless options[:no_follow]
209
+ @tags.delete(tag_or_index)
210
+ end
211
+ end
212
+
213
+ # Deletes all child elements from this parent.
214
+ #
215
+ def delete_children
216
+ @tags.each_key do |tag|
217
+ delete(tag)
218
+ end
219
+ end
220
+
221
+ # Deletes all elements of the specified group from this parent.
222
+ #
223
+ # @param [String] group_string a group string (the first 4 characters of a tag string)
224
+ # @example Delete the File Meta Group of a DICOM object
225
+ # dcm.delete_group("0002")
226
+ #
227
+ def delete_group(group_string)
228
+ group_elements = group(group_string)
229
+ group_elements.each do |element|
230
+ delete(element.tag)
231
+ end
232
+ end
233
+
234
+ # Deletes all private data/sequence elements from this parent.
235
+ #
236
+ # @example Delete all private elements from a DObject instance
237
+ # dcm.delete_private
238
+ # @example Delete only private elements belonging to a specific Sequence
239
+ # dcm["3006,0020"].delete_private
240
+ #
241
+ def delete_private
242
+ # Iterate all children, and repeat recursively if a child itself has children, to delete all private data elements:
243
+ children.each do |element|
244
+ delete(element.tag) if element.tag.private?
245
+ element.delete_private if element.children?
246
+ end
247
+ end
248
+
249
+ # Deletes all retired data/sequence elements from this parent.
250
+ #
251
+ # @example Delete all retired elements from a DObject instance
252
+ # dcm.delete_retired
253
+ #
254
+ def delete_retired
255
+ # Iterate all children, and repeat recursively if a child itself has children, to delete all retired elements:
256
+ children.each do |element|
257
+ dict_element = LIBRARY.element(element.tag)
258
+ delete(element.tag) if dict_element && dict_element.retired?
259
+ element.delete_retired if element.children?
260
+ end
261
+ end
262
+
263
+ # Iterates all children of this parent, calling <tt>block</tt> for each child.
264
+ #
265
+ def each(&block)
266
+ children.each_with_index(&block)
267
+ end
268
+
269
+ # Iterates the child elements of this parent, calling <tt>block</tt> for each element.
270
+ #
271
+ def each_element(&block)
272
+ elements.each_with_index(&block) if children?
273
+ end
274
+
275
+ # Iterates the child items of this parent, calling <tt>block</tt> for each item.
276
+ #
277
+ def each_item(&block)
278
+ items.each_with_index(&block) if children?
279
+ end
280
+
281
+ # Iterates the child sequences of this parent, calling <tt>block</tt> for each sequence.
282
+ #
283
+ def each_sequence(&block)
284
+ sequences.each_with_index(&block) if children?
285
+ end
286
+
287
+ # Iterates the child tags of this parent, calling <tt>block</tt> for each tag.
288
+ #
289
+ def each_tag(&block)
290
+ @tags.each_key(&block)
291
+ end
292
+
293
+ # Retrieves all child elements of this parent in an array.
294
+ #
295
+ # @return [Array<Element>] child elements (or empty array, if childless)
296
+ #
297
+ def elements
298
+ children.select { |child| child.is_a?(Element)}
299
+ end
300
+
301
+ # A boolean which indicates whether the parent has any child elements.
302
+ #
303
+ # @return [Boolean] true if any child elements exists, and false if not
304
+ #
305
+ def elements?
306
+ elements.any?
307
+ end
308
+
309
+ # Re-encodes the binary data strings of all child Element instances.
310
+ # This also includes all the elements contained in any possible child elements.
311
+ #
312
+ # @note This method is only intended for internal library use, but for technical reasons
313
+ # (the fact that is called between instances of different classes), can't be made private.
314
+ # @param [Boolean] old_endian the previous endianness of the elements/DObject instance (used for decoding values from binary)
315
+ #
316
+ def encode_children(old_endian)
317
+ # Cycle through all levels of children recursively:
318
+ children.each do |element|
319
+ if element.children?
320
+ element.encode_children(old_endian)
321
+ elsif element.is_a?(Element)
322
+ encode_child(element, old_endian)
323
+ end
324
+ end
325
+ end
326
+
327
+ # Checks whether a specific data element tag is defined for this parent.
328
+ #
329
+ # @param [String, Integer] tag_or_index a ruby-dicom tag string or item index
330
+ # @return [Boolean] true if the element is found, and false if not
331
+ # @example Do something with an element only if it exists
332
+ # process_name(dcm["0010,0010"]) if dcm.exists?("0010,0010")
333
+ #
334
+ def exists?(tag_or_index)
335
+ if self[tag_or_index]
336
+ return true
337
+ else
338
+ return false
339
+ end
340
+ end
341
+
342
+ # Returns an array of all child elements that belongs to the specified group.
343
+ #
344
+ # @param [String] group_string a group string (the first 4 characters of a tag string)
345
+ # @return [Array<Element, Item, Sequence>] the matching child elements (an empty array if no children matches)
346
+ #
347
+ def group(group_string)
348
+ raise ArgumentError, "Expected String, got #{group_string.class}." unless group_string.is_a?(String)
349
+ found = Array.new
350
+ children.each do |child|
351
+ found << child if child.tag.group == group_string.upcase
352
+ end
353
+ return found
354
+ end
355
+
356
+ # Gathers the desired information from the selected data elements and
357
+ # processes this information to make a text output which is nicely formatted.
358
+ #
359
+ # @note This method is only intended for internal library use, but for technical reasons
360
+ # (the fact that is called between instances of different classes), can't be made private.
361
+ # The method is used by the print() method to construct its text output.
362
+ #
363
+ # @param [Integer] index the index which is given to the first child of this parent
364
+ # @param [Integer] max_digits the maximum number of digits in the index of an element (in reality the number of digits of the last element)
365
+ # @param [Integer] max_name the maximum number of characters in the name of any element to be printed
366
+ # @param [Integer] max_length the maximum number of digits in the length of an element
367
+ # @param [Integer] max_generations the maximum number of generations of children for this parent
368
+ # @param [Integer] visualization an array of string symbols which visualizes the tree structure that the children of this particular parent belongs to (for no visualization, an empty array is passed)
369
+ # @param [Hash] options the options to use when processing the print information
370
+ # @option options [Integer] :value_max if a value max length is specified, the element values which exceeds this are trimmed
371
+ # @return [Array] a text array and an index of the last element
372
+ #
373
+ def handle_print(index, max_digits, max_name, max_length, max_generations, visualization, options={})
374
+ # FIXME: This method is somewhat complex, and some simplification, if possible, wouldn't hurt.
375
+ elements = Array.new
376
+ s = " "
377
+ hook_symbol = "|_"
378
+ last_item_symbol = " "
379
+ nonlast_item_symbol = "| "
380
+ children.each_with_index do |element, i|
381
+ n_parents = element.parents.length
382
+ # Formatting: Index
383
+ i_s = s*(max_digits-(index).to_s.length)
384
+ # Formatting: Name (and Tag)
385
+ if element.tag == ITEM_TAG
386
+ # Add index numbers to the Item names:
387
+ name = "#{element.name} (\##{i})"
388
+ else
389
+ name = element.name
390
+ end
391
+ n_s = s*(max_name-name.length)
392
+ # Formatting: Tag
393
+ tag = "#{visualization.join}#{element.tag}"
394
+ t_s = s*((max_generations-1)*2+9-tag.length)
395
+ # Formatting: Length
396
+ l_s = s*(max_length-element.length.to_s.length)
397
+ # Formatting Value:
398
+ if element.is_a?(Element)
399
+ value = element.value.to_s
400
+ else
401
+ value = ""
402
+ end
403
+ if options[:value_max]
404
+ value = "#{value[0..(options[:value_max]-3)]}.." if value.length > options[:value_max]
405
+ end
406
+ elements << "#{i_s}#{index} #{tag}#{t_s} #{name}#{n_s} #{element.vr} #{l_s}#{element.length} #{value}"
407
+ index += 1
408
+ # If we have child elements, print those elements recursively:
409
+ if element.children?
410
+ if n_parents > 1
411
+ child_visualization = Array.new
412
+ child_visualization.replace(visualization)
413
+ if element == children.first
414
+ if children.length == 1
415
+ # Last item:
416
+ child_visualization.insert(n_parents-2, last_item_symbol)
417
+ else
418
+ # More items follows:
419
+ child_visualization.insert(n_parents-2, nonlast_item_symbol)
420
+ end
421
+ elsif element == children.last
422
+ # Last item:
423
+ child_visualization[n_parents-2] = last_item_symbol
424
+ child_visualization.insert(-1, hook_symbol)
425
+ else
426
+ # Neither first nor last (more items follows):
427
+ child_visualization.insert(n_parents-2, nonlast_item_symbol)
428
+ end
429
+ elsif n_parents == 1
430
+ child_visualization = Array.new(1, hook_symbol)
431
+ else
432
+ child_visualization = Array.new
433
+ end
434
+ new_elements, index = element.handle_print(index, max_digits, max_name, max_length, max_generations, child_visualization, options)
435
+ elements << new_elements
436
+ end
437
+ end
438
+ return elements.flatten, index
439
+ end
440
+
441
+ # Gives a string containing a human-readable hash representation of the parent.
442
+ #
443
+ # @return [String] a hash representation string of the parent
444
+ #
445
+ def inspect
446
+ to_hash.inspect
447
+ end
448
+
449
+ # Checks if an elemental is a parent.
450
+ #
451
+ # @return [Boolean] true for all parent elementals (Item, Sequence, DObject)
452
+ #
453
+ def is_parent?
454
+ return true
455
+ end
456
+
457
+ # Retrieves all child items of this parent in an array.
458
+ #
459
+ # @return [Array<Item>] child items (or empty array, if childless)
460
+ #
461
+ def items
462
+ children.select { |child| child.is_a?(Item)}
463
+ end
464
+
465
+ # A boolean which indicates whether the parent has any child items.
466
+ #
467
+ # @return [Boolean] true if any child items exists, and false if not
468
+ #
469
+ def items?
470
+ items.any?
471
+ end
472
+
473
+ # Sets the length of a Sequence or Item.
474
+ #
475
+ # @note Currently, ruby-dicom does not use sequence/item lengths when writing DICOM files
476
+ # (it sets the length to -1, meaning UNDEFINED). Therefore, in practice, it isn't
477
+ # necessary to use this method, at least as far as writing (valid) DICOM files is concerned.
478
+ #
479
+ # @param [Integer] new_length the new length to assign to the Sequence/Item
480
+ #
481
+ def length=(new_length)
482
+ unless self.is_a?(DObject)
483
+ @length = new_length
484
+ else
485
+ raise "Length can not be set for a DObject instance."
486
+ end
487
+ end
488
+
489
+ # Finds and returns the maximum character lengths of name and length which occurs for any child element,
490
+ # as well as the maximum number of generations of elements.
491
+ #
492
+ # @note This method is only intended for internal library use, but for technical reasons
493
+ # (the fact that is called between instances of different classes), can't be made private.
494
+ # The method is used by the print() method to achieve a proper format in its output.
495
+ #
496
+ def max_lengths
497
+ max_name = 0
498
+ max_length = 0
499
+ max_generations = 0
500
+ children.each do |element|
501
+ if element.children?
502
+ max_nc, max_lc, max_gc = element.max_lengths
503
+ max_name = max_nc if max_nc > max_name
504
+ max_length = max_lc if max_lc > max_length
505
+ max_generations = max_gc if max_gc > max_generations
506
+ end
507
+ n_length = element.name.length
508
+ l_length = element.length.to_s.length
509
+ generations = element.parents.length
510
+ max_name = n_length if n_length > max_name
511
+ max_length = l_length if l_length > max_length
512
+ max_generations = generations if generations > max_generations
513
+ end
514
+ return max_name, max_length, max_generations
515
+ end
516
+
517
+ # Handles missing methods, which in our case is intended to be dynamic
518
+ # method names matching DICOM elements in the dictionary.
519
+ #
520
+ # When a dynamic method name is matched against a DICOM element, this method:
521
+ # * Returns the element if the method name suggests an element retrieval, and the element exists.
522
+ # * Returns nil if the method name suggests an element retrieval, but the element doesn't exist.
523
+ # * Returns a boolean, if the method name suggests a query (?), based on whether the matched element exists or not.
524
+ # * When the method name suggests assignment (=), an element is created with the supplied arguments, or if the argument is nil, the element is deleted.
525
+ #
526
+ # * When a dynamic method name is not matched against a DICOM element, and the method is not defined by the parent, a NoMethodError is raised.
527
+ #
528
+ # @param [Symbol] sym a method name
529
+ #
530
+ def method_missing(sym, *args, &block)
531
+ # Try to match the method against a tag from the dictionary:
532
+ tag = LIBRARY.as_tag(sym.to_s) || LIBRARY.as_tag(sym.to_s[0..-2])
533
+ if tag
534
+ if sym.to_s[-1..-1] == '?'
535
+ # Query:
536
+ return self.exists?(tag)
537
+ elsif sym.to_s[-1..-1] == '='
538
+ # Assignment:
539
+ unless args.length==0 || args[0].nil?
540
+ # What kind of element to create?
541
+ if tag == 'FFFE,E000'
542
+ return self.add_item
543
+ elsif LIBRARY.element(tag).vr == 'SQ'
544
+ return self.add(Sequence.new(tag))
545
+ else
546
+ return self.add(Element.new(tag, *args))
547
+ end
548
+ else
549
+ return self.delete(tag)
550
+ end
551
+ else
552
+ # Retrieval:
553
+ return self[tag] rescue nil
554
+ end
555
+ end
556
+ # Forward to Object#method_missing:
557
+ super
558
+ end
559
+
560
+ # Prints all child elementals of this particular parent.
561
+ # Information such as tag, parent-child relationship, name, vr, length and value is
562
+ # gathered for each element and processed to produce a nicely formatted output.
563
+ #
564
+ # @param [Hash] options the options to use for handling the printout
565
+ # option options [Integer] :value_max if a value max length is specified, the element values which exceeds this are trimmed
566
+ # option options [String] :file if a file path is specified, the output is printed to this file instead of being printed to the screen
567
+ # @return [Array<String>] an array of formatted element string lines
568
+ # @example Print a DObject instance to screen
569
+ # dcm.print
570
+ # @example Print the DObject to the screen, but specify a 25 character value cutoff to produce better-looking results
571
+ # dcm.print(:value_max => 25)
572
+ # @example Print to a text file the elements that belong to a specific Sequence
573
+ # dcm["3006,0020"].print(:file => "dicom.txt")
574
+ #
575
+ def print(options={})
576
+ # FIXME: Perhaps a :children => false option would be a good idea (to avoid lengthy printouts in cases where this would be desirable)?
577
+ # FIXME: Speed. The new print algorithm may seem to be slower than the old one (observed on complex, hiearchical DICOM files). Perhaps it can be optimized?
578
+ elements = Array.new
579
+ # We first gather some properties that is necessary to produce a nicely formatted printout (max_lengths, count_all),
580
+ # then the actual information is gathered (handle_print),
581
+ # and lastly, we pass this information on to the methods which print the output (print_file or print_screen).
582
+ if count > 0
583
+ max_name, max_length, max_generations = max_lengths
584
+ max_digits = count_all.to_s.length
585
+ visualization = Array.new
586
+ elements, index = handle_print(start_index=1, max_digits, max_name, max_length, max_generations, visualization, options)
587
+ if options[:file]
588
+ print_file(elements, options[:file])
589
+ else
590
+ print_screen(elements)
591
+ end
592
+ else
593
+ puts "Notice: Object #{self} is empty (contains no data elements)!"
594
+ end
595
+ return elements
596
+ end
597
+
598
+ # Resets the length of a Sequence or Item to -1, which is the number used for 'undefined' length.
599
+ #
600
+ def reset_length
601
+ unless self.is_a?(DObject)
602
+ @length = -1
603
+ @bin = ""
604
+ else
605
+ raise "Length can not be set for a DObject instance."
606
+ end
607
+ end
608
+
609
+ # Checks if the parent responds to the given method (symbol) (whether the method is defined or not).
610
+ #
611
+ # @param [Symbol] method a method name who's response is tested
612
+ # @param [Boolean] include_private if true, private methods are included in the search (not used by ruby-dicom)
613
+ # @return [Boolean] true if the parent responds to the given method (method is defined), and false if not
614
+ #
615
+ def respond_to?(method, include_private=false)
616
+ # Check the library for a tag corresponding to the given method name symbol:
617
+ return true unless LIBRARY.as_tag(method.to_s).nil?
618
+ # In case of a query (xxx?) or assign (xxx=), remove last character and try again:
619
+ return true unless LIBRARY.as_tag(method.to_s[0..-2]).nil?
620
+ # Forward to Object#respond_to?:
621
+ super
622
+ end
623
+
624
+ # Retrieves all child sequences of this parent in an array.
625
+ #
626
+ # @return [Array<Sequence>] child sequences (or empty array, if childless)
627
+ #
628
+ def sequences
629
+ children.select { |child| child.is_a?(Sequence) }
630
+ end
631
+
632
+ # A boolean which indicates whether the parent has any child sequences.
633
+ #
634
+ # @return [Boolean] true if any child sequences exists, and false if not
635
+ #
636
+ def sequences?
637
+ sequences.any?
638
+ end
639
+
640
+ # Builds a nested hash containing all children of this parent.
641
+ #
642
+ # Keys are determined by the key_representation attribute, and data element values are used as values.
643
+ # * For private elements, the tag is used for key instead of the key representation, as private tags lacks names.
644
+ # * For child-less parents, the key_representation attribute is used as value.
645
+ #
646
+ # @return [Hash] a nested hash containing key & value pairs of all children
647
+ #
648
+ def to_hash
649
+ as_hash = Hash.new
650
+ unless children?
651
+ if self.is_a?(DObject)
652
+ as_hash = {}
653
+ else
654
+ as_hash[(self.tag.private?) ? self.tag : self.send(DICOM.key_representation)] = nil
655
+ end
656
+ else
657
+ children.each do |child|
658
+ if child.tag.private?
659
+ hash_key = child.tag
660
+ elsif child.is_a?(Item)
661
+ hash_key = "Item #{child.index}"
662
+ else
663
+ hash_key = child.send(DICOM.key_representation)
664
+ end
665
+ if child.is_a?(Element)
666
+ as_hash[hash_key] = child.to_hash[hash_key]
667
+ else
668
+ as_hash[hash_key] = child.to_hash
669
+ end
670
+ end
671
+ end
672
+ return as_hash
673
+ end
674
+
675
+ # Builds a json string containing a human-readable representation of the parent.
676
+ #
677
+ # @return [String] a human-readable representation of this parent
678
+ #
679
+ def to_json
680
+ to_hash.to_json
681
+ end
682
+
683
+ # Returns a yaml string containing a human-readable representation of the parent.
684
+ #
685
+ # @return [String] a human-readable representation of this parent
686
+ #
687
+ def to_yaml
688
+ to_hash.to_yaml
689
+ end
690
+
691
+ # Gives the value of a specific Element child of this parent.
692
+ #
693
+ # * Only Element instances have values. Parent elements like Sequence and Item have no value themselves.
694
+ # * If the specified tag is that of a parent element, an exception is raised.
695
+ #
696
+ # @param [String] tag a tag string which identifies the child Element
697
+ # @return [String, Integer, Float, NilClass] an element value (or nil, if no element is matched)
698
+ # @example Get the patient's name value
699
+ # name = dcm.value("0010,0010")
700
+ # @example Get the Frame of Reference UID from the first item in the Referenced Frame of Reference Sequence
701
+ # uid = dcm["3006,0010"][0].value("0020,0052")
702
+ #
703
+ def value(tag)
704
+ if tag.is_a?(String) or tag.is_a?(Integer)
705
+ raise ArgumentError, "Argument (#{tag}) is not a valid tag string." if tag.is_a?(String) && !tag.tag?
706
+ raise ArgumentError, "Negative Integer argument (#{tag}) is not allowed." if tag.is_a?(Integer) && tag < 0
707
+ else
708
+ raise ArgumentError, "Expected String or Integer, got #{tag.class}."
709
+ end
710
+ if exists?(tag)
711
+ if self[tag].is_parent?
712
+ raise ArgumentError, "Illegal parameter '#{tag}'. Parent elements, like the referenced '#{@tags[tag].class}', have no value. Only Element tags are valid."
713
+ else
714
+ return self[tag].value
715
+ end
716
+ else
717
+ return nil
718
+ end
719
+ end
720
+
721
+
722
+ private
723
+
724
+
725
+ # Re-encodes the value of a child Element (but only if the
726
+ # Element encoding is influenced by a shift in endianness).
727
+ #
728
+ # @param [Element] element the Element who's value will be re-encoded
729
+ # @param [Boolean] old_endian the previous endianness of the element binary (used for decoding the value)
730
+ #
731
+ def encode_child(element, old_endian)
732
+ if element.tag == "7FE0,0010"
733
+ # As encoding settings of the DObject has already been changed, we need to decode the old pixel values with the old encoding:
734
+ stream_old_endian = Stream.new(nil, old_endian)
735
+ pixels = decode_pixels(element.bin, stream_old_endian)
736
+ encode_pixels(pixels, stream)
737
+ else
738
+ # Not all types of tags needs to be reencoded when switching endianness:
739
+ case element.vr
740
+ when "US", "SS", "UL", "SL", "FL", "FD", "OF", "OW", "AT" # Numbers or tag reference
741
+ # Re-encode, as long as it is not a group 0002 element (which must always be little endian):
742
+ unless element.tag.group == "0002"
743
+ stream_old_endian = Stream.new(element.bin, old_endian)
744
+ formatted_value = stream_old_endian.decode(element.length, element.vr)
745
+ element.value = formatted_value # (the value=() method also encodes a new binary for the element)
746
+ end
747
+ end
748
+ end
749
+ end
750
+
751
+ # Initializes common variables among the parent elements.
752
+ #
753
+ def initialize_parent
754
+ # All child data elements and sequences are stored in a hash where the tag string is used as key:
755
+ @tags = Hash.new
756
+ end
757
+
758
+ # Prints an array of formatted element string lines gathered by the print() method to file.
759
+ #
760
+ # @param [Array<String>] elements an array of formatted element string lines
761
+ # @param [String] file a path/file_name string
762
+ #
763
+ def print_file(elements, file)
764
+ File.open(file, 'w') do |output|
765
+ elements.each do |line|
766
+ output.print line + "\n"
767
+ end
768
+ end
769
+ end
770
+
771
+ # Prints an array of formatted element string lines gathered by the print() method to the screen.
772
+ #
773
+ # @param [Array<String>] elements an array of formatted element string lines
774
+ #
775
+ def print_screen(elements)
776
+ elements.each do |line|
777
+ puts line
778
+ end
779
+ end
780
+
781
+ end
782
+ end