dicom 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dicom/d_read.rb CHANGED
@@ -36,8 +36,9 @@ module DICOM
36
36
  #
37
37
  # === Options
38
38
  #
39
- # * <tt>:syntax</tt> -- String. If specified, the decoding of the DICOM string will be forced to use this transfer syntax.
40
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.
41
42
  #
42
43
  def initialize(obj, string=nil, options={})
43
44
  # Set the DICOM object as an instance variable:
@@ -69,8 +70,8 @@ module DICOM
69
70
  end
70
71
  # Create a Stream instance to handle the decoding of content from this binary string:
71
72
  @stream = Stream.new(@str, @file_endian)
72
- # Do not check for header information when supplied a (network) binary string:
73
- unless options[:bin]
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]
74
75
  # Read and verify the DICOM header:
75
76
  header = check_header
76
77
  # If the file didnt have the expected header, we will attempt to read
@@ -92,8 +93,8 @@ module DICOM
92
93
  data_element = process_data_element
93
94
  rescue Exception => msg
94
95
  # The parse algorithm crashed. Set data_element to false to break the loop and toggle the success boolean to indicate failure.
95
- @msg << msg
96
- @msg << "Error: Failed to parse Data Elements. This was probably an invalid/corrupt DICOM file."
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."]
97
98
  @success = false
98
99
  data_element = false
99
100
  end
@@ -123,7 +124,7 @@ module DICOM
123
124
  @header_length += 132
124
125
  if identifier != "DICM" then
125
126
  # Header signature is not valid (we will still try to read it is a DICOM file though):
126
- @msg << "Warning: The specified file does not contain the official DICOM header. Will try to read the file anyway, as some sources are known to skip this header."
127
+ @msg << [:warn, "This file does not contain the expected DICOM header. Will try to parse the file anyway (assuming a missing header)."]
127
128
  # As the file is not conforming to the DICOM standard, it is possible that it does not contain a
128
129
  # transfer syntax element, and as such, we attempt to choose the most probable encoding values here:
129
130
  @explicit = false
@@ -209,9 +210,9 @@ module DICOM
209
210
  # If length is specified (no delimitation items), load a new DRead instance to read these child elements
210
211
  # and load them into the current sequence. The exception is when we have a pixel data item.
211
212
  if length > 0 and not @enc_image
212
- child_reader = DRead.new(@current_element, bin, :bin => true, :syntax => @transfer_syntax)
213
+ child_reader = DRead.new(@current_element, bin, :bin => true, :no_meta => true, :syntax => @transfer_syntax)
213
214
  @current_parent = @current_parent.parent
214
- @msg << child_reader.msg
215
+ @msg += child_reader.msg unless child_reader.msg.empty?
215
216
  @success = child_reader.success
216
217
  return false unless @success
217
218
  end
@@ -223,7 +224,7 @@ module DICOM
223
224
  # Create an ordinary Data Element:
224
225
  @current_element = Element.new(tag, value, :bin => bin, :name => name, :parent => @current_parent, :vr => vr)
225
226
  # Check that the data stream didnt end abruptly:
226
- raise "Error: The actual length of the binary (#{@current_element.bin.length}) does not match the specified length (#{length}) for Data Element #{@current_element.tag}." if length != @current_element.bin.length
227
+ raise "The actual length of the value (#{@current_element.bin.length}) does not match its specified length (#{length}) for Data Element #{@current_element.tag}" if length != @current_element.bin.length
227
228
  end
228
229
  # Return true to indicate success:
229
230
  return true
@@ -291,7 +292,7 @@ module DICOM
291
292
  length = @stream.decode(bytes, "SL") # (4)
292
293
  end
293
294
  # Check that length is valid (according to the DICOM standard, it must be even):
294
- raise "Error: Encountered a Data Element (#{tag}) with an invalid (odd) value length." if length%2 == 1 and length > 0
295
+ raise "Encountered a Data Element (#{tag}) with an invalid (odd) value length." if length%2 == 1 and length > 0
295
296
  return vr, length
296
297
  end
297
298
 
@@ -338,42 +339,25 @@ module DICOM
338
339
  #
339
340
  # === Parameters
340
341
  #
341
- # * <tt>file</tt> -- A path/file string. The string may point to a local file or a http location.
342
+ # * <tt>file</tt> -- A path/file string.
342
343
  #
343
344
  def open_file(file)
