dicom 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,8 +26,8 @@ module DICOM
26
26
  # * <tt>:ae</tt> -- String. The name of the client (application entity).
27
27
  # * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
28
28
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
29
- # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
30
- # * <tt>:timeout</tt> -- Fixnum. The maximum period to wait for an answer before aborting the communication.
29
+ # * <tt>:max_package_size</tt> -- Integer. The maximum allowed size of network packages (in bytes).
30
+ # * <tt>:timeout</tt> -- Integer. The maximum period to wait for an answer before aborting the communication.
31
31
  #
32
32
  def initialize(options={})
33
33
  require 'socket'
@@ -1075,7 +1075,7 @@ module DICOM
1075
1075
  # === Parameters
1076
1076
  #
1077
1077
  # * <tt>adress</tt> -- String. The adress (IP) of the remote node.
1078
- # * <tt>port</tt> -- Fixnum. The network port to be used in the network communication.
1078
+ # * <tt>port</tt> -- Integer. The network port to be used in the network communication.
1079
1079
  #
1080
1080
  def start_session(adress, port)
1081
1081
  @session = TCPSocket.new(adress, port)
@@ -1303,7 +1303,7 @@ module DICOM
1303
1303
  #
1304
1304
  # === Parameters
1305
1305
  #
1306
- # * <tt>result</tt> -- Fixnum. The result code from an association response.
1306
+ # * <tt>result</tt> -- Integer. The result code from an association response.
1307
1307
  #
1308
1308
  def process_result(result)
1309
1309
  unless result == 0
@@ -1350,7 +1350,7 @@ module DICOM
1350
1350
  #
1351
1351
  # === Parameters
1352
1352
  #
1353
- # * <tt>status</tt> -- Fixnum. A status code from a command fragment.
1353
+ # * <tt>status</tt> -- Integer. A status code from a command fragment.
1354
1354
  #
1355
1355
  def process_status(status)
1356
1356
  case status
@@ -1397,7 +1397,7 @@ module DICOM
1397
1397
  #
1398
1398
  # === Parameters
1399
1399
  #
1400
- # * <tt>min_length</tt> -- Fixnum. The minimum possible length of a valid incoming transmission.
1400
+ # * <tt>min_length</tt> -- Integer. The minimum possible length of a valid incoming transmission.
1401
1401
  #
1402
1402
  def receive_transmission(min_length=0)
1403
1403
  data = receive_transmission_data
