dicom 0.9.1 → 0.9.2

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