344
- if file.index('http')==0
345
- # Try to open the remote file using open-uri:
346
- @retrials = 0
347
- begin
348
- @file = open(file, 'rb') # binary encoding (ASCII-8BIT)
349
- rescue Exception => e
350
- if @retrials>3
351
- @retrials = 0
352
- raise "Unable to read the file. File does not exist?"
353
- else
354
- puts "Warning: Exception in ruby-dicom when loading a dicom file from: #{file}"
355
- puts "Retrying... #{@retrials}"
356
- @retrials+=1
357
- retry
358
- end
359
- end
360
- elsif File.exist?(file)
361
- # Try to read the file on the local file system:
345
+ if File.exist?(file)
362
346
  if File.readable?(file)
363
347
  if !File.directory?(file)
364
348
  if File.size(file) > 8
365
349
  @file = File.new(file, "rb")
366
350
  else
367
- @msg << "Error! File is too small to contain DICOM information (#{file})."
351
+ @msg << [:error, "This file is too small to contain valid DICOM information: #{file}."]
368
352
  end
369
353
  else
370
- @msg << "Error! File is a directory (#{file})."
354
+ @msg << [:error, "Expected a file, got a directory: #{file}"]
371
355
  end
372
356
  else
373
- @msg << "Error! File exists but I don't have permission to read it (#{file})."
357
+ @msg << [:error, "File exists but I don't have permission to read it: #{file}"]
374
358
  end
375
359
  else
376
- @msg << "Error! The file you have supplied does not exist (#{file})."
360
+ @msg << [:error, "Invalid (non-existing) file: #{file}"]
377
361
  end
378
362
  end
379
363
 
@@ -407,7 +391,10 @@ module DICOM
407
391
  # Creates various instance variables that are used when parsing the DICOM string.
408
392
  #
409
393
  def init_variables
410
- # Array that will holde any messages generated while reading the DICOM file:
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)
411
398
  @msg = Array.new
412
399
  # Presence of the official DICOM signature:
413
400
  @signature = false
@@ -3,7 +3,9 @@ module DICOM
3
3
  # This class contains code for setting up a Service Class Provider (SCP),
4
4
  # which will act as a simple storage node (a DICOM server that receives images).
5
5
  #
6
+
6
7
  class DServer
8
+ include Logging
7
9
 
8
10
  # Runs the server and takes a block for initializing.
9
11
  #
@@ -39,20 +41,18 @@ module DICOM
39
41
  attr_accessor :port
40
42
  # The maximum period the server will wait on an answer from a client before aborting the communication.
41
43
  attr_accessor :timeout
42
- # A boolean which defines if notices/warnings/errors will be printed to the screen (true) or not (false).
43
- attr_accessor :verbose
44
44
 
45
45
  # A hash containing the abstract syntaxes that will be accepted.
46
46
  attr_reader :accepted_abstract_syntaxes
47
47
  # A hash containing the transfer syntaxes that will be accepted.
48
48
  attr_reader :accepted_transfer_syntaxes
49
- # An array containing any error messages recorded.
50
- attr_reader :errors
51
- # An array containing any status messages recorded.
52
- attr_reader :notices
53
49
 
54
50
  # Creates a DServer instance.
55
51
  #
52
+ # === Notes
53
+ #
54
+ # * To customize logging behaviour, refer to the Logging module documentation.
55
+ #
56
56
  # === Parameters
57
57
  #
58
58
  # * <tt>port</tt> -- Fixnum. The network port to be used. Defaults to 104.
@@ -64,7 +64,6 @@ module DICOM
64
64
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
65
65
  # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
66
66
  # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
67
- # * <tt>:verbose</tt> -- Boolean. If set to false, the DServer instance will run silently and not output warnings and error messages to the screen. Defaults to true.
68
67
  #
69
68
  # === Examples
70
69
  #
@@ -84,11 +83,6 @@ module DICOM
84
83
  @max_package_size = options[:max_package_size] || 32768 # 16384
85
84
  @timeout = options[:timeout] || 10 # seconds
86
85
  @min_length = 12 # minimum number of bytes to expect in an incoming transmission
87
- @verbose = options[:verbose]
88
- @verbose = true if @verbose == nil # Default verbosity is 'on'.
89
- # Other instance variables:
90
- @errors = Array.new # errors and warnings are put in this array
91
- @notices = Array.new # information on successful transmissions are put in this array
92
86
  # Variables used for monitoring state of transmission:
93
87
  @connection = nil # TCP connection status
