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