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/CHANGELOG.rdoc CHANGED
@@ -1,9 +1,26 @@
1
+ = 0.9.2
2
+
3
+ === (Not released yet)
4
+
5
+ * Enabled the use of lower case tag letters in methods which previously required the use of upper case letters.
6
+ * Added new DObject class methods to offload DObject#new:
7
+ * DObject#read is the new preferred method for reading a DICOM file.
8
+ * DObject#parse is the new preferred method for parsing an encoded DICOM string.
9
+ * The experimental feature of retrieving a DICOM file through http was moved to DObject#get.
10
+ * Calling DObject with a string argument was deprecated.
11
+ * Introduced proper logging capabilities which replaced the simple message printouts to STDOUT:
12
+ * Based on the Logger class of the Ruby Standard Library.
13
+ * Automatically integrates with the Rails logger in a Rails application.
14
+ * Supports information levels, logging to file, and more as available in the Ruby Logger.
15
+
16
+
1
17
  = 0.9.1
2
18
 
3
19
  === 27th May, 2011
4
20
 
5
21
  * Fixed a regression in 0.9 where ruby-dicom would cause a Rails application to crash.
6
22
 
23
+
7
24
  = 0.9
8
25
 
9
26
  === 17th May, 2011
data/README.rdoc CHANGED
@@ -23,7 +23,7 @@ communication modalities like querying, moving, sending and receiving files.
23
23
  === Read, modify and write
24
24
 
25
25
  # Read file:
26
- obj = DObject.new("some_file.dcm")
26
+ obj = DObject.read("some_file.dcm")
27
27
  # Extract the Patient's Name value:
28
28
  obj.patients_name.value
29
29
  # Add or modify the Patient's Name element:
@@ -72,6 +72,18 @@ communication modalities like querying, moving, sending and receiving files.
72
72
  s = DServer.new(104, :host_ae => "MY_DICOM_SERVER")
73
73
  s.start_scp("C:/temp/")
74
74
 
75
+ === Log settings
76
+
77
+ # Change the log level so that only error messages are displayed:
78
+ DICOM.logger.level = Logger::ERROR
79
+ # Setting up a simple file log:
80
+ l = Logger.new('my_logfile.log')
81
+ DICOM.logger = l
82
+ # Create a logger which ages logfile daily/monthly:
83
+ DICOM.logger = Logger.new('foo.log', 'daily')
84
+ DICOM.logger = Logger.new('foo.log', 'monthly')
85
+
86
+
75
87
  === IRB Tip
76
88
 
77
89
  When working with Ruby DICOM in irb, you may be annoyed with all the information
@@ -120,7 +132,8 @@ Please don't hesitate to email me if you have any feedback related to this proje
120
132
 
