dicom 0.7 → 0.8

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