dicom 0.9.3 → 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- 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/sequence.rb
CHANGED
@@ -1,93 +1,110 @@
|
|
1
|
-
module DICOM
|
2
|
-
|
3
|
-
# The Sequence class handles information related to Sequence elements.
|
4
|
-
#
|
5
|
-
class Sequence < Parent
|
6
|
-
|
7
|
-
# Include the Elemental mix-in module:
|
8
|
-
include Elemental
|
9
|
-
|
10
|
-
# Creates a Sequence instance.
|
11
|
-
#
|
12
|
-
#
|
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
|
-
if options[:
|
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
|
-
|
1
|
+
module DICOM
|
2
|
+
|
3
|
+
# The Sequence class handles information related to Sequence elements.
|
4
|
+
#
|
5
|
+
class Sequence < Parent
|
6
|
+
|
7
|
+
# Include the Elemental mix-in module:
|
8
|
+
include Elemental
|
9
|
+
|
10
|
+
# Creates a Sequence instance.
|
11
|
+
#
|
12
|
+
# @note Private sequences are named as 'Private'.
|
13
|
+
# @note Non-private sequences that are not found in the dictionary are named as 'Unknown'.
|
14
|
+
#
|
15
|
+
# @param [String] tag a ruby-dicom type element tag string
|
16
|
+
# @param [Hash] options the options to use for creating the sequence
|
17
|
+
# @option options [Integer] :length the sequence length, which refers to the length of the encoded string of children of this sequence
|
18
|
+
# @option options [Integer] :name the name of the sequence may be specified upon creation (if it is not, the name is retrieved from the dictionary)
|
19
|
+
# @option options [Integer] :parent an Item or DObject instance which the sequence instance shall belong to
|
20
|
+
# @option options [Integer] :vr the value representation of the Sequence may be specified upon creation (if it is not, a default vr is chosen)
|
21
|
+
#
|
22
|
+
# @example Create a new Sequence and connect it to a DObject instance
|
23
|
+
# structure_set_roi = Sequence.new('3006,0020', :parent => dcm)
|
24
|
+
# @example Create an "Encapsulated Pixel Data" Sequence
|
25
|
+
# encapsulated_pixel_data = Sequence.new('7FE0,0010', :name => 'Encapsulated Pixel Data', :parent => dcm, :vr => 'OW')
|
26
|
+
#
|
27
|
+
def initialize(tag, options={})
|
28
|
+
raise ArgumentError, "The supplied tag (#{tag}) is not valid. The tag must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
|
29
|
+
# Set common parent variables:
|
30
|
+
initialize_parent
|
31
|
+
# Set instance variables:
|
32
|
+
@tag = tag.upcase
|
33
|
+
@value = nil
|
34
|
+
@bin = nil
|
35
|
+
# We may beed to retrieve name and vr from the library:
|
36
|
+
if options[:name] and options[:vr]
|
37
|
+
@name = options[:name]
|
38
|
+
@vr = options[:vr]
|
39
|
+
else
|
40
|
+
name, vr = LIBRARY.name_and_vr(tag)
|
41
|
+
@name = options[:name] || name
|
42
|
+
@vr = options[:vr] || 'SQ'
|
43
|
+
end
|
44
|
+
@length = options[:length] || -1
|
45
|
+
if options[:parent]
|
46
|
+
@parent = options[:parent]
|
47
|
+
@parent.add(self, :no_follow => true)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks for equality.
|
52
|
+
#
|
53
|
+
# Other and self are considered equivalent if they are
|
54
|
+
# of compatible types and their attributes are equivalent.
|
55
|
+
#
|
56
|
+
# @param other an object to be compared with self.
|
57
|
+
# @return [Boolean] true if self and other are considered equivalent
|
58
|
+
#
|
59
|
+
def ==(other)
|
60
|
+
if other.respond_to?(:to_sequence)
|
61
|
+
other.send(:state) == state
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
alias_method :eql?, :==
|
66
|
+
|
67
|
+
# Computes a hash code for this object.
|
68
|
+
#
|
69
|
+
# @note Two objects with the same attributes will have the same hash code.
|
70
|
+
#
|
71
|
+
# @return [Fixnum] the object's hash code
|
72
|
+
#
|
73
|
+
def hash
|
74
|
+
state.hash
|
75
|
+
end
|
76
|
+
|
77
|
+
# Loads data from an encoded DICOM string and creates
|
78
|
+
# items and elements which are linked to this instance.
|
79
|
+
#
|
80
|
+
# @param [String] bin an encoded binary string containing DICOM information
|
81
|
+
# @param [String] syntax the transfer syntax to use when decoding the DICOM string
|
82
|
+
#
|
83
|
+
def parse(bin, syntax)
|
84
|
+
raise ArgumentError, "Invalid argument 'bin'. Expected String, got #{bin.class}." unless bin.is_a?(String)
|
85
|
+
raise ArgumentError, "Invalid argument 'syntax'. Expected String, got #{syntax.class}." unless syntax.is_a?(String)
|
86
|
+
read(bin, signature=false, :syntax => syntax)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns self.
|
90
|
+
#
|
91
|
+
# @return [Sequence] self
|
92
|
+
#
|
93
|
+
def to_sequence
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
|
101
|
+
# Collects the attributes of this instance.
|
102
|
+
#
|
103
|
+
# @return [Array<String, Item>] an array of attributes
|
104
|
+
#
|
105
|
+
def state
|
106
|
+
[@tag, @vr, @tags]
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
93
110
|
end
|
data/lib/dicom/stream.rb
CHANGED
@@ -1,512 +1,481 @@
|
|
1
|
-
module DICOM
|
2
|
-
|
3
|
-
# The Stream class handles string operations (encoding to and decoding from binary strings).
|
4
|
-
# It is used by the various classes of
|
5
|
-
#
|
6
|
-
#
|
7
|
-
class
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
value =
|
90
|
-
end
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
end
|
221
|
-
return
|
222
|
-
end
|
223
|
-
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
str
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
#
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
#
|
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
|
-
def
|
339
|
-
@file
|
340
|
-
end
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
#
|
347
|
-
#
|
348
|
-
def
|
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
|
-
# Note: Surprisingly the Ruby pack/unpack methods lack a format for signed short
|
484
|
-
# and signed long in the network byte order. A hack has been implemented to to ensure
|
485
|
-
# correct behaviour in this case, but it is slower (~4 times slower than a normal pack/unpack).
|
486
|
-
#
|
487
|
-
def set_string_formats
|
488
|
-
if @equal_endian
|
489
|
-
# Native byte order:
|
490
|
-
@us = "S*" # Unsigned short (2 bytes)
|
491
|
-
@ss = "s*" # Signed short (2 bytes)
|
492
|
-
@ul = "I*" # Unsigned long (4 bytes)
|
493
|
-
@sl = "l*" # Signed long (4 bytes)
|
494
|
-
@fs = "e*" # Floating point single (4 bytes)
|
495
|
-
@fd = "E*" # Floating point double ( 8 bytes)
|
496
|
-
else
|
497
|
-
# Network byte order:
|
498
|
-
@us = "n*"
|
499
|
-
@ss = CUSTOM_SS # Custom string for our redefined pack/unpack.
|
500
|
-
@ul = "N*"
|
501
|
-
@sl = CUSTOM_SL # Custom string for our redefined pack/unpack.
|
502
|
-
@fs = "g*"
|
503
|
-
@fd = "G*"
|
504
|
-
end
|
505
|
-
# Format strings that are not dependent on endianness:
|
506
|
-
@by = "C*" # Unsigned char (1 byte)
|
507
|
-
@str = "a*"
|
508
|
-
@hex = "H*" # (this may be dependent on endianness(?))
|
509
|
-
end
|
510
|
-
|
511
|
-
end
|
1
|
+
module DICOM
|
2
|
+
|
3
|
+
# The Stream class handles string operations (encoding to and decoding from binary strings).
|
4
|
+
# It is used by the various classes of ruby-dicom for tasks such as reading and writing
|
5
|
+
# from/to files or network packets.
|
6
|
+
#
|
7
|
+
# @note In practice, this class is for internal library use. It is typically not accessed
|
8
|
+
# by the user, and can thus be considered a 'private' class.
|
9
|
+
#
|
10
|
+
class Stream
|
11
|
+
|
12
|
+
# A boolean which reports the relationship between the endianness of the system and the instance string.
|
13
|
+
attr_reader :equal_endian
|
14
|
+
# Our current position in the instance string (used only for decoding).
|
15
|
+
attr_accessor :index
|
16
|
+
# The instance string.
|
17
|
+
attr_accessor :string
|
18
|
+
# The endianness of the instance string.
|
19
|
+
attr_reader :str_endian
|
20
|
+
# An array of warning/error messages that (may) have been accumulated.
|
21
|
+
attr_reader :errors
|
22
|
+
# A hash with vr as key and its corresponding pad byte as value.
|
23
|
+
attr_reader :pad_byte
|
24
|
+
|
25
|
+
# Creates a Stream instance.
|
26
|
+
#
|
27
|
+
# @param [String, NilClass] binary a binary string (or nil, if creating an empty instance)
|
28
|
+
# @param [Boolean] string_endian the endianness of the instance string (true for big endian, false for small endian)
|
29
|
+
# @param [Hash] options the options to use for creating the instance
|
30
|
+
# @option options [Integer] :index a position (offset) in the instance string where reading will start
|
31
|
+
#
|
32
|
+
def initialize(binary, string_endian, options={})
|
33
|
+
@string = binary || ''
|
34
|
+
@index = options[:index] || 0
|
35
|
+
@errors = Array.new
|
36
|
+
self.endian = string_endian
|
37
|
+
end
|
38
|
+
|
39
|
+
# Prepends a pre-encoded string to the instance string (inserts at the beginning).
|
40
|
+
#
|
41
|
+
# @param [String] binary a binary string
|
42
|
+
#
|
43
|
+
def add_first(binary)
|
44
|
+
@string = binary + @string if binary
|
45
|
+
end
|
46
|
+
|
47
|
+
# Appends a pre-encoded string to the instance string (inserts at the end).
|
48
|
+
#
|
49
|
+
# @param [String] binary a binary string
|
50
|
+
#
|
51
|
+
def add_last(binary)
|
52
|
+
@string = @string + binary if binary
|
53
|
+
end
|
54
|
+
|
55
|
+
# Decodes a section of the instance string.
|
56
|
+
# The instance index is offset in accordance with the length read.
|
57
|
+
#
|
58
|
+
# @note If multiple numbers are decoded, these are returned in an array.
|
59
|
+
# @param [Integer] length the string length to be decoded
|
60
|
+
# @param [String] type the type (vr) of data to decode
|
61
|
+
# @return [String, Integer, Float, Array] the formatted (decoded) data
|
62
|
+
#
|
63
|
+
def decode(length, type)
|
64
|
+
raise ArgumentError, "Invalid argument length. Expected Fixnum, got #{length.class}" unless length.is_a?(Fixnum)
|
65
|
+
raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
|
66
|
+
# Check if values are valid:
|
67
|
+
if (@index + length) > @string.length
|
68
|
+
# The index number is bigger then the length of the binary string.
|
69
|
+
# We have reached the end and will return nil.
|
70
|
+
value = nil
|
71
|
+
else
|
72
|
+
if type == 'AT'
|
73
|
+
# We need to guard ourselves against the case where a string contains an invalid 'AT' value:
|
74
|
+
if length == 4
|
75
|
+
value = decode_tag
|
76
|
+
else
|
77
|
+
# Invalid. Just return nil.
|
78
|
+
skip(length)
|
79
|
+
value = nil
|
80
|
+
end
|
81
|
+
else
|
82
|
+
# Decode the binary string and return value:
|
83
|
+
value = @string.slice(@index, length).unpack(vr_to_str(type))
|
84
|
+
# If the result is an array of one element, return the element instead of the array.
|
85
|
+
# If result is contained in a multi-element array, the original array is returned.
|
86
|
+
if value.length == 1
|
87
|
+
value = value[0]
|
88
|
+
# If value is a string, strip away possible trailing whitespace:
|
89
|
+
value = value.rstrip if value.is_a?(String)
|
90
|
+
end
|
91
|
+
# Update our position in the string:
|
92
|
+
skip(length)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
return value
|
96
|
+
end
|
97
|
+
|
98
|
+
# Decodes the entire instance string (typically used for decoding image data).
|
99
|
+
#
|
100
|
+
# @note If multiple numbers are decoded, these are returned in an array.
|
101
|
+
# @param [String] type the type (vr) of data to decode
|
102
|
+
# @return [String, Integer, Float, Array] the formatted (decoded) data
|
103
|
+
#
|
104
|
+
def decode_all(type)
|
105
|
+
length = @string.length
|
106
|
+
value = @string.slice(@index, length).unpack(vr_to_str(type))
|
107
|
+
skip(length)
|
108
|
+
return value
|
109
|
+
end
|
110
|
+
|
111
|
+
# Decodes 4 bytes of the instance string and formats it as a ruby-dicom tag string.
|
112
|
+
#
|
113
|
+
# @return [String, NilClass] a formatted tag string ('GGGG,EEEE'), or nil (e.g. if at end of string)
|
114
|
+
#
|
115
|
+
def decode_tag
|
116
|
+
length = 4
|
117
|
+
# Check if values are valid:
|
118
|
+
if (@index + length) > @string.length
|
119
|
+
# The index number is bigger then the length of the binary string.
|
120
|
+
# We have reached the end and will return nil.
|
121
|
+
tag = nil
|
122
|
+
else
|
123
|
+
# Decode and process:
|
124
|
+
string = @string.slice(@index, length).unpack(@hex)[0].upcase
|
125
|
+
if @equal_endian
|
126
|
+
tag = string[2..3] + string[0..1] + ',' + string[6..7] + string[4..5]
|
127
|
+
else
|
128
|
+
tag = string[0..3] + ',' + string[4..7]
|
129
|
+
end
|
130
|
+
# Update our position in the string:
|
131
|
+
skip(length)
|
132
|
+
end
|
133
|
+
return tag
|
134
|
+
end
|
135
|
+
|
136
|
+
# Encodes a given value to a binary string.
|
137
|
+
#
|
138
|
+
# @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
|
139
|
+
# @param [String] type the type (vr) of data to encode
|
140
|
+
# @return [String] an encoded binary string
|
141
|
+
#
|
142
|
+
def encode(value, type)
|
143
|
+
raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
|
144
|
+
value = [value] unless value.is_a?(Array)
|
145
|
+
return value.pack(vr_to_str(type))
|
146
|
+
end
|
147
|
+
|
148
|
+
# Encodes a value to a binary string and prepends it to the instance string.
|
149
|
+
#
|
150
|
+
# @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
|
151
|
+
# @param [String] type the type (vr) of data to encode
|
152
|
+
#
|
153
|
+
def encode_first(value, type)
|
154
|
+
value = [value] unless value.is_a?(Array)
|
155
|
+
bin = value.pack(vr_to_str(type))
|
156
|
+
@string = bin + @string
|
157
|
+
end
|
158
|
+
|
159
|
+
# Encodes a value to a binary string and appends it to the instance string.
|
160
|
+
#
|
161
|
+
# @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
|
162
|
+
# @param [String] type the type (vr) of data to encode
|
163
|
+
#
|
164
|
+
def encode_last(value, type)
|
165
|
+
value = [value] unless value.is_a?(Array)
|
166
|
+
bin = value.pack(vr_to_str(type))
|
167
|
+
@string = @string + bin
|
168
|
+
end
|
169
|
+
|
170
|
+
# Appends a string with trailling spaces to achieve a target length, and encodes it to a binary string.
|
171
|
+
#
|
172
|
+
# @param [String] string a string to be padded
|
173
|
+
# @param [Integer] target_length the target length of the string
|
174
|
+
# @return [String] an encoded binary string
|
175
|
+
#
|
176
|
+
def encode_string_with_trailing_spaces(string, target_length)
|
177
|
+
length = string.length
|
178
|
+
if length < target_length
|
179
|
+
return [string].pack(@str)+['20'*(target_length-length)].pack(@hex)
|
180
|
+
elsif length == target_length
|
181
|
+
return [string].pack(@str)
|
182
|
+
else
|
183
|
+
raise "The specified string is longer than the allowed maximum length (String: #{string}, Target length: #{target_length})."
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Encodes a tag from the ruby-dicom format ('GGGG,EEEE') to a proper binary string.
|
188
|
+
#
|
189
|
+
# @param [String] tag a ruby-dicom type tag string
|
190
|
+
# @return [String] an encoded binary string
|
191
|
+
#
|
192
|
+
def encode_tag(tag)
|
193
|
+
if @equal_endian
|
194
|
+
clean_tag = tag[2..3] + tag[0..1] + tag[7..8] + tag[5..6]
|
195
|
+
else
|
196
|
+
clean_tag = tag[0..3] + tag[5..8]
|
197
|
+
end
|
198
|
+
return [clean_tag].pack(@hex)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Encodes a value, and if the the resulting binary string has an
|
202
|
+
# odd length, appends a proper padding byte to make it even length.
|
203
|
+
#
|
204
|
+
# @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
|
205
|
+
# @param [String] vr the value representation of data to encode
|
206
|
+
# @return [String] the encoded binary string
|
207
|
+
#
|
208
|
+
def encode_value(value, vr)
|
209
|
+
if vr == 'AT'
|
210
|
+
bin = encode_tag(value)
|
211
|
+
else
|
212
|
+
# Make sure the value is in an array:
|
213
|
+
value = [value] unless value.is_a?(Array)
|
214
|
+
# Get the proper pack string:
|
215
|
+
type = vr_to_str(vr)
|
216
|
+
# Encode:
|
217
|
+
bin = value.pack(type)
|
218
|
+
# Add an empty byte if the resulting binary has an odd length:
|
219
|
+
bin = bin + @pad_byte[vr] if bin.length.odd?
|
220
|
+
end
|
221
|
+
return bin
|
222
|
+
end
|
223
|
+
|
224
|
+
# Sets the endianness of the instance string. The relationship between the string
|
225
|
+
# endianness and the system endianness determines which encoding/decoding flags to use.
|
226
|
+
#
|
227
|
+
# @param [Boolean] string_endian the endianness of the instance string (true for big endian, false for small endian)
|
228
|
+
#
|
229
|
+
def endian=(string_endian)
|
230
|
+
@str_endian = string_endian
|
231
|
+
configure_endian
|
232
|
+
set_pad_byte
|
233
|
+
set_string_formats
|
234
|
+
set_format_hash
|
235
|
+
end
|
236
|
+
|
237
|
+
# Extracts the entire instance string, or optionally,
|
238
|
+
# just the first part of it if a length is specified.
|
239
|
+
#
|
240
|
+
# @note The exported string is removed from the instance string.
|
241
|
+
# @param [Integer] length the length of the string to cut out (if nil, the entire string is exported)
|
242
|
+
# @return [String] the instance string (or part of it)
|
243
|
+
#
|
244
|
+
def export(length=nil)
|
245
|
+
if length
|
246
|
+
string = @string.slice!(0, length)
|
247
|
+
else
|
248
|
+
string = @string
|
249
|
+
reset
|
250
|
+
end
|
251
|
+
return string
|
252
|
+
end
|
253
|
+
|
254
|
+
# Extracts and returns a binary string of the given length, starting at the index position.
|
255
|
+
# The instance index is then offset in accordance with the length read.
|
256
|
+
#
|
257
|
+
# @param [Integer] length the length of the string to be extracted
|
258
|
+
# @return [String] a part of the instance string
|
259
|
+
#
|
260
|
+
def extract(length)
|
261
|
+
str = @string.slice(@index, length)
|
262
|
+
skip(length)
|
263
|
+
return str
|
264
|
+
end
|
265
|
+
|
266
|
+
# Gives the length of the instance string.
|
267
|
+
#
|
268
|
+
# @return [Integer] the instance string's length
|
269
|
+
#
|
270
|
+
def length
|
271
|
+
return @string.length
|
272
|
+
end
|
273
|
+
|
274
|
+
# Calculates the remaining length of the instance string (from the index position).
|
275
|
+
#
|
276
|
+
# @return [Integer] the remaining length of the instance string
|
277
|
+
#
|
278
|
+
def rest_length
|
279
|
+
length = @string.length - @index
|
280
|
+
return length
|
281
|
+
end
|
282
|
+
|
283
|
+
# Extracts the remaining part of the instance string (from the index position to the end of the string).
|
284
|
+
#
|
285
|
+
# @return [String] the remaining part of the instance string
|
286
|
+
#
|
287
|
+
def rest_string
|
288
|
+
str = @string[@index..(@string.length-1)]
|
289
|
+
return str
|
290
|
+
end
|
291
|
+
|
292
|
+
# Resets the instance string and index.
|
293
|
+
#
|
294
|
+
def reset
|
295
|
+
@string = ''
|
296
|
+
@index = 0
|
297
|
+
end
|
298
|
+
|
299
|
+
# Resets the instance index.
|
300
|
+
#
|
301
|
+
def reset_index
|
302
|
+
@index = 0
|
303
|
+
end
|
304
|
+
|
305
|
+
# Sets the instance file variable.
|
306
|
+
#
|
307
|
+
# @note For performance reasons, we enable the Stream instance to write directly to file,
|
308
|
+
# to avoid expensive string operations which will otherwise slow down the write performance.
|
309
|
+
#
|
310
|
+
# @param [File] file a File object
|
311
|
+
#
|
312
|
+
def set_file(file)
|
313
|
+
@file = file
|
314
|
+
end
|
315
|
+
|
316
|
+
# Sets a new instance string, and resets the index variable.
|
317
|
+
#
|
318
|
+
# @param [String] binary an encoded string
|
319
|
+
#
|
320
|
+
def set_string(binary)
|
321
|
+
binary = binary[0] if binary.is_a?(Array)
|
322
|
+
@string = binary
|
323
|
+
@index = 0
|
324
|
+
end
|
325
|
+
|
326
|
+
# Applies an offset (positive or negative) to the instance index.
|
327
|
+
#
|
328
|
+
# @param [Integer] offset the length to skip (positive) or rewind (negative)
|
329
|
+
#
|
330
|
+
def skip(offset)
|
331
|
+
@index += offset
|
332
|
+
end
|
333
|
+
|
334
|
+
# Writes a binary string to the File object of this instance.
|
335
|
+
#
|
336
|
+
# @param [String] binary a binary string
|
337
|
+
#
|
338
|
+
def write(binary)
|
339
|
+
@file.write(binary)
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
private
|
344
|
+
|
345
|
+
|
346
|
+
# Determines the relationship between system and string endianness, and sets the instance endian variable.
|
347
|
+
#
|
348
|
+
def configure_endian
|
349
|
+
if CPU_ENDIAN == @str_endian
|
350
|
+
@equal_endian = true
|
351
|
+
else
|
352
|
+
@equal_endian = false
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Converts a data type/vr to an encode/decode string used by Ruby's pack/unpack methods.
|
357
|
+
#
|
358
|
+
# @param [String] vr a value representation (data type)
|
359
|
+
# @return [String] an encode/decode format string
|
360
|
+
#
|
361
|
+
def vr_to_str(vr)
|
362
|
+
unless @format[vr]
|
363
|
+
errors << "Warning: Element type #{vr} does not have a reading method assigned to it. Something is not implemented correctly or the DICOM data analyzed is invalid."
|
364
|
+
return @hex
|
365
|
+
else
|
366
|
+
return @format[vr]
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
# Sets the hash which is used to convert data element types (VR) to
|
371
|
+
# encode/decode format strings accepted by Ruby's pack/unpack methods.
|
372
|
+
#
|
373
|
+
def set_format_hash
|
374
|
+
@format = {
|
375
|
+
"BY" => @by, # Byte/Character (1-byte integers)
|
376
|
+
"US" => @us, # Unsigned short (2 bytes)
|
377
|
+
"SS" => @ss, # Signed short (2 bytes)
|
378
|
+
"UL" => @ul, # Unsigned long (4 bytes)
|
379
|
+
"SL" => @sl, # Signed long (4 bytes)
|
380
|
+
"FL" => @fs, # Floating point single (4 bytes)
|
381
|
+
"FD" => @fd, # Floating point double (8 bytes)
|
382
|
+
"OB" => @by, # Other byte string (1-byte integers)
|
383
|
+
"OF" => @fs, # Other float string (4-byte floating point numbers)
|
384
|
+
"OW" => @us, # Other word string (2-byte integers)
|
385
|
+
"AT" => @hex, # Tag reference (4 bytes) NB: For tags the spesialized encode_tag/decode_tag methods are used instead of this lookup table.
|
386
|
+
"UN" => @hex, # Unknown information (header element is not recognized from local database)
|
387
|
+
"HEX" => @hex, # HEX
|
388
|
+
# We have a number of VRs that are decoded as string:
|
389
|
+
"AE" => @str,
|
390
|
+
"AS" => @str,
|
391
|
+
"CS" => @str,
|
392
|
+
"DA" => @str,
|
393
|
+
"DS" => @str,
|
394
|
+
"DT" => @str,
|
395
|
+
"IS" => @str,
|
396
|
+
"LO" => @str,
|
397
|
+
"LT" => @str,
|
398
|
+
"PN" => @str,
|
399
|
+
"SH" => @str,
|
400
|
+
"ST" => @str,
|
401
|
+
"TM" => @str,
|
402
|
+
"UI" => @str,
|
403
|
+
"UT" => @str,
|
404
|
+
"STR" => @str
|
405
|
+
}
|
406
|
+
end
|
407
|
+
|
408
|
+
# Sets the hash which is used to keep track of which bytes to use for padding
|
409
|
+
# data elements of various vr which have an odd value length.
|
410
|
+
#
|
411
|
+
def set_pad_byte
|
412
|
+
@pad_byte = {
|
413
|
+
# Space character:
|
414
|
+
"AE" => "\x20",
|
415
|
+
"AS" => "\x20",
|
416
|
+
"CS" => "\x20",
|
417
|
+
"DA" => "\x20",
|
418
|
+
"DS" => "\x20",
|
419
|
+
"DT" => "\x20",
|
420
|
+
"IS" => "\x20",
|
421
|
+
"LO" => "\x20",
|
422
|
+
"LT" => "\x20",
|
423
|
+
"PN" => "\x20",
|
424
|
+
"SH" => "\x20",
|
425
|
+
"ST" => "\x20",
|
426
|
+
"TM" => "\x20",
|
427
|
+
"UT" => "\x20",
|
428
|
+
# Zero byte:
|
429
|
+
"AT" => "\x00",
|
430
|
+
"BY" => "\x00",
|
431
|
+
"FL" => "\x00",
|
432
|
+
"FD" => "\x00",
|
433
|
+
"OB" => "\x00",
|
434
|
+
"OF" => "\x00",
|
435
|
+
"OW" => "\x00",
|
436
|
+
"SL" => "\x00",
|
437
|
+
"SQ" => "\x00",
|
438
|
+
"SS" => "\x00",
|
439
|
+
"UI" => "\x00",
|
440
|
+
"UL" => "\x00",
|
441
|
+
"UN" => "\x00",
|
442
|
+
"US" => "\x00"
|
443
|
+
}
|
444
|
+
end
|
445
|
+
|
446
|
+
# Sets the pack/unpack format strings that are used for encoding/decoding.
|
447
|
+
# Some of these depends on the endianness of the system and the encoded string.
|
448
|
+
#
|
449
|
+
def set_string_formats
|
450
|
+
# FIXME:
|
451
|
+
# Surprisingly the Ruby pack/unpack methods lack a format for signed short
|
452
|
+
# and signed long in the network byte order. A hack has been implemented to to ensure
|
453
|
+
# correct behaviour in this case, but it is slower (~4 times slower than a normal pack/unpack).
|
454
|
+
# Update: This seems to have been fixed in Ruby 1.9.3, so when we are able to bump the Ruby
|
455
|
+
# dependency eventually, this situation can finally be cleaned up.
|
456
|
+
#
|
457
|
+
if @equal_endian
|
458
|
+
# Native byte order:
|
459
|
+
@us = "S*" # Unsigned short (2 bytes)
|
460
|
+
@ss = "s*" # Signed short (2 bytes)
|
461
|
+
@ul = "I*" # Unsigned long (4 bytes)
|
462
|
+
@sl = "l*" # Signed long (4 bytes)
|
463
|
+
@fs = "e*" # Floating point single (4 bytes)
|
464
|
+
@fd = "E*" # Floating point double ( 8 bytes)
|
465
|
+
else
|
466
|
+
# Network byte order:
|
467
|
+
@us = "n*"
|
468
|
+
@ss = CUSTOM_SS # Custom string for our redefined pack/unpack.
|
469
|
+
@ul = "N*"
|
470
|
+
@sl = CUSTOM_SL # Custom string for our redefined pack/unpack.
|
471
|
+
@fs = "g*"
|
472
|
+
@fd = "G*"
|
473
|
+
end
|
474
|
+
# Format strings that are not dependent on endianness:
|
475
|
+
@by = "C*" # Unsigned char (1 byte)
|
476
|
+
@str = "a*"
|
477
|
+
@hex = "H*" # (this may be dependent on endianness(?))
|
478
|
+
end
|
479
|
+
|
480
|
+
end
|
512
481
|
end
|