@@ -1,98 +1,98 @@
1
- module DICOM
2
-
3
- # The Sequence class handles information related to Sequence elements.
4
- #
5
- class Sequence < Parent
6
-
7
- include Elemental
8
- include ElementalParent
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
- # Returns self.
78
- #
79
- # @return [Sequence] self
80
- #
81
- def to_sequence
82
- self
83
- end
84
-
85
-
86
- private
87
-
88
-
89
- # Collects the attributes of this instance.
90
- #
91
- # @return [Array<String, Item>] an array of attributes
92
- #
93
- def state
94
- [@tag, @vr, @tags]
95
- end
96
-
97
- end
98
- end
1
+ module DICOM
2
+
3
+ # The Sequence class handles information related to Sequence elements.
4
+ #
5
+ class Sequence < Parent
6
+
7
+ include Elemental
8
+ include ElementalParent
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 [Integer] the object's hash code
72
+ #
73
+ def hash
74
+ state.hash
75
+ end
76
+
77
+ # Returns self.
78
+ #
79
+ # @return [Sequence] self
80
+ #
81
+ def to_sequence
82
+ self
83
+ end
84
+
85
+
86
+ private
87
+
88
+
89
+ # Collects the attributes of this instance.
90
+ #
91
+ # @return [Array<String, Item>] an array of attributes
92
+ #
93
+ def state
94
+ [@tag, @vr, @tags]
95
+ end
96
+
97
+ end
98
+ end
@@ -1,461 +1,461 @@
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
- value = nil
67
- if (@index + length) <= @string.length
68
- # There are sufficient bytes remaining to extract the value:
69
- if type == 'AT'
70
- # We need to guard ourselves against the case where a string contains an invalid 'AT' value:
71
- if length == 4
72
- value = decode_tag
73
- else
74
- # Invalid. Just return nil.
75
- skip(length)
76
- end
77
- else
78
- # Decode the binary string and return value:
79
- value = @string.slice(@index, length).unpack(vr_to_str(type))
80
- # If the result is an array of one element, return the element instead of the array.
81
- # If result is contained in a multi-element array, the original array is returned.
82
- if value.length == 1
83
- value = value[0]
84
- # If value is a string, strip away possible trailing whitespace:
85
- value = value.rstrip if value.is_a?(String)
86
- end
87
- # Update our position in the string:
88
- skip(length)
89
- end
90
- end
91
- value
92
- end
93
-
94
- # Decodes the entire instance string (typically used for decoding image data).
95
- #
96
- # @note If multiple numbers are decoded, these are returned in an array.
97
- # @param [String] type the type (vr) of data to decode
98
- # @return [String, Integer, Float, Array] the formatted (decoded) data
99
- #
100
- def decode_all(type)
101
- length = @string.length
102
- value = @string.slice(@index, length).unpack(vr_to_str(type))
103
- skip(length)
104
- return value
105
- end
106
-
107
- # Decodes 4 bytes of the instance string and formats it as a ruby-dicom tag string.
108
- #
109
- # @return [String, NilClass] a formatted tag string ('GGGG,EEEE'), or nil (e.g. if at end of string)
110
- #
111
- def decode_tag
112
- length = 4
113
- tag = nil
114
- if (@index + length) <= @string.length
115
- # There are sufficient bytes remaining to extract a full tag:
116
- str = @string.slice(@index, length).unpack(@hex)[0].upcase
117
- if @equal_endian
118
- tag = "#{str[2..3]}#{str[0..1]},#{str[6..7]}#{str[4..5]}"
119
- else
120
- tag = "#{str[0..3]},#{str[4..7]}"
121
- end
122
- # Update our position in the string:
123
- skip(length)
124
- end
125
- tag
126
- end
127
-
128
- # Encodes a given value to a binary string.
129
- #
130
- # @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
131
- # @param [String] type the type (vr) of data to encode
132
- # @return [String] an encoded binary string
133
- #
134
- def encode(value, type)
135
- raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
136
- value = [value] unless value.is_a?(Array)
137
- return value.pack(vr_to_str(type))
138
- end
139
-
140
- # Encodes a value to a binary string and prepends it to the instance string.
141
- #
142
- # @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
143
- # @param [String] type the type (vr) of data to encode
144
- #
145
- def encode_first(value, type)
146
- value = [value] unless value.is_a?(Array)
147
- @string = "#{value.pack(vr_to_str(type))}#{@string}"
148
- end
149
-
150
- # Encodes a value to a binary string and appends it to the instance string.
151
- #
152
- # @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
153
- # @param [String] type the type (vr) of data to encode
154
- #
155
- def encode_last(value, type)
156
- value = [value] unless value.is_a?(Array)
157
- @string = "#{@string}#{value.pack(vr_to_str(type))}"
158
- end
159
-
160
- # Appends a string with trailling spaces to achieve a target length, and encodes it to a binary string.
161
- #
162
- # @param [String] string a string to be padded
163
- # @param [Integer] target_length the target length of the string
164
- # @return [String] an encoded binary string
165
- #
166
- def encode_string_with_trailing_spaces(string, target_length)
167
- length = string.length
168
- if length < target_length
169
- return "#{[string].pack(@str)}#{['20'*(target_length-length)].pack(@hex)}"
170
- elsif length == target_length
171
- return [string].pack(@str)
172
- else
173
- raise "The specified string is longer than the allowed maximum length (String: #{string}, Target length: #{target_length})."
174
- end
175
- end
176
-
177
- # Encodes a tag from the ruby-dicom format ('GGGG,EEEE') to a proper binary string.
178
- #
179
- # @param [String] tag a ruby-dicom type tag string
180
- # @return [String] an encoded binary string
181
- #
182
- def encode_tag(tag)
183
- [
184
- @equal_endian ? "#{tag[2..3]}#{tag[0..1]}#{tag[7..8]}#{tag[5..6]}" : "#{tag[0..3]}#{tag[5..8]}"
185
- ].pack(@hex)
186
- end
187
-
188
- # Encodes a value, and if the the resulting binary string has an
189
- # odd length, appends a proper padding byte to make it even length.
190
- #
191
- # @param [String, Integer, Float, Array] value a formatted value (String, Fixnum, etc..) or an array of numbers
192
- # @param [String] vr the value representation of data to encode
193
- # @return [String] the encoded binary string
194
- #
195
- def encode_value(value, vr)
196
- if vr == 'AT'
197
- bin = encode_tag(value)
198
- else
199
- # Make sure the value is in an array:
200
- value = [value] unless value.is_a?(Array)
201
- # Get the proper pack string:
202
- type = vr_to_str(vr)
203
- # Encode:
204
- bin = value.pack(type)
205
- # Add an empty byte if the resulting binary has an odd length:
206
- bin = "#{bin}#{@pad_byte[vr]}" if bin.length.odd?
207
- end
208
- return bin
209
- end
210
-
211
- # Sets the endianness of the instance string. The relationship between the string
212
- # endianness and the system endianness determines which encoding/decoding flags to use.
213
- #
214
- # @param [Boolean] string_endian the endianness of the instance string (true for big endian, false for small endian)
215
- #
216
- def endian=(string_endian)
217
- @str_endian = string_endian
218
- configure_endian
219
- set_pad_byte
220
- set_string_formats
221
- set_format_hash
222
- end
223
-
224
- # Extracts the entire instance string, or optionally,
225
- # just the first part of it if a length is specified.
226
- #
227
- # @note The exported string is removed from the instance string.
228
- # @param [Integer] length the length of the string to cut out (if nil, the entire string is exported)
229
- # @return [String] the instance string (or part of it)
230
- #
231
- def export(length=nil)
232
- if length
233
- string = @string.slice!(0, length)
234
- else
235
- string = @string
236
- reset
237
- end
238
- return string
239
- end
240
-
241
- # Extracts and returns a binary string of the given length, starting at the index position.
242
- # The instance index is then offset in accordance with the length read.
243
- #
244
- # @param [Integer] length the length of the string to be extracted
245
- # @return [String] a part of the instance string
246
- #
247
- def extract(length)
248
- str = @string.slice(@index, length)
249
- skip(length)
250
- return str
251
- end
252
-
253
- # Gives the length of the instance string.
254
- #
255
- # @return [Integer] the instance string's length
256
- #
257
- def length
258
- return @string.length
259
- end
260
-
261
- # Calculates the remaining length of the instance string (from the index position).
262
- #
263
- # @return [Integer] the remaining length of the instance string
264
- #
265
- def rest_length
266
- length = @string.length - @index
267
- return length
268
- end
269
-
270
- # Extracts the remaining part of the instance string (from the index position to the end of the string).
271
- #
272
- # @return [String] the remaining part of the instance string
273
- #
274
- def rest_string
275
- str = @string[@index..(@string.length-1)]
276
- return str
277
- end
278
-
279
- # Resets the instance string and index.
280
- #
281
- def reset
282
- @string = ''
283
- @index = 0
284
- end
285
-
286
- # Resets the instance index.
287
- #
288
- def reset_index
289
- @index = 0
290
- end
291
-
292
- # Sets the instance file variable.
293
- #
294
- # @note For performance reasons, we enable the Stream instance to write directly to file,
295
- # to avoid expensive string operations which will otherwise slow down the write performance.
296
- #
297
- # @param [File] file a File object
298
- #
299
- def set_file(file)
300
- @file = file
301
- end
302
-
303
- # Sets a new instance string, and resets the index variable.
304
- #
305
- # @param [String] binary an encoded string
306
- #
307
- def set_string(binary)
308
- binary = binary[0] if binary.is_a?(Array)
309
- @string = binary
310
- @index = 0
311
- end
312
-
313
- # Applies an offset (positive or negative) to the instance index.
314
- #
315
- # @param [Integer] offset the length to skip (positive) or rewind (negative)
316
- #
317
- def skip(offset)
318
- @index += offset
319
- end
320
-
321
- # Writes a binary string to the File object of this instance.
322
- #
323
- # @param [String] binary a binary string
324
- #
325
- def write(binary)
326
- @file.write(binary)
327
- end
328
-
329
-
330
- private
331
-
332
-
333
- # Determines the relationship between system and string endianness, and sets the instance endian variable.
334
- #
335
- def configure_endian
336
- if CPU_ENDIAN == @str_endian
337
- @equal_endian = true
338
- else
339
- @equal_endian = false
340
- end
341
- end
342
-
343
- # Converts a data type/vr to an encode/decode string used by Ruby's pack/unpack methods.
344
- #
345
- # @param [String] vr a value representation (data type)
346
- # @return [String] an encode/decode format string
347
- #
348
- def vr_to_str(vr)
349
- unless @format[vr]
350
- 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."
351
- return @hex
352
- else
353
- return @format[vr]
354
- end
355
- end
356
-
357
- # Sets the hash which is used to convert data element types (VR) to
358
- # encode/decode format strings accepted by Ruby's pack/unpack methods.
359
- #
360
- def set_format_hash
361
- @format = {
362
- 'BY' => @by, # Byte/Character (1-byte integers)
363
- 'US' => @us, # Unsigned short (2 bytes)
364
- 'SS' => @ss, # Signed short (2 bytes)
365
- 'UL' => @ul, # Unsigned long (4 bytes)
366
- 'SL' => @sl, # Signed long (4 bytes)
367
- 'FL' => @fs, # Floating point single (4 bytes)
368
- 'FD' => @fd, # Floating point double (8 bytes)
369
- 'OB' => @by, # Other byte string (1-byte integers)
370
- 'OF' => @fs, # Other float string (4-byte floating point numbers)
371
- 'OW' => @us, # Other word string (2-byte integers)
372
- 'AT' => @hex, # Tag reference (4 bytes) NB: For tags the spesialized encode_tag/decode_tag methods are used instead of this lookup table.
373
- 'UN' => @hex, # Unknown information (header element is not recognized from local database)
374
- 'HEX' => @hex, # HEX
375
- # We have a number of VRs that are decoded as string:
376
- 'AE' => @str,
377
- 'AS' => @str,
378
- 'CS' => @str,
379
- 'DA' => @str,
380
- 'DS' => @str,
381
- 'DT' => @str,
382
- 'IS' => @str,
383
- 'LO' => @str,
384
- 'LT' => @str,
385
- 'PN' => @str,
386
- 'SH' => @str,
387
- 'ST' => @str,
388
- 'TM' => @str,
389
- 'UI' => @str,
390
- 'UT' => @str,
391
- 'STR' => @str
392
- }
393
- end
394
-
395
- # Sets the hash which is used to keep track of which bytes to use for padding
396
- # data elements of various vr which have an odd value length.
397
- #
398
- def set_pad_byte
399
- @pad_byte = {
400
- # Space character:
401
- 'AE' => "\x20",
402
- 'AS' => "\x20",
403
- 'CS' => "\x20",
404
- 'DA' => "\x20",
405
- 'DS' => "\x20",
406
- 'DT' => "\x20",
407
- 'IS' => "\x20",
408
- 'LO' => "\x20",
409
- 'LT' => "\x20",
410
- 'PN' => "\x20",
411
- 'SH' => "\x20",
412
- 'ST' => "\x20",
413
- 'TM' => "\x20",
414
- 'UT' => "\x20",
415
- # Zero byte:
416
- 'AT' => "\x00",
417
- 'BY' => "\x00",
418
- 'FL' => "\x00",
419
- 'FD' => "\x00",
420
- 'OB' => "\x00",
421
- 'OF' => "\x00",
422
- 'OW' => "\x00",
423
- 'SL' => "\x00",
424
- 'SQ' => "\x00",
425
- 'SS' => "\x00",
426
- 'UI' => "\x00",
427
- 'UL' => "\x00",
428
- 'UN' => "\x00",
429
- 'US' => "\x00"
430
- }
431
- end
432
-
433
- # Sets the pack/unpack format strings that are used for encoding/decoding.
434
- # Some of these depends on the endianness of the system and the encoded string.
435
- #
436
- def set_string_formats
437
- if @equal_endian
438
- # Little endian byte order:
439
- @us = 'S<*' # Unsigned short (2 bytes)
440
- @ss = 's<*' # Signed short (2 bytes)
441
- @ul = 'L<*' # Unsigned long (4 bytes)
442
- @sl = 'l<*' # Signed long (4 bytes)
443
- @fs = 'e*' # Floating point single (4 bytes)
444
- @fd = 'E*' # Floating point double ( 8 bytes)
445
- else
446
- # Network (big endian) byte order:
447
- @us = 'S>*'
448
- @ss = 's>*'
449
- @ul = 'L>*'
450
- @sl = 'l>'
451
- @fs = 'g*'
452
- @fd = 'G*'
453
- end
454
- # Format strings that are not dependent on endianness:
455
- @by = 'C*' # Unsigned char (1 byte)
456
- @str = 'a*'
457
- @hex = 'H*' # (this may be dependent on endianness(?))
458
- end
459
-
460
- end
461
- 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 Integer, got #{length.class}" unless length.is_a?(Integer)
65
+ raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
66
+ value = nil
67
+ if (@index + length) <= @string.length
68
+ # There are sufficient bytes remaining to extract the value:
69
+ if type == 'AT'
70
+ # We need to guard ourselves against the case where a string contains an invalid 'AT' value:
71
+ if length == 4
72
+ value = decode_tag
73
+ else
74
+ # Invalid. Just return nil.
75
+ skip(length)
76
+ end
77
+ else
78
+ # Decode the binary string and return value:
79
+ value = @string.slice(@index, length).unpack(vr_to_str(type))
80
+ # If the result is an array of one element, return the element instead of the array.
81
+ # If result is contained in a multi-element array, the original array is returned.
82
+ if value.length == 1
83
+ value = value[0]
84
+ # If value is a string, strip away possible trailing whitespace:
85
+ value = value.rstrip if value.is_a?(String)
86
+ end
87
+ # Update our position in the string:
88
+ skip(length)
89
+ end
90
+ end
91
+ value
92
+ end
93
+
94
+ # Decodes the entire instance string (typically used for decoding image data).
95
+ #
96
+ # @note If multiple numbers are decoded, these are returned in an array.
97
+ # @param [String] type the type (vr) of data to decode
98
+ # @return [String, Integer, Float, Array] the formatted (decoded) data
99
+ #
100
+ def decode_all(type)
101
+ length = @string.length
102
+ value = @string.slice(@index, length).unpack(vr_to_str(type))
103
+ skip(length)
104
+ return value
105
+ end
106
+
107
+ # Decodes 4 bytes of the instance string and formats it as a ruby-dicom tag string.
108
+ #
109
+ # @return [String, NilClass] a formatted tag string ('GGGG,EEEE'), or nil (e.g. if at end of string)
110
+ #
111
+ def decode_tag
112
+ length = 4
113
+ tag = nil
114
+ if (@index + length) <= @string.length
115
+ # There are sufficient bytes remaining to extract a full tag:
116
+ str = @string.slice(@index, length).unpack(@hex)[0].upcase
117
+ if @equal_endian
118
+ tag = "#{str[2..3]}#{str[0..1]},#{str[6..7]}#{str[4..5]}"
119
+ else
120
+ tag = "#{str[0..3]},#{str[4..7]}"
121
+ end
122
+ # Update our position in the string:
123
+ skip(length)
124
+ end
125
+ tag
126
+ end
127
+
128
+ # Encodes a given value to a binary string.
129
+ #
130
+ # @param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers
131
+ # @param [String] type the type (vr) of data to encode
132
+ # @return [String] an encoded binary string
133
+ #
134
+ def encode(value, type)
135
+ raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
136
+ value = [value] unless value.is_a?(Array)
137
+ return value.pack(vr_to_str(type))
138
+ end
139
+
140
+ # Encodes a value to a binary string and prepends it to the instance string.
141
+ #
142
+ # @param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers
143
+ # @param [String] type the type (vr) of data to encode
144
+ #
145
+ def encode_first(value, type)
146
+ value = [value] unless value.is_a?(Array)
147
+ @string = "#{value.pack(vr_to_str(type))}#{@string}"
148
+ end
149
+
150
+ # Encodes a value to a binary string and appends it to the instance string.
151
+ #
152
+ # @param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers
153
+ # @param [String] type the type (vr) of data to encode
154
+ #
155
+ def encode_last(value, type)
156
+ value = [value] unless value.is_a?(Array)
157
+ @string = "#{@string}#{value.pack(vr_to_str(type))}"
158
+ end
159
+
160
+ # Appends a string with trailling spaces to achieve a target length, and encodes it to a binary string.
161
+ #
162
+ # @param [String] string a string to be padded
163
+ # @param [Integer] target_length the target length of the string
164
+ # @return [String] an encoded binary string
165
+ #
166
+ def encode_string_with_trailing_spaces(string, target_length)
167
+ length = string.length
168
+ if length < target_length
169
+ return "#{[string].pack(@str)}#{['20'*(target_length-length)].pack(@hex)}"
170
+ elsif length == target_length
171
+ return [string].pack(@str)
172
+ else
173
+ raise "The specified string is longer than the allowed maximum length (String: #{string}, Target length: #{target_length})."
174
+ end
175
+ end
176
+
177
+ # Encodes a tag from the ruby-dicom format ('GGGG,EEEE') to a proper binary string.
178
+ #
179
+ # @param [String] tag a ruby-dicom type tag string
180
+ # @return [String] an encoded binary string
181
+ #
182
+ def encode_tag(tag)
183
+ [
184
+ @equal_endian ? "#{tag[2..3]}#{tag[0..1]}#{tag[7..8]}#{tag[5..6]}" : "#{tag[0..3]}#{tag[5..8]}"
185
+ ].pack(@hex)
186
+ end
187
+
188
+ # Encodes a value, and if the the resulting binary string has an
189
+ # odd length, appends a proper padding byte to make it even length.
190
+ #
191
+ # @param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers
192
+ # @param [String] vr the value representation of data to encode
193
+ # @return [String] the encoded binary string
194
+ #
195
+ def encode_value(value, vr)
196
+ if vr == 'AT'
197
+ bin = encode_tag(value)
198
+ else
199
+ # Make sure the value is in an array:
200
+ value = [value] unless value.is_a?(Array)
201
+ # Get the proper pack string:
202
+ type = vr_to_str(vr)
203
+ # Encode:
204
+ bin = value.pack(type)
205
+ # Add an empty byte if the resulting binary has an odd length:
206
+ bin = "#{bin}#{@pad_byte[vr]}" if bin.length.odd?
207
+ end
208
+ return bin
209
+ end
210
+
211
+ # Sets the endianness of the instance string. The relationship between the string
212
+ # endianness and the system endianness determines which encoding/decoding flags to use.
213
+ #
214
+ # @param [Boolean] string_endian the endianness of the instance string (true for big endian, false for small endian)
215
+ #
216
+ def endian=(string_endian)
217
+ @str_endian = string_endian
218
+ configure_endian
219
+ set_pad_byte
220
+ set_string_formats
221
+ set_format_hash
222
+ end
223
+
224
+ # Extracts the entire instance string, or optionally,
225
+ # just the first part of it if a length is specified.
226
+ #
227
+ # @note The exported string is removed from the instance string.
228
+ # @param [Integer] length the length of the string to cut out (if nil, the entire string is exported)
229
+ # @return [String] the instance string (or part of it)
230
+ #
231
+ def export(length=nil)
232
+ if length
233
+ string = @string.slice!(0, length)
234
+ else
235
+ string = @string
236
+ reset
237
+ end
238
+ return string
239
+ end
240
+
241
+ # Extracts and returns a binary string of the given length, starting at the index position.
242
+ # The instance index is then offset in accordance with the length read.
243
+ #
244
+ # @param [Integer] length the length of the string to be extracted
245
+ # @return [String] a part of the instance string
246
+ #
247
+ def extract(length)
248
+ str = @string.slice(@index, length)
249
+ skip(length)
250
+ return str
251
+ end
252
+
253
+ # Gives the length of the instance string.
254
+ #
255
+ # @return [Integer] the instance string's length
256
+ #
257
+ def length
258
+ return @string.length
259
+ end
260
+
261
+ # Calculates the remaining length of the instance string (from the index position).
262
+ #
263
+ # @return [Integer] the remaining length of the instance string
264
+ #
265
+ def rest_length
266
+ length = @string.length - @index
267
+ return length
268
+ end
269
+
270
+ # Extracts the remaining part of the instance string (from the index position to the end of the string).
271
+ #
272
+ # @return [String] the remaining part of the instance string
273
+ #
274
+ def rest_string
275
+ str = @string[@index..(@string.length-1)]
276
+ return str
277
+ end
278
+
279
+ # Resets the instance string and index.
280
+ #
281
+ def reset
282
+ @string = ''
283
+ @index = 0
284
+ end
285
+
286
+ # Resets the instance index.
287
+ #
288
+ def reset_index
289
+ @index = 0
290
+ end
291
+
292
+ # Sets the instance file variable.
293
+ #
294
+ # @note For performance reasons, we enable the Stream instance to write directly to file,
295
+ # to avoid expensive string operations which will otherwise slow down the write performance.
296
+ #
297
+ # @param [File] file a File object
298
+ #
299
+ def set_file(file)
300
+ @file = file
301
+ end
302
+
303
+ # Sets a new instance string, and resets the index variable.
304
+ #
305
+ # @param [String] binary an encoded string
306
+ #
307
+ def set_string(binary)
308
+ binary = binary[0] if binary.is_a?(Array)
309
+ @string = binary
310
+ @index = 0
311
+ end
312
+
313
+ # Applies an offset (positive or negative) to the instance index.
314
+ #
315
+ # @param [Integer] offset the length to skip (positive) or rewind (negative)
316
+ #
317
+ def skip(offset)
318
+ @index += offset
319
+ end
320
+
321
+ # Writes a binary string to the File object of this instance.
322
+ #
323
+ # @param [String] binary a binary string
324
+ #
325
+ def write(binary)
326
+ @file.write(binary)
327
+ end
328
+
329
+
330
+ private
331
+
332
+
333
+ # Determines the relationship between system and string endianness, and sets the instance endian variable.
334
+ #
335
+ def configure_endian
336
+ if CPU_ENDIAN == @str_endian
337
+ @equal_endian = true
338
+ else
339
+ @equal_endian = false
340
+ end
341
+ end
342
+
343
+ # Converts a data type/vr to an encode/decode string used by Ruby's pack/unpack methods.
344
+ #
345
+ # @param [String] vr a value representation (data type)
346
+ # @return [String] an encode/decode format string
347
+ #
348
+ def vr_to_str(vr)
349
+ unless @format[vr]
350
+ 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."
351
+ return @hex
352
+ else
353
+ return @format[vr]
354
+ end
355
+ end
356
+
357
+ # Sets the hash which is used to convert data element types (VR) to
358
+ # encode/decode format strings accepted by Ruby's pack/unpack methods.
359
+ #
360
+ def set_format_hash
361
+ @format = {
362
+ 'BY' => @by, # Byte/Character (1-byte integers)
363
+ 'US' => @us, # Unsigned short (2 bytes)
364
+ 'SS' => @ss, # Signed short (2 bytes)
365
+ 'UL' => @ul, # Unsigned long (4 bytes)
366
+ 'SL' => @sl, # Signed long (4 bytes)
367
+ 'FL' => @fs, # Floating point single (4 bytes)
368
+ 'FD' => @fd, # Floating point double (8 bytes)
369
+ 'OB' => @by, # Other byte string (1-byte integers)
370
+ 'OF' => @fs, # Other float string (4-byte floating point numbers)
371
+ 'OW' => @us, # Other word string (2-byte integers)
372
+ 'AT' => @hex, # Tag reference (4 bytes) NB: For tags the spesialized encode_tag/decode_tag methods are used instead of this lookup table.
373
+ 'UN' => @hex, # Unknown information (header element is not recognized from local database)
374
+ 'HEX' => @hex, # HEX
375
+ # We have a number of VRs that are decoded as string:
376
+ 'AE' => @str,
377
+ 'AS' => @str,
378
+ 'CS' => @str,
379
+ 'DA' => @str,
380
+ 'DS' => @str,
381
+ 'DT' => @str,
382
+ 'IS' => @str,
383
+ 'LO' => @str,
384
+ 'LT' => @str,
385
+ 'PN' => @str,
386
+ 'SH' => @str,
387
+ 'ST' => @str,
388
+ 'TM' => @str,
389
+ 'UI' => @str,
390
+ 'UT' => @str,
391
+ 'STR' => @str
392
+ }
393
+ end
394
+
395
+ # Sets the hash which is used to keep track of which bytes to use for padding
396
+ # data elements of various vr which have an odd value length.
397
+ #
398
+ def set_pad_byte
399
+ @pad_byte = {
400
+ # Space character:
401
+ 'AE' => "\x20",
402
+ 'AS' => "\x20",
403
+ 'CS' => "\x20",
404
+ 'DA' => "\x20",
405
+ 'DS' => "\x20",
406
+ 'DT' => "\x20",
407
+ 'IS' => "\x20",
408
+ 'LO' => "\x20",
409
+ 'LT' => "\x20",
410
+ 'PN' => "\x20",
411
+ 'SH' => "\x20",
412
+ 'ST' => "\x20",
413
+ 'TM' => "\x20",
414
+ 'UT' => "\x20",
415
+ # Zero byte:
416
+ 'AT' => "\x00",
417
+ 'BY' => "\x00",
418
+ 'FL' => "\x00",
419
+ 'FD' => "\x00",
420
+ 'OB' => "\x00",
421
+ 'OF' => "\x00",
422
+ 'OW' => "\x00",
423
+ 'SL' => "\x00",
424
+ 'SQ' => "\x00",
425
+ 'SS' => "\x00",
426
+ 'UI' => "\x00",
427
+ 'UL' => "\x00",
428
+ 'UN' => "\x00",
429
+ 'US' => "\x00"
430
+ }
431
+ end
432
+
433
+ # Sets the pack/unpack format strings that are used for encoding/decoding.
434
+ # Some of these depends on the endianness of the system and the encoded string.
435
+ #
436
+ def set_string_formats
437
+ if @equal_endian
438
+ # Little endian byte order:
439
+ @us = 'S<*' # Unsigned short (2 bytes)
440
+ @ss = 's<*' # Signed short (2 bytes)
441
+ @ul = 'L<*' # Unsigned long (4 bytes)
442
+ @sl = 'l<*' # Signed long (4 bytes)
443
+ @fs = 'e*' # Floating point single (4 bytes)
444
+ @fd = 'E*' # Floating point double ( 8 bytes)
445
+ else
446
+ # Network (big endian) byte order:
447
+ @us = 'S>*'
448
+ @ss = 's>*'
449
+ @ul = 'L>*'
450
+ @sl = 'l>'
451
+ @fs = 'g*'
452
+ @fd = 'G*'
453
+ end
454
+ # Format strings that are not dependent on endianness:
455
+ @by = 'C*' # Unsigned char (1 byte)
456
+ @str = 'a*'
457
+ @hex = 'H*' # (this may be dependent on endianness(?))
458
+ end
459
+
460
+ end
461
+ end