dicom 0.9.3 → 0.9.4

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,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