94
88
  @association = nil # DICOM Association status
@@ -212,19 +206,18 @@ module DICOM
212
206
  #
213
207
  def start_scp(path='./received/')
214
208
  if @accepted_abstract_syntaxes.size > 0 and @accepted_transfer_syntaxes.size > 0
215
- add_notice("Starting DICOM SCP server...")
216
- add_notice("*********************************")
209
+ logger.info("Started DICOM SCP server on port #{@port}.")
210
+ logger.info("Waiting for incoming transmissions...\n\n")
217
211
  # Initiate server:
218
212
  @scp = TCPServer.new(@port)
219
213
  # Use a loop to listen for incoming messages:
220
214
  loop do
221
215
  Thread.start(@scp.accept) do |session|
222
216
  # Initialize the network package handler for this session:
223
- link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose, :file_handler => @file_handler)
217
+ link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :file_handler => @file_handler)
224
218
  link.set_session(session)
225
- # Note the time of reception as well as who has contacted us:
226
- add_notice(Time.now.strftime("%Y-%m-%d %H:%M:%S"))
227
- add_notice("Connection established with: #{session.peeraddr[2]} (IP: #{session.peeraddr[3]})")
219
+ # Note who has contacted us:
220
+ logger.info("Connection established with: #{session.peeraddr[2]} (IP: #{session.peeraddr[3]})")
228
221
  # Receive an incoming message:
229
222
  segments = link.receive_multiple_transmissions
230
223
  info = segments.first
@@ -236,28 +229,24 @@ module DICOM
236
229
  link.handle_association_accept(info)
237
230
  if approved > 0
238
231
  if approved == 1
239
- add_notice("Accepted the association request with context: #{LIBRARY.get_syntax_description(info[:pc].first[:abstract_syntax])}")
232
+ logger.info("Accepted the association request with context: #{LIBRARY.get_syntax_description(info[:pc].first[:abstract_syntax])}")
240
233
  else
241
234
  if rejected == 0
242
- add_notice("Accepted all #{approved} proposed contexts in the association request.")
235
+ logger.info("Accepted all #{approved} proposed contexts in the association request.")
243
236
  else
244
- add_notice("Accepted only #{approved} of #{approved+rejected} of the proposed contexts in the association request.")
237
+ logger.warn("Accepted only #{approved} of #{approved+rejected} of the proposed contexts in the association request.")
245
238
  end
246
239
  end
247
240
  # Process the incoming data. This method will also take care of releasing the association:
248
241
  success, messages = link.handle_incoming_data(path)
249
- if success
250
- add_notice(messages) if messages.first
251
- else
252
- # Something has gone wrong:
253
- add_error(messages) if messages.first
254
- end
242
+ # Pass along any messages that has been recorded:
243
+ messages.each { |m| logger.public_send(m.first, m.last) } if messages.first
255
244
  else
256
245
  # No abstract syntaxes in the incoming request were accepted:
257
246
  if rejected == 1
258
- add_notice("Rejected the association request with proposed context: #{LIBRARY.get_syntax_description(info[:pc].first[:abstract_syntax])}")
247
+ logger.warn("Rejected the association request with proposed context: #{LIBRARY.get_syntax_description(info[:pc].first[:abstract_syntax])}")
259
248
  else
260
- add_notice("Rejected all #{rejected} proposed contexts in the association request.")
249
+ logger.warn("Rejected all #{rejected} proposed contexts in the association request.")
261
250
  end
262
251
  # Since the requested abstract syntax was not accepted, the association must be released.
263
252
  link.await_release
@@ -272,7 +261,7 @@ module DICOM
272
261
  end
273
262
  # Terminate the connection:
274
263
  link.stop_session
275
- add_notice("*********************************")
264
+ logger.info("Connection closed.\n\n")
276
265
  end
277
266
  end
278
267
  else
@@ -285,35 +274,6 @@ module DICOM
285
274
  # Following methods are private:
286
275
  private
287
276
 
288
-
289
- # Adds a warning or error message to the instance array holding messages,
290
- # and prints the information to the screen if verbose is set.
291
- #
292
- # === Parameters
293
- #
294
- # * <tt>error</tt> -- A single error message or an array of error messages.
295
- #
296
- def add_error(error)
297
- if @verbose
298
- puts error
299
- end
300
- @errors << error
301
- end
302
-
303
- # Adds a notice (information regarding progress or successful communications) to the instance array,
304
- # and prints the information to the screen if verbose is set.
305
- #
306
- # === Parameters
307
- #
308
- # * <tt>notice</tt> -- A single status message or an array of status messages.
309
- #
310
- def add_notice(notice)
311
- if @verbose
312
- puts notice
313
- end
314
- @notices << notice
315
- end
316
-
317
277
  # Checks if the association request is formally correct, by matching against an exact application context UID.
