dicom 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,121 +1,20 @@
1
1
  module DICOM
2
2
 
3
- # The DRead class parses the DICOM data from a binary string.
4
- #
5
- # The source of this binary string is typically either a DICOM file or a DICOM network transmission.
6
- #
7
- # === Notes
8
- #
9
- # In addition to reading files that are compliant to DICOM 3 Part 10, the philosophy of the
10
- # Ruby DICOM library is to feature maximum compatibility, and as such it will also
11
- # successfully read many types of 'DICOM' files that deviate in some way from the standard.
12
- #
13
- class DRead
3
+ class Parent
14
4
 
15
- # A boolean which reports the explicitness of the DICOM string, true if explicit and false if implicit.
16
- attr_reader :explicit
17
- # A boolean which reports the endianness of the post-meta group part of the DICOM string (true for big endian, false for little endian).
18
- attr_reader :file_endian
19
- # An array which records any status messages that are generated while parsing the DICOM string.
20
- attr_reader :msg
21
- # A DObject instance which the parsed data elements will be connected to.
22
- attr_reader :dcm
23
- # A boolean which records whether the DICOM string contained the proper DICOM header signature of 128 bytes + 'DICM'.
24
- attr_reader :signature
25
- # A boolean which reports whether the DICOM string was parsed successfully (true) or not (false).
26
- attr_reader :success
27
-
28
- # Creates a DRead instance.
29
- # Parses the DICOM string, builds data element objects and connects these with the DObject instance.
30
- #
31
- # === Parameters
32
- #
33
- # * <tt>dcm</tt> -- A DObject instance which the parsed data elements will be connected to.
34
- # * <tt>string</tt> -- A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
35
- # * <tt>options</tt> -- A hash of parameters.
36
- #
37
- # === Options
38
- #
39
- # * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
40
- # * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
41
- # * <tt>:syntax</tt> -- String. If specified, the decoding of the DICOM string will be forced to use this transfer syntax.
42
- #
43
- def initialize(dcm, string=nil, options={})
44
- # Set the DICOM object as an instance variable:
45
- @dcm = dcm
46
- # If a transfer syntax has been specified as an option for a DICOM object, make sure that it makes it into the object:
47
- if options[:syntax]
48
- @transfer_syntax = options[:syntax]
49
- dcm.add(Element.new("0002,0010", options[:syntax])) if dcm.is_a?(DObject)
50
- end
51
- # Initiate the variables that are used during file reading:
52
- init_variables
53
- # Are we going to read from a file, or read from a binary string?
54
- if options[:bin]
55
- # Read from the provided binary string:
56
- @str = string
57
- else
58
- # Read from file:
59
- open_file(string)
60
- # Read the initial header of the file:
61
- if @file == nil
62
- # File is not readable, so we return:
63
- @success = false
64
- return
65
- else
66
- # Extract the content of the file to a binary string:
67
- @str = @file.read
68
- @file.close
69
- end
70
- end
71
- # Create a Stream instance to handle the decoding of content from this binary string:
72
- @stream = Stream.new(@str, @file_endian)
73
- # Do not check for header information if we've been told there is none (typically for (network) binary strings):
74
- unless options[:no_meta]
75
- # Read and verify the DICOM header:
76
- header = check_header
77
- # If the file didnt have the expected header, we will attempt to read
78
- # data elements from the very start of the file:
79
- if header == false
80
- @stream.skip(-132)
81
- elsif header == nil
82
- # Not a valid DICOM file, return:
83
- @success = false
84
- return
85
- end
86
- end
87
- # Run a loop which parses Data Elements, one by one, until the end of the data string is reached:
88
- data_element = true
89
- while data_element do
90
- # Using a rescue clause since processing Data Elements can cause errors when parsing an invalid DICOM string.
91
- begin
92
- # Extracting Data element information (nil is returned if end of file is encountered in a normal way).
93
- data_element = process_data_element
94
- rescue Exception => msg
95
- # The parse algorithm crashed. Set data_element to false to break the loop and toggle the success boolean to indicate failure.
96
- @msg << [:error, msg]
97
- @msg << [:warn, "Parsing a Data Element has failed. This was probably caused by an invalidly encoded (or corrupted) DICOM file."]
98
- @success = false
99
- data_element = false
100
- end
101
- end
102
- end
103
-
104
-
105
- # Following methods are private:
106
5
  private
107
6
 
108
7
 
