dicom 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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