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.
- data/CHANGELOG.rdoc +312 -290
- data/COPYING +674 -674
- data/Gemfile +3 -0
- data/dicom.gemspec +31 -0
- data/lib/dicom.rb +53 -51
- data/lib/dicom/anonymizer.rb +98 -123
- data/lib/dicom/audit_trail.rb +104 -116
- data/lib/dicom/constants.rb +219 -170
- data/lib/dicom/d_client.rb +122 -150
- data/lib/dicom/d_library.rb +219 -287
- data/lib/dicom/d_object.rb +451 -539
- data/lib/dicom/d_read.rb +151 -245
- data/lib/dicom/d_server.rb +329 -359
- data/lib/dicom/d_write.rb +327 -395
- data/lib/dicom/deprecated.rb +1 -72
- data/lib/dicom/dictionary/elements.txt +3646 -0
- data/lib/dicom/dictionary/uids.txt +334 -0
- data/lib/dicom/dictionary_element.rb +61 -0
- data/lib/dicom/element.rb +278 -218
- data/lib/dicom/elemental.rb +21 -27
- data/lib/dicom/file_handler.rb +121 -121
- data/lib/dicom/image_item.rb +819 -861
- data/lib/dicom/image_processor.rb +24 -15
- data/lib/dicom/image_processor_mini_magick.rb +21 -23
- data/lib/dicom/image_processor_r_magick.rb +39 -34
- data/lib/dicom/item.rb +133 -120
- data/lib/dicom/link.rb +1531 -1532
- data/lib/dicom/logging.rb +155 -158
- data/lib/dicom/parent.rb +782 -847
- data/lib/dicom/ruby_extensions.rb +248 -229
- data/lib/dicom/sequence.rb +109 -92
- data/lib/dicom/stream.rb +480 -511
- data/lib/dicom/uid.rb +82 -0
- data/lib/dicom/variables.rb +9 -9
- data/lib/dicom/version.rb +5 -5
- data/rakefile.rb +29 -0
- metadata +130 -76
- data/lib/dicom/dictionary.rb +0 -3280
data/lib/dicom/logging.rb
CHANGED
@@ -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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# DICOM.logger
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# logger
|
23
|
-
# logger
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# DICOM.logger.info
|
28
|
-
# logger.info "Message"
|
29
|
-
#
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
@target.send(method_name,
|
84
|
-
|
85
|
-
@target.send(method_name,
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
data/lib/dicom/parent.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
#
|
164
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
#
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
#
|
197
|
-
#
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
end
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
#
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
#
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
#
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
def
|
242
|
-
children
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
#
|
252
|
-
#
|
253
|
-
#
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
#
|
276
|
-
#
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
#
|
282
|
-
#
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
end
|
292
|
-
|
293
|
-
#
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
end
|
308
|
-
|
309
|
-
#
|
310
|
-
#
|
311
|
-
#
|
312
|
-
#
|
313
|
-
#
|
314
|
-
#
|
315
|
-
#
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
#
|
328
|
-
#
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
#
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
#
|
442
|
-
#
|
443
|
-
#
|
444
|
-
#
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
#
|
477
|
-
#
|
478
|
-
#
|
479
|
-
#
|
480
|
-
#
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
#
|
518
|
-
#
|
519
|
-
|
520
|
-
#
|
521
|
-
#
|
522
|
-
#
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
#
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
#
|
625
|
-
#
|
626
|
-
#
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
#
|
635
|
-
#
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
#
|
641
|
-
#
|
642
|
-
#
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
return
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
#
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
#
|
693
|
-
#
|
694
|
-
#
|
695
|
-
#
|
696
|
-
#
|
697
|
-
#
|
698
|
-
#
|
699
|
-
#
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
#
|
728
|
-
#
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
#
|
752
|
-
#
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
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
|