dicom 0.9.7 → 0.9.8

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.
@@ -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