318
278
  # Returns nil if valid, and an error code if it is not approved.
319
279
  #
@@ -330,7 +290,7 @@ module DICOM
330
290
  def check_association_request(info)
331
291
  unless info[:application_context] == APPLICATION_CONTEXT
332
292
  error = 2 # (application context name not supported)
333
- add_error("Error: The application context in the incoming association request was not recognized: (#{info[:application_context]})")
293
+ logger.error("The application context in the incoming association request was not recognized: (#{info[:application_context]})")
334
294
  else
335
295
  error = nil
336
296
  end
@@ -392,4 +352,4 @@ module DICOM
392
352
  end
393
353
 
394
354
  end
395
- end
355
+ end
data/lib/dicom/d_write.rb CHANGED
@@ -17,9 +17,8 @@ module DICOM
17
17
  # that this is actually achieved. You are encouraged to thouroughly test your files for compatibility after creation.
18
18
  #
19
19
  class DWrite
20
+ include Logging
20
21
 
21
- # An array which records any status messages that are generated while encoding/writing the DICOM string.
22
- attr_reader :msg
23
22
  # An array of partial DICOM strings.
24
23
  attr_reader :segments
25
24
  # A boolean which reports whether the DICOM string was encoded/written successfully (true) or not (false).
@@ -48,8 +47,6 @@ module DICOM
48
47
  @file_name = file_name
49
48
  # As default, signature will be written and meta header added:
50
49
  @signature = (options[:signature] == false ? false : true)
51
- # Array for storing error/warning messages:
52
- @msg = Array.new
53
50
  end
54
51
 
55
52
  # Handles the encoding of DICOM information to string as well as writing it to file.
@@ -329,7 +326,7 @@ module DICOM
329
326
  @file = File.new(file, "wb")
330
327
  else
331
328
  # Existing file is not writable:
332
- @msg << "Error! The program does not have permission or resources to create the file you specified: (#{file})"
329
+ logger.error("The program does not have permission or resources to create this file: #{file}")
333
330
  end
334
331
  else
335
332
  # File does not exist.
@@ -370,7 +367,7 @@ module DICOM
370
367
  # The information from the Transfer syntax element (if present), needs to be processed:
371
368
  valid_syntax, @rest_explicit, @rest_endian = LIBRARY.process_transfer_syntax(@transfer_syntax)
372
369
  unless valid_syntax
373
- @msg << "Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."
370
+ logger.warn("Invalid (unknown) transfer syntax! Will complete encoding the file, but an investigation of the result is recommended.")
374
371
  end
375
372
  # We only plan to run this method once:
376
373
  @switched = true
data/lib/dicom/element.rb CHANGED
@@ -44,7 +44,7 @@ module DICOM
44
44
  def initialize(tag, value, options={})
45
45
  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?
46
46
  # Set instance variables:
47
- @tag = tag
47
+ @tag = tag.upcase
48
48
  # We may beed to retrieve name and vr from the library:
49
49
  if options[:name] and options[:vr]
50
50
  @name = options[:name]
@@ -47,7 +47,7 @@ module DICOM
47
47
  full_path = path_prefix + local_path + extension
48
48
  # Save the DICOM object to disk:
49
49
  obj.write(full_path, :transfer_syntax => transfer_syntax)
50
- message = "DICOM file saved to: #{full_path}"
50
+ message = [:info, "DICOM file saved to: #{full_path}"]
51
51
  return message
52
52
  end
53
53
 
@@ -74,8 +74,13 @@ module DICOM
74
74
  # Process each DICOM object:
75
75
  objects.each_index do |i|
76
76
  if objects[i].length > 8
77
- # Parse the received data stream and load it as a DICOM object:
78
- obj = DObject.new(objects[i], :bin => true, :syntax => transfer_syntaxes[i])
77
+ # Temporarily increase the log threshold to suppress messages from the DObject class:
78
+ server_level = DICOM.logger.level
79
+ DICOM.logger.level = Logger::FATAL
80
+ # Parse the received data string and load it to a DICOM object:
81
+ obj = DObject.parse(objects[i], :no_meta => true, :syntax => transfer_syntaxes[i])
82
+ # Reset the logg threshold:
83
+ DICOM.logger.level = server_level
79
84
  if obj.read_success
