dicom 0.9.3 → 0.9.4

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