109
8
  # Checks for the official DICOM header signature.
110
- # Returns true if the proper signature is present, false if it is not present,
111
- # and nil if thestring was shorter then the length of the DICOM signature.
9
+ #
10
+ # @return [Boolean] true if the proper signature is present, false if not, and nil if the string was shorter then the length of the DICOM signature
112
11
  #
113
12
  def check_header
114
13
  # According to the official DICOM standard, a DICOM file shall contain 128 consequtive (zero) bytes,
115
14
  # followed by 4 bytes that spell the string 'DICM'. Apparently, some providers seems to skip this in their DICOM files.
116
- # Check that the file is long enough to contain a valid header:
15
+ # Check that the string is long enough to contain a valid header:
117
16
  if @str.length < 132
118
- # This does not seem to be a valid DICOM file and so we return.
17
+ # This does not seem to be a valid DICOM string and so we return.
119
18
  return nil
120
19
  else
121
20
  @stream.skip(128)
@@ -123,9 +22,9 @@ module DICOM
123
22
  identifier = @stream.decode(4, "STR")
124
23
  @header_length += 132
125
24
  if identifier != "DICM" then
126
- # Header signature is not valid (we will still try to read it is a DICOM file though):
127
- @msg << [:warn, "This file does not contain the expected DICOM header. Will try to parse the file anyway (assuming a missing header)."]
128
- # As the file is not conforming to the DICOM standard, it is possible that it does not contain a
25
+ # Header signature is not valid (we will still try to parse it is a DICOM string though):
26
+ logger.warn("This string does not contain the expected DICOM header. Will try to parse the string anyway (assuming a missing header).")
27
+ # As the string is not conforming to the DICOM standard, it is possible that it does not contain a
129
28
  # transfer syntax element, and as such, we attempt to choose the most probable encoding values here:
130
29
  @explicit = false
131
30
  return false
@@ -137,13 +36,13 @@ module DICOM
137
36
  end
138
37
  end
139
38
 
140
- # Handles the process of reading a data element from the DICOM string, and creating an element object from the parsed data.
141
- # Returns nil if end of file has been reached (in an expected way), false if the element parse failed, and true if an element was parsed successfully.
39
+ # Handles the process of reading a data element from the DICOM string, and
40
+ # creating an element object from the parsed data.
142
41
  #
143
- #--
144
- # FIXME: This method has grown a bit messy and isn't very easy to follow. It would be nice if it could be cleaned up somewhat.
42
+ # @return [Boolean] nil if end of string has been reached (in an expected way), false if the element parse failed, and true if an element was parsed successfully
145
43
  #
146
44
  def process_data_element
45
+ # FIXME: This method has grown a bit messy and isn't very pleasant to read. Cleanup possible?
147
46
  # STEP 1:
148
47
  # Attempt to read data element tag:
149
48
  tag = read_tag
@@ -151,8 +50,8 @@ module DICOM
151
50
  return nil unless tag
152
51
  # STEP 2:
153
52
  # Access library to retrieve the data element name and VR from the tag we just read:
154
- # (Note: VR will be overwritten in the next step if the DICOM file contains VR (explicit encoding))
155
- name, vr = LIBRARY.get_name_vr(tag)
53
+ # (Note: VR will be overwritten in the next step if the DICOM string contains VR (explicit encoding))
54
+ name, vr = LIBRARY.name_and_vr(tag)
156
55
  # STEP 3:
157
56
  # Read VR (if it exists) and the length value:
158
57
  vr, length = read_vr_length(vr,tag)
@@ -210,11 +109,9 @@ module DICOM
210
109
  # If length is specified (no delimitation items), load a new DRead instance to read these child elements
211
110
  # and load them into the current sequence. The exception is when we have a pixel data item.
212
111
  if length > 0 and not @enc_image
213
- child_reader = DRead.new(@current_element, bin, :bin => true, :no_meta => true, :syntax => @transfer_syntax)
112
+ @current_element.parse(bin, @transfer_syntax)
214
113
  @current_parent = @current_parent.parent
215
- @msg += child_reader.msg unless child_reader.msg.empty?
216
- @success = child_reader.success
217
- return false unless @success
114
+ return false unless @read_success
218
115
  end
219
116
  elsif DELIMITER_TAGS.include?(tag)
220
117
  # We do not create an element for the delimiter items.
@@ -230,15 +127,103 @@ module DICOM
230
127
  return true