80
85
  begin
81
86
  message = self.save_file(path, obj, transfer_syntaxes[i])
@@ -83,28 +88,28 @@ module DICOM
83
88
  rescue
84
89
  handle_fail += 1
85
90
  all_success = false
86
- messages << "Error: Saving file failed!"
91
+ messages << [:error, "Saving file failed!"]
87
92
  end
88
93
  else
89
94
  parse_fail += 1
90
95
  all_success = false
91
- messages << "Error: Invalid DICOM data encountered: The DICOM data string could not be parsed successfully."
96
+ messages << [:error, "Invalid DICOM data encountered: The received string was not parsed successfully."]
92
97
  end
93
98
  else
94
99
  too_short += 1
95
100
  all_success = false
96
- messages << "Error: Invalid data encountered: The data was too small to be a valid DICOM file."
101
+ messages << [:error, "Invalid data encountered: The received string was too small to contain any DICOM data."]
97
102
  end
98
103
  end
99
104
  # Create a summary status message, when multiple files have been received:
100
105
  if total > 1
101
106
  if successful == total
102
- messages << "All #{total} DICOM files received successfully."
107
+ messages << [:info, "All #{total} DICOM files received successfully."]
103
108
  else
104
109
  if successful == 0
105
- messages << "All #{total} received DICOM files failed!"
110
+ messages << [:warn, "All #{total} received DICOM files failed!"]
106
111
  else
107
- messages << "Only #{successful} of #{total} DICOM files received successfully!"
112
+ messages << [:warn, "Only #{successful} of #{total} DICOM files received successfully!"]
108
113
  end
109
114
  end
110
115
  else
@@ -11,6 +11,7 @@ module DICOM
11
11
  class ImageItem < Parent
12
12
 
13
13
  include ImageProcessor
14
+ include Logging
14
15
 
15
16
  # Checks if colored pixel data is present.
16
17
  # Returns true if it is, false if not.
@@ -169,7 +170,7 @@ module DICOM
169
170
  strings.each {|string| pixel_frames << decode_rle(num_cols, num_rows, string)}
170
171
  else
171
172
  images = decompress(strings) || Array.new
172
- add_msg("Warning: Decompressing pixel values has failed (unsupported transfer syntax: '#{transfer_syntax}')") unless images
173
+ logger.warn("Decompressing pixel values has failed (unsupported transfer syntax: '#{transfer_syntax}')") unless images
173
174
  end
174
175
  else
175
176
  # Uncompressed: Decode to numbers.
@@ -184,7 +185,7 @@ module DICOM
184
185
  if pixels
185
186
  images << read_image(pixels, num_cols, num_rows, options)
186
187
  else
187
- add_msg("Warning: Processing pixel values for this particular color mode failed, unable to construct image(s).")
188
+ logger.warn("Processing pixel values for this particular color mode failed, unable to construct image(s).")
188
189
  end
189
190
  end
190
191
  end
@@ -210,7 +211,7 @@ module DICOM
210
211
  # Write the binary data to the Pixel Data Element:
211
212
  write_pixels(bin)
212
213
  else
213
- add_msg("Notice: The specified file (#{file}) is empty. Nothing to transfer.")
214
+ logger.info("The specified file (#{file}) is empty. Nothing to transfer.")
214
215
  end
215
216
  end
216
217
 
@@ -362,10 +363,10 @@ module DICOM
362
363
  # Remap the image from pixel values to presentation values if the user has requested this:
363
364
  pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]) if options[:remap] or options[:level]
364
365
  else
365
- add_msg("Warning: Decompressing the Pixel Data failed. Pixel values can not be extracted.")
366
+ logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
366
367
  end
367
368
  else
368
- add_msg("The DICOM object contains colored pixel data. Retrieval of colored pixels is not supported by this method yet.")
369
+ logger.warn("The DICOM object contains colored pixel data. Retrieval of colored pixels is not supported by this method yet.")
369
370
  pixels = false
370
371
  end
371
372
  end
@@ -419,7 +420,7 @@ module DICOM
419
420
  end
420
421
  end
421
422
  else
422
- add_msg("Warning: Decompressing the Pixel Data failed. Pixel values can not be extracted.")
423
+ logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
423
424
  end
424
425
  end
425
426
  return pixels