121
133
  * {Christoffer Lervåg}[https://github.com/dicom]
122
134
  * {John Axel Eriksson}[https://github.com/johnae]
135
+ * {Kamil Bujniewicz}[https://github.com/icdark]
123
136
  * {Donnie Millar}[https://github.com/dmillar]
124
- * Björn Albers
137
+ * {Björn Albers}[https://github.com/bjoernalbers]
125
138
  * {Lars Benner}[https://github.com/Maturin]
126
- * {Steven Bedrick}[https://github.com/stevenbedrick]
139
+ * {Steven Bedrick}[https://github.com/stevenbedrick]
data/lib/dicom.rb CHANGED
@@ -11,6 +11,8 @@
11
11
  # The rest of the classes visible in the documentation generated by RDoc is in principle
12
12
  # 'private' classes, which are mainly of interest to developers.
13
13
 
14
+ # Logging:
15
+ require 'dicom/logging'
14
16
  # Core library:
15
17
  # Super classes/modules:
16
18
  require 'dicom/image_processor'
@@ -42,4 +44,4 @@ require 'dicom/image_processor_mini_magick'
42
44
  require 'dicom/image_processor_r_magick'
43
45
 
44
46
  # Extensions (non-core functionality):
45
- require 'dicom/anonymizer'
47
+ require 'dicom/anonymizer'
@@ -1,4 +1,3 @@
1
-
2
1
  module DICOM
3
2
 
4
3
  # This is a convenience class for handling anonymization of DICOM files.
@@ -10,6 +9,7 @@ module DICOM
10
9
  # (Clinical Trials De-identification Profiles, DICOM Standards Committee, Working Group 18)
11
10
  #
12
11
  class Anonymizer
12
+ include Logging
13
13
 
14
14
  # A boolean that if set as true will cause all anonymized tags to be blank instead of get some generic value.
15
15
  attr_accessor :blank
@@ -26,23 +26,17 @@ module DICOM
26
26
 
27
27
  # Creates an Anonymizer instance.
28
28
  #
29
- # === Parameters
30
- #
31
- # * <tt>options</tt> -- A hash of parameters.
32
- #
33
- # === Options
29
+ # === Notes
34
30
  #
35
- # * <tt>:verbose</tt> -- Boolean. If set to false, the Anonymizer instance will run silently and not output status updates to the screen. Defaults to true.
31
+ # * To customize logging behaviour, refer to the Logging module documentation.
36
32
  #
37
33
  # === Examples
38
34
  #
35
+ # # Create an Anonymizer instance and restrict the log output:
39
36
  # a = Anonymizer.new
40
- # # Create an instance in non-verbose mode:
41
- # a = Anonymizer.new(:verbose => false)
37
+ # a.logger.level = Logger::ERROR
42
38
  #
43
- def initialize(options={})
44
- # Default verbosity is true if verbosity hasn't been specified (nil):
45
- @verbose = (options[:verbose] == false ? false : true)
39
+ def initialize
46
40
  # Default value of accessors:
47
41
  @blank = false
48
42
  @enumeration = false
@@ -120,7 +114,7 @@ module DICOM
120
114
  if pos
121
115
  return @enumerations[pos]
122
116
  else
123
- add_msg("The specified tag is not found in the list of tags to be anonymized.")
117
+ logger.warn("The specified tag (#{tag}) was not found in the list of tags to be anonymized.")
124
118
  return nil
125
119
  end
126
120
  end
@@ -133,47 +127,45 @@ module DICOM
133
127
  #
134
128
  # * Only top level data elements are anonymized!
135
129
  #
136
- # === Parameters
137
- #
138
- # * <tt>verbose</tt> -- Boolean. If set as true, verbose behaviour will be set for the DObject instances that are anonymized. Defaults to false.
139
- #
140
130
  #--
141
131
  # FIXME: This method has grown a bit lengthy. Perhaps it should be looked at one day.
142
132
  #
143
- def execute(verbose=false)
133
+ def execute
144
134
  # Search through the folders to gather all the files to be anonymized:
145
- add_msg("*******************************************************")
146
- add_msg("Initiating anonymization process.")
135
+ logger.info("Initiating anonymization process.")
147
136
  start_time = Time.now.to_f
148
- add_msg("Searching for files...")
137
+ logger.info("Searching for files...")
149
138
  load_files
150
- add_msg("Done.")
139
+ logger.info("Done.")
151
140
  if @files.length > 0
152
141
  if @tags.length > 0
153
- add_msg(@files.length.to_s + " files have been identified in the specified folder(s).")
142
+ logger.info(@files.length.to_s + " files have been identified in the specified folder(s).")
154
143
  if @write_path
155
144
  # Determine the write paths, as anonymized files will be written to a separate location:
156
- add_msg("Processing write paths...")
145
+ logger.info("Processing write paths...")
157
146
  process_write_paths
158
- add_msg("Done")
147
+ logger.info("Done")
159
148
  else
160
149
  # Overwriting old files:
161
- add_msg("Separate write folder not specified. Will overwrite existing DICOM files.")
150
+ logger.warn("Separate write folder not specified. Existing DICOM files will be overwritten.")
162
151
  @write_paths = @files
163
152
  end
164
153
  # If the user wants enumeration, we need to prepare variables for storing
165
154
  # existing information associated with each tag:
166
155
  create_enum_hash if @enumeration
167
156
  # Start the read/update/write process:
168
- add_msg("Initiating read/update/write process (This may take some time)...")
157
+ logger.info("Initiating read/update/write process. This may take some time...")
169
158
  # Monitor whether every file read/write was successful:
170
159
  all_read = true
171
160
  all_write = true
172
161
  files_written = 0
173
162
  files_failed_read = 0
163
+ # Temporarily increase the log threshold to suppress messages from the DObject class:
164
+ anonymizer_level = logger.level
165
+ logger.level = Logger::FATAL
174
166
  @files.each_index do |i|
175
167
  # Read existing file to DICOM object:
176
- obj = DICOM::DObject.new(@files[i], :verbose => verbose)
168
+ obj = DICOM::DObject.read(@files[i])
177
169
  if obj.read_success
178
170
  # Anonymize the desired tags:
179
171
  @tags.each_index do |j|
@@ -212,34 +204,35 @@ module DICOM
212
204
  files_failed_read += 1
213
205
  end
214
206
  end
215
- # Finished anonymizing files. Print elapsed time and status of anonymization:
207
+ # Finished anonymizing files. Reset the logg threshold:
208
+ logger.level = anonymizer_level
209
+ # Print elapsed time and status of anonymization:
216
210
  end_time = Time.now.to_f
217
- add_msg("Anonymization process completed!")
211
+ logger.info("Anonymization process completed!")
218
212
  if all_read
219
- add_msg("All files in specified folder(s) were SUCCESSFULLY read to DICOM objects.")
213
+ logger.info("All files in the specified folder(s) were SUCCESSFULLY read to DICOM objects.")
220
214
  else
221
- add_msg("Some files were NOT successfully read (#{files_failed_read} files). If folder(s) contain non-DICOM files, this is probably the reason.")
215
+ logger.warn("Some files were NOT successfully read (#{files_failed_read} files). If some folder(s) contain non-DICOM files, this is expected.")
222
216
  end
223
217
  if all_write
224
- add_msg("All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files).")
218
+ logger.info("All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files).")
225
219
  else
226
- add_msg("Some DICOM objects were NOT succesfully written to file. You are advised to have a closer look (#{files_written} files succesfully written).")
220
+ logger.warn("Some DICOM objects were NOT succesfully written to file. You are advised to investigate the result (#{files_written} files succesfully written).")
227
221
  end
228
222
  # Has user requested enumeration and specified an identity file in which to store the anonymized values?
229
223
  if @enumeration and @identity_file
230
- add_msg("Writing identity file.")
224
+ logger.info("Writing identity file.")
231
225
  write_identity_file
232
- add_msg("Done")
226
+ logger.info("Done")
233
227
  end
234
228
  elapsed = (end_time-start_time).to_s
235
- add_msg("Elapsed time: " + elapsed[0..elapsed.index(".")+1] + " seconds")
229
+ logger.info("Elapsed time: #{elapsed[0..elapsed.index(".")+1]} seconds")
236
230
  else
237
- add_msg("No tags have been selected for anonymization. Aborting.")
231
+ logger.warn("No tags were selected for anonymization. Aborting.")
238
232
  end
239
233
  else
240
- add_msg("No files were found in specified folders. Aborting.")
234
+ logger.warn("No files were found in specified folders. Aborting.")
241
235
  end
242
- add_msg("*******************************************************")
243
236
  end
244
237
 
245
238
  # Prints to screen a list of which tags are currently selected for anonymization along with
@@ -367,7 +360,7 @@ module DICOM
367
360
  if pos
368
361
  return @values[pos]
369
362
  else
370
- add_msg("The specified tag is not found in the list of tags to be anonymized.")
363
+ logger.warn("The specified tag (#{tag}) was not found in the list of tags to be anonymized.")
371
364
  return nil
372
365
  end
373
366
  end
@@ -377,18 +370,6 @@ module DICOM
377
370
  private
378
371
 
379
372
 
380
- # Adds one or more status messages to the log instance array, and if the verbose
381
- # instance variable is true, the status message is printed to the screen as well.
382
- #
383
- # === Parameters
384
- #
385
- # * <tt>msg</tt> -- Status message string.
386
- #
387
- def add_msg(msg)
388
- puts msg if @verbose
389
- @log << msg
390
- end
391
-
392
373
  # Finds the common path (if any) in the instance file path array, by performing a recursive search
393
374
  # on the folders that make up the path of one such file.
394
375
  # Returns the index of the last folder in the path of the selected file that is common for all file paths.
@@ -573,4 +554,4 @@ module DICOM
573
554
  end
574
555
 
575
556
  end
576
- end
557
+ end
@@ -10,6 +10,7 @@ module DICOM
10
10
  # FIXME: The code which waits for incoming network packets seems to be very CPU intensive. Perhaps there is a more elegant way to wait for incoming messages?
11
11
  #
12
12
  class DClient
13
+ include Logging
13
14
 
14
15
  # The name of this client (application entity).
15
16
  attr_accessor :ae
@@ -23,19 +24,17 @@ module DICOM
23
24
  attr_accessor :port
24
25
  # The maximum period the client will wait on an answer from a server before aborting the communication.
25
26
  attr_accessor :timeout
26
- # A boolean which defines if notices/warnings/errors will be printed to the screen (true) or not (false).
27
- attr_accessor :verbose
28
27
  # An array, where each index contains a hash with the data elements received in a command response (with tags as keys).
29
28
  attr_reader :command_results
30
29
  # An array, where each index contains a hash with the data elements received in a data response (with tags as keys).
31
30
  attr_reader :data_results
32
- # An array containing any error messages recorded.
33
- attr_reader :errors
34
- # An array containing any status messages recorded.
35
- attr_reader :notices
36
31
 
37
32
  # Creates a DClient instance.
38
33
  #
34
+ # === Notes
35
+ #
36
+ # * To customize logging behaviour, refer to the Logging module documentation.
37
+ #
39
38
  # === Parameters
40
39
  #
41
40
  # * <tt>host_ip</tt> -- String. The IP adress of the server which you are going to communicate with.
@@ -48,7 +47,6 @@ module DICOM
48
47
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
49
48
  # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
50
49
  # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
51
- # * <tt>:verbose</tt> -- Boolean. If set to false, the DClient instance will run silently and not output warnings and error messages to the screen. Defaults to true.
52
50
  #
53
51
  # === Examples
54
52
  #
@@ -66,11 +64,6 @@ module DICOM
66
64
  @max_package_size = options[:max_package_size] || 32768 # 16384
67
65
  @timeout = options[:timeout] || 10 # seconds
68
66
  @min_length = 12 # minimum number of bytes to expect in an incoming transmission
69
- @verbose = options[:verbose]
70
- @verbose = true if @verbose == nil # Default verbosity is 'on'.
71
- # Other instance variables:
72
- @errors = Array.new # errors and warnings are put in this array
73
- @notices = Array.new # information on successful transmissions are put in this array
74
67
  # Variables used for monitoring state of transmission:
75
68
  @association = nil # DICOM Association status
76
69
  @request_approved = nil # Status of our DICOM request
@@ -396,14 +389,14 @@ module DICOM
396
389
  establish_release
397
390
  else
398
391
  # Failed when loading the specified parameter as DICOM file(s). Will not transmit.
399
- add_error(message)
392
+ logger.error(message)
400
393
  end
401
394
  end
402
395
 
403
396
  # Tests the connection to the server in a very simple way by negotiating an association and then releasing it.
404
397
  #
405
398
  def test
406
- add_notice("TESTING CONNECTION...")
399
+ logger.info("TESTING CONNECTION...")
407
400
  success = false
408
401
  # Verification SOP Class:
409
402
  set_default_presentation_context(VERIFICATION_SOP)
@@ -417,9 +410,9 @@ module DICOM
417
410
  establish_release
418
411
  end
419
412
  if success
420
- add_notice("TEST SUCCSESFUL!")
413
+ logger.info("TEST SUCCSESFUL!")
421
414
  else
422
- add_error("TEST FAILED!")
415
+ logger.warn("TEST FAILED!")
423
416
  end
424
417
  return success
425
418
  end
@@ -429,30 +422,6 @@ module DICOM
429
422
  private
430
423
 
431
424
 
432
- # Adds a warning or error message to the instance array holding messages,
433
- # and prints the information to the screen if verbose is set.
434
- #
435
- # === Parameters
436
- #
437
- # * <tt>error</tt> -- A single error message or an array of error messages.
438
- #
439
- def add_error(error)
440
- puts error if @verbose
441
- @errors << error
442
- end
443
-
444
- # Adds a notice (information regarding progress or successful communications) to the instance array,
445
- # and prints the information to the screen if verbose is set.
446
- #
447
- # === Parameters
448
- #
449
- # * <tt>notice</tt> -- A single status message or an array of status messages.
450
- #
451
- def add_notice(notice)
452
- puts notice if @verbose
453
- @notices << notice
454
- end
455
-
456
425
  # Returns an array of supported transfer syntaxes for the specified transfer syntax.
457
426
  # For compressed transfer syntaxes, we currently do not support reencoding these to other syntaxes.
458
427
  #
@@ -486,11 +455,11 @@ module DICOM
486
455
  # Values of importance are extracted and put into instance variables:
487
456
  @association = true
488
457
  @max_pdu_length = info[:max_pdu_length]
489
- add_notice("Association successfully negotiated with host #{@host_ae} (#{@host_ip}).")
458
+ logger.info("Association successfully negotiated with host #{@host_ae} (#{@host_ip}).")
490
459
  # Check if all our presentation contexts was accepted by the host:
491
460
  process_presentation_context_response(info[:pc])
492
461
  else
493
- add_error("Association was denied from host #{@host_ae} (#{@host_ip})!")
462
+ logger.error("Association was denied from host #{@host_ae} (#{@host_ip})!")
494
463
  end
495
464
  end
496
465
  end
@@ -501,7 +470,7 @@ module DICOM
501
470
  @release = false
502
471
  if @abort
503
472
  @link.stop_session
504
- add_notice("Association has been closed. (#{@host_ae}, #{@host_ip})")
473
+ logger.info("Association has been closed. (#{@host_ae}, #{@host_ip})")
505
474
  else
506
475
  unless @link.session.closed?
507
476
  @link.build_release_request
@@ -509,12 +478,12 @@ module DICOM
509
478
  info = @link.receive_single_transmission.first
510
479
  @link.stop_session
511
480
  if info[:pdu] == PDU_RELEASE_RESPONSE
512
- add_notice("Association released properly from host #{@host_ae}.")
481
+ logger.info("Association released properly from host #{@host_ae}.")
513
482
  else
514
- add_error("Association released from host #{@host_ae}, but a release response was not registered.")
483
+ logger.error("Association released from host #{@host_ae}, but a release response was not registered.")
515
484
  end
516
485
  else
517
- add_error("Connection was closed by the host (for some unknown reason) before the association could be released properly.")
486
+ logger.error("Connection was closed by the host (for some unknown reason) before the association could be released properly.")
518
487
  end
519
488
  end
520
489
  @abort = false
@@ -533,33 +502,38 @@ module DICOM
533
502
  #
534
503
  # === Parameters
535
504
  #
536
- # * <tt>files</tt> -- A single file path or an array of paths, or a DObject or an array of DObject instances.
505
+ # * <tt>files_or_objects</tt> -- A single file path or an array of paths, or a DObject or an array of DObject instances.
537
506
  #
538
- def load_files(files)
507
+ def load_files(files_or_objects)
508
+ files_or_objects = [files_or_objects] unless files_or_objects.is_a?(Array)
539
509
  status = true
540
510
  message = ""
541
511
  objects = Array.new
542
512
  abstracts = Array.new
543
513
  id = 1
544
514
  @presentation_contexts = Hash.new
545
- files = [files] unless files.is_a?(Array)
546
- files.each do |file|
547
- if file.is_a?(String)
548
- obj = DObject.new(file, :verbose => false)
515
+ files_or_objects.each do |file_or_object|
516
+ if file_or_object.is_a?(String)
517
+ # Temporarily increase the log threshold to suppress messages from the DObject class:
518
+ client_level = logger.level
519
+ logger.level = Logger::FATAL
520
+ obj = DObject.read(file_or_object)
521
+ # Reset the logg threshold:
522
+ logger.level = client_level
549
523
  if obj.read_success
550
524
  # Load the DICOM object:
551
525
  objects << obj
552
526
  else
553
527
  status = false
554
- message = "Failed to successfully parse a DObject for the following string: #{file}"
528
+ message = "Failed to read a DObject from this file: #{file_or_object}"
555
529
  end
556
- elsif file.is_a?(DObject)
530
+ elsif file_or_object.is_a?(DObject)
557
531
  # Load the DICOM object and its abstract syntax:
558
- abstracts << file.value("0008,0016")
559
- objects << file
532
+ abstracts << file_or_object.value("0008,0016")
533
+ objects << file_or_object
560
534
  else
561
535
  status = false
562
- message = "Array contains invalid object #{file}."
536
+ message = "Array contains invalid object: #{file_or_object.class}."
563
537
  end
564
538
  end
565
539
  # Extract available transfer syntaxes for the various sop classes found amongst these objects
@@ -732,7 +706,7 @@ module DICOM
732
706
  process_returned_data(segments)
733
707
  end
734
708
  else
735
- add_error("Error: Unable to extract SOP Class UID and/or SOP Instance UID for this DICOM object. File will not be sent to its destination.")
709
+ logger.error("Unable to extract SOP Class UID and/or SOP Instance UID for this DICOM object. File will not be sent to its destination.")
736
710
  end
737
711
  end
738
712
  end
@@ -767,16 +741,27 @@ module DICOM
767
741
  if rejected.length == 0
768
742
  @request_approved = true
769
743
  if @approved_syntaxes.length == 1 and presentation_contexts.length == 1
770
- add_notice("The presentation context was accepted by host #{@host_ae}.")
744
+ logger.info("The presentation context was accepted by host #{@host_ae}.")
771
745
  else
772
- add_notice("All #{presentation_contexts.length} presentation contexts were accepted by host #{@host_ae} (#{@host_ip}).")
746
+ logger.info("All #{presentation_contexts.length} presentation contexts were accepted by host #{@host_ae} (#{@host_ip}).")
773
747
  end
774
748
  else
775
749
  # We still consider the request 'approved' if at least one context were accepted:
776
750
  @request_approved = true if @approved_syntaxes.length > 0
777
- add_error("One or more of your presentation contexts were denied by host #{@host_ae}!")
778
- @approved_syntaxes.each_pair {|key, value| add_error("APPROVED: #{LIBRARY.get_syntax_description(key)} (#{LIBRARY.get_syntax_description(value[1])})")}
779
- rejected.each_pair {|key, value| add_error("REJECTED: #{LIBRARY.get_syntax_description(key)} (#{LIBRARY.get_syntax_description(value[1])})")}
751
+
752
+ logger.error("One or more of your presentation contexts were denied by host #{@host_ae}!")
753
+
754
+ @approved_syntaxes.each_pair do |key, value|
755
+ sntx_k = LIBRARY.get_syntax_description(key)
756
+ sntx_v = LIBRARY.get_syntax_description(value[1])
757
+ logger.info("APPROVED: #{sntx_k} (#{sntx_v})")
758
+ end
759
+
760
+ rejected.each_pair do |key, value|
761
+ sntx_k = LIBRARY.get_syntax_description(key)
762
+ sntx_v = LIBRARY.get_syntax_description(value[1])
763
+ logger.error("REJECTED: #{sntx_k} (#{sntx_v})")
764
+ end
780
765
  end
781
766
  end
782
767
 
@@ -958,4 +943,4 @@ module DICOM
958
943
  end
959
944
 
960
945
  end
961
- end
946
+ end