231
128
  end
232
129
 
233
- # Reads and returns the data element's tag (the 4 first bytes of a data element).
234
- # Returns nil if no tag could be read (end of string).
130
+ # Builds a DICOM object by parsing an encoded DICOM string.
131
+ #
132
+ # @param [String] string a binary DICOM string to be parsed
133
+ # @param [Boolean] signature if true (default), the parsing algorithm will look for the DICOM header signature
134
+ # @param [Hash] options the options to use for parsing the DICOM string
135
+ # @option options [String] :syntax if a syntax string is specified, the parsing algorithm is forced to use this transfer syntax when decoding the string
136
+ #
137
+ def read(string, signature=true, options={})
138
+ # (Re)Set variables:
139
+ @str = string
140
+ # Presence of the official DICOM signature:
141
+ @signature = false
142
+ # Default explicitness of start of DICOM string:
143
+ @explicit = true
144
+ # Default endianness of start of DICOM string is little endian:
145
+ @str_endian = false
146
+ # A switch of endianness may occur after the initial meta group, an this needs to be monitored:
147
+ @switched_endian = false
148
+ # Explicitness of the remaining groups after the initial 0002 group:
149
+ @rest_explicit = false
150
+ # Endianness of the remaining groups after the first group:
151
+ @rest_endian = false
152
+ # When the string switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
153
+ @switched = false
154
+ # Keeping track of the data element parent status while parsing the DICOM string:
155
+ @current_parent = self
156
+ # Keeping track of what is the current data element:
157
+ @current_element = self
158
+ # Items contained under the pixel data element may contain data directly, so we need a variable to keep track of this:
159
+ @enc_image = false
160
+ # Assume header size is zero bytes until otherwise is determined:
161
+ @header_length = 0
162
+ # Assume string will be read successfully and toggle it later if we experience otherwise:
163
+ @read_success = true
164
+ # Our encoding instance:
165
+ @stream = Stream.new(@str, @str_endian)
166
+ # If a transfer syntax has been specified as an option for a DICOM object,
167
+ # make sure that it makes it into the object:
168
+ if options[:syntax]
169
+ @transfer_syntax = options[:syntax]
170
+ Element.new("0002,0010", options[:syntax], :parent => self) if self.is_a?(DObject)
171
+ end
172
+ # Check for header information if indicated:
173
+ if signature
174
+ # Read and verify the DICOM header:
175
+ header = check_header
176
+ # If the string is without the expected header, we will attempt
177
+ # to read data elements from the very start of the string:
178
+ if header == false
179
+ @stream.skip(-132)
180
+ elsif header.nil?
181
+ # Not a valid DICOM string, return:
182
+ @read_success = false
183
+ return
184
+ end
185
+ end
186
+ # Run a loop which parses Data Elements, one by one, until the end of the data string is reached:
187
+ data_element = true
188
+ while data_element do
189
+ # Using a rescue clause since processing Data Elements can cause errors when parsing an invalid DICOM string.
190
+ begin
191
+ # Extracting Data element information (nil is returned if end of the string is encountered in a normal way).
192
+ data_element = process_data_element
193
+ rescue Exception => msg
194
+ # The parse algorithm crashed. Set data_element as false to break
195
+ # the loop and toggle the success boolean to indicate failure.
196
+ @read_success = false
197
+ data_element = false
198
+ # Output the raised message as a warning:
199
+ logger.warn(msg.to_s)
200
+ # Ouput the backtrace as debug information:
201
+ logger.debug(msg.backtrace)
202
+ # Explain the failure as an error:
203
+ logger.error("Parsing a Data Element has failed. This is likely caused by an invalid DICOM encoding.")
204
+ end
205
+ end
206
+ end
207
+
208
+ # Reads the data element's binary value string (varying length).
209
+ #
210
+ # @param [Integer] length the length of the binary string to be extracted
211
+ # @return [String] the element value
212
+ #
213
+ def read_bin(length)
214
+ return @stream.extract(length)
215
+ end
216
+
217
+ # Reads the data element's tag (the 4 first bytes of a data element).
218
+ #
219
+ # @return [String, NilClass] the element tag, or nil (if end of string reached)
235
220
  #
236
221
  def read_tag
237
222
  tag = @stream.decode_tag
238
223
  if tag
239
224
  # When we shift from group 0002 to another group we need to update our endian/explicitness variables:
240
225
  if tag.group != META_GROUP and @switched == false
241
- switch_syntax
226
+ switch_syntax_on_read
242
227
  # We may need to read our tag again if endian has switched (in which case it has been misread):
243
228
  if @switched_endian
244
229
  @stream.skip(-4)
@@ -249,14 +234,40 @@ module DICOM
249
234
  return tag
250
235
  end
251
236
 
252
- # Reads the data element's value representation (2 bytes), as well as the data element's length (varying length: 2-6 bytes).
253
- # The decoding scheme to be applied depends on explicitness, data element type and vr.
254
- # Returns vr and length.
237
+ # Decodes the data element's value (varying length).
255
238
  #
256
- # === Parameters
239
+ # * Data elements which have multiple numbers as value, will have these numbers joined to a string, separated by the \ character.
240
+ # * For some value representations (OW, OB, OF, UN), a value is not processed, and nil is returned.
257
241
  #
258
- # * <tt>vr</tt> -- String. The value representation that was retrieved from the dictionary for the tag of this data element.
259
- # * <tt>tag</tt> -- String. The tag of this data element.
242
+ # This means that for data like pixel data, compressed data, unknown data, a value is not
243
+ # available in the data element, and must be processed from the data element's binary variable.
244
+ #
245
+ # @param [String] vr the value representation of the data element which the value to be decoded belongs to
246
+ # @param [Integer] length the length of the binary string to be extracted
247
+ # @return [String, NilClass] the data element value
248
+ #
249
+ def read_value(vr, length)
250
+ unless vr == "OW" or vr == "OB" or vr == "OF" or vr == "UN"
251
+ # Since the binary string has already been extracted for this data element, we must first "rewind":
252
+ @stream.skip(-length)
253
+ # Decode data:
254
+ value = @stream.decode(length, vr)
255
+ # If the returned value is an array of multiple values, we will join these values to a string with the separator "\":
256
+ value = value.join("\\") if value.is_a?(Array)
257
+ else
258
+ # No decoded value:
259
+ value = nil
260
+ end
261
+ return value
262
+ end
263
+
264
+ # Reads the data element's value representation (2 bytes), as well as the
265
+ # data element's length (varying length: 2-6 bytes). The decoding scheme
266
+ # to be applied depends on explicitness, data element type and vr.
267
+ #
268
+ # @param [String] vr the value representation that was retrieved from the dictionary for the tag of this data element
269
+ # @param [String] tag the tag of this data element
270
+ # @return [Array<String, Integer>] the value representation and length of the element
260
271
  #
261
272
  def read_vr_length(vr, tag)
262
273
  # Structure will differ, dependent on whether we have explicit or implicit encoding:
@@ -296,131 +307,26 @@ module DICOM
296
307
  return vr, length
297
308
  end
298
309
 
299
- # Reads and returns the data element's binary value string (varying length).
300
- #
301
- # === Parameters
302
- #
303
- # * <tt>length</tt> -- Fixnum. The length of the binary string that will be extracted.
304
- #
305
- def read_bin(length)
306
- return @stream.extract(length)
307
- end
308
-
309
- # Decodes and returns the data element's value (varying length).
310
- #
311
- # === Notes
310
+ # Changes encoding variables as the parsing proceeds past the initial meta
311
+ # group part (0002,xxxx) of the DICOM string.
312
312
  #
313
- # * Data elements which have multiple numbers as value, will have these numbers joined to a string, separated by the \ character.
314
- # * For some value representations (OW, OB, OF, UN), a value is not processed, and nil is returned.
315
- # This means that for data like pixel data, compressed data, unknown data, a value is not available in the data element,
316
- # and must be processed from the data element's binary variable.
317
- #
318
- # === Parameters
319
- #
320
- # * <tt>vr</tt> -- String. The value representation of the data element which the value to be decoded belongs to.
321
- # * <tt>length</tt> -- Fixnum. The length of the binary string that will be extracted.
322
- #
323
- def read_value(vr, length)
324
- unless vr == "OW" or vr == "OB" or vr == "OF" or vr == "UN"
325
- # Since the binary string has already been extracted for this data element, we must first "rewind":
326
- @stream.skip(-length)
327
- # Decode data:
328
- value = @stream.decode(length, vr)
329
- # If the returned value is an array of multiple values, we will join these values to a string with the separator "\":
330
- value = value.join("\\") if value.is_a?(Array)
331
- else
332
- # No decoded value:
333
- value = nil
334
- end
335
- return value
336
- end
337
-
338
- # Tests if a file is readable, and if so, opens it.
339
- #
340
- # === Parameters
341
- #
342
- # * <tt>file</tt> -- A path/file string.
343
- #
344
- def open_file(file)
345
- if File.exist?(file)
346
- if File.readable?(file)
347
- if !File.directory?(file)
348
- if File.size(file) > 8
349
- @file = File.new(file, "rb")
350
- else
351
- @msg << [:error, "This file is too small to contain valid DICOM information: #{file}."]
352
- end
353
- else
354
- @msg << [:error, "Expected a file, got a directory: #{file}"]
355
- end
356
- else
357
- @msg << [:error, "File exists but I don't have permission to read it: #{file}"]
358
- end
359
- else
360
- @msg << [:error, "Invalid (non-existing) file: #{file}"]
361
- end
362
- end
363
-
364
- # Changes encoding variables as the file reading proceeds past the initial meta group part (0002,xxxx) of the DICOM file.
365
- #
366
- def switch_syntax
313
+ def switch_syntax_on_read
367
314
  # Get the transfer syntax string, unless it has already been provided by keyword:
368
- unless @transfer_syntax
369
- ts_element = @dcm["0002,0010"]
370
- if ts_element
371
- @transfer_syntax = ts_element.value
372
- else
373
- @transfer_syntax = IMPLICIT_LITTLE_ENDIAN
374
- end
375
- end
315
+ @transfer_syntax = (self["0002,0010"] ? self["0002,0010"].value : IMPLICIT_LITTLE_ENDIAN) unless @transfer_syntax
376
316
  # Query the library with our particular transfer syntax string:
377
- valid_syntax, @rest_explicit, @rest_endian = LIBRARY.process_transfer_syntax(@transfer_syntax)
378
- unless valid_syntax
379
- @msg << "Warning: Invalid/unknown transfer syntax! Will try reading the file, but errors may occur."
380
- end
381
- # We only plan to run this method once:
317
+ ts = LIBRARY.uid(@transfer_syntax)
318
+ logger.warn("Invalid/unknown transfer syntax: #{@transfer_syntax} Will try parsing the string, but errors may occur.") unless ts && ts.transfer_syntax?
319
+ @rest_explicit = ts ? ts.explicit? : true
320
+ @rest_endian = ts ? ts.big_endian? : false
321
+ # Make sure we only run this method once:
382
322
  @switched = true
383
323
  # Update endian, explicitness and unpack variables:
384
- @switched_endian = true if @rest_endian != @file_endian
385
- @file_endian = @rest_endian
324
+ @switched_endian = true if @rest_endian != @str_endian
325
+ @str_endian = @rest_endian
386
326
  @stream.endian = @rest_endian
387
327
  @explicit = @rest_explicit
388
328
  end
389
329
 
390
-
391
- # Creates various instance variables that are used when parsing the DICOM string.
392
- #
393
- def init_variables
394
- # Array for storing any messages that is generated while reading the DICOM file.
395
- # The messages shall be of the format: [:type, "message"]
396
- # (Because of the possibility of multi-pass file reading, the DRead instance does not access
397
- # the Logging module directly; it lets the DObject instance pass along the messages instead)
398
- @msg = Array.new
399
- # Presence of the official DICOM signature:
400
- @signature = false
401
- # Default explicitness of start of DICOM file:
402
- @explicit = true
403
- # Default endianness of start of DICOM files is little endian:
404
- @file_endian = false
405
- # A switch of endianness may occur after the initial meta group, an this needs to be monitored:
406
- @switched_endian = false
407
- # Explicitness of the remaining groups after the initial 0002 group:
408
- @rest_explicit = false
409
- # Endianness of the remaining groups after the first group:
410
- @rest_endian = false
411
- # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
412
- @switched = false
413
- # Keeping track of the data element parent status while parsing the DICOM string:
414
- @current_parent = @dcm
415
- # Keeping track of what is the current data element:
416
- @current_element = @dcm
417
- # Items contained under the pixel data element may contain data directly, so we need a variable to keep track of this:
418
- @enc_image = false
419
- # Assume header size is zero bytes until otherwise is determined:
420
- @header_length = 0
421
- # Assume file will be read successfully and toggle it later if we experience otherwise:
422
- @success = true
423
- end
424
-
425
330
  end
331
+
426
332
  end