dicom 0.9.2 → 0.9.3

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.
@@ -0,0 +1,117 @@
1
+ module DICOM
2
+
3
+ # The AuditTrail class handles key/value storage for the Anonymizer.
4
+ # When using the advanced Anonymization options such as enumeration
5
+ # and UID replacement, the AuditTrail class keeps track of key/value
6
+ # pairs and dumps this information to a text file using the json format.
7
+ # This enables us to ensure a unique relationship between the anonymized
8
+ # values and the original values, as well as preserving this relationship
9
+ # for later restoration of original values.
10
+ #
11
+ class AuditTrail
12
+
13
+ # The hash used for storing the key/value pairs of this instace.
14
+ attr_reader :dictionary
15
+
16
+ # Creates a new AuditTrail instance by loading the information stored
17
+ # in the specified file.
18
+ #
19
+ # === Parameters
20
+ #
21
+ # * <tt>file_name</tt> -- The path to a file containing a previously stored audit trail.
22
+ #
23
+ def self.read(file_name)
24
+ audit_trail = AuditTrail.new
25
+ audit_trail.load(file_name)
26
+ return audit_trail
27
+ end
28
+
29
+ # Creates a new AuditTrail instance.
30
+ #
31
+ def initialize
32
+ # The AuditTrail requires JSON for serialization:
33
+ require 'json'
34
+ # Define the key/value hash used for tag records:
35
+ @dictionary = Hash.new
36
+ end
37
+
38
+ # Adds a tag record to the log.
39
+ #
40
+ # === Parameters
41
+ #
42
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
43
+ # * <tt>original</tt> -- The original value (e.q. "John Doe").
44
+ # * <tt>replacement</tt> -- The replacement value (e.q. "Patient1").
45
+ #
46
+ def add_record(tag, original, replacement)
47
+ @dictionary[tag] = Hash.new unless @dictionary.key?(tag)
48
+ @dictionary[tag][original] = replacement
49
+ end
50
+
51
+ # Loads the key/value dictionary hash from a specified file.
52
+ #
53
+ # === Parameters
54
+ #
55
+ # * <tt>file_name</tt> -- The path to a file containing a previously stored audit trail.
56
+ #
57
+ def load(file_name)
58
+ @dictionary = JSON.load(File.new(file_name, "r"))
59
+ end
60
+
61
+ # Retrieves the replacement value used for the given tag and its original value.
62
+ #
63
+ # === Parameters
64
+ #
65
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
66
+ # * <tt>replacement</tt> -- The replacement value (e.q. "Patient1").
67
+ #
68
+ def original(tag, replacement)
69
+ original = nil
70
+ if @dictionary.key?(tag)
71
+ original = @dictionary[tag].key(replacement)
72
+ end
73
+ return original
74
+ end
75
+
76
+ # Returns the key/value pairs for a specific tag.
77
+ #
78
+ # === Parameters
79
+ #
80
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
81
+ #
82
+ def records(tag)
83
+ if @dictionary.key?(tag)
84
+ return @dictionary[tag]
85
+ else
86
+ return Hash.new
87
+ end
88
+ end
89
+
90
+ # Retrieves the replacement value used for the given tag and its original value.
91
+ #
92
+ # === Parameters
93
+ #
94
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
95
+ # * <tt>original</tt> -- The original value (e.q. "John Doe").
96
+ #
97
+ def replacement(tag, original)
98
+ replacement = nil
99
+ replacement = @dictionary[tag][original] if @dictionary.key?(tag)
100
+ return replacement
101
+ end
102
+
103
+ # Dumps the key/value pairs to a json string which is written to
104
+ # file as specified by the @file_name attribute of this instance.
105
+ #
106
+ #
107
+ # === Parameters
108
+ #
109
+ # * <tt>file_name</tt> -- The file name string to be used for storing & retrieving key/value pairs on disk.
110
+ #
111
+ def write(file_name)
112
+ str = JSON.pretty_generate(@dictionary)
113
+ File.open(file_name, 'w') {|f| f.write(str) }
114
+ end
115
+
116
+ end
117
+ end
@@ -1,7 +1,6 @@
1
-
2
1
  module DICOM
3
2
 
4
- # Ruby DICOM's Implementation UID.
3
+ # Ruby DICOM's registered DICOM UID root (Implementation Class UID).
5
4
  UID = "1.2.826.0.1.3680043.8.641"
6
5
  # Ruby DICOM name & version (max 16 characters).
7
6
  NAME = "RUBY_DCM_" + DICOM::VERSION
@@ -517,12 +517,12 @@ module DICOM
517
517
  # Temporarily increase the log threshold to suppress messages from the DObject class:
518
518
  client_level = logger.level
519
519
  logger.level = Logger::FATAL
520
- obj = DObject.read(file_or_object)
520
+ dcm = DObject.read(file_or_object)
521
521
  # Reset the logg threshold:
522
522
  logger.level = client_level
523
- if obj.read_success
523
+ if dcm.read_success
524
524
  # Load the DICOM object:
525
- objects << obj
525
+ objects << dcm
526
526
  else
527
527
  status = false
528
528
  message = "Failed to read a DObject from this file: #{file_or_object}"
@@ -538,10 +538,10 @@ module DICOM
538
538
  end
539
539
  # Extract available transfer syntaxes for the various sop classes found amongst these objects
540
540
  syntaxes = Hash.new
541
- objects.each do |obj|
542
- sop_class = obj.value("0008,0016")
541
+ objects.each do |dcm|
542
+ sop_class = dcm.value("0008,0016")
543
543
  if sop_class
544
- transfer_syntaxes = available_transfer_syntaxes(obj.transfer_syntax)
544
+ transfer_syntaxes = available_transfer_syntaxes(dcm.transfer_syntax)
545
545
  if syntaxes[sop_class]
546
546
  syntaxes[sop_class] << transfer_syntaxes
547
547
  else
@@ -670,10 +670,10 @@ module DICOM
670
670
  # conveys the information from the selected DICOM file.
671
671
  #
672
672
  def perform_send(objects)
673
- objects.each_with_index do |obj, index|
673
+ objects.each_with_index do |dcm, index|
674
674
  # Gather necessary information from the object (SOP Class & Instance UID):
675
- sop_class = obj.value("0008,0016")
676
- sop_instance = obj.value("0008,0018")
675
+ sop_class = dcm.value("0008,0016")
676
+ sop_instance = dcm.value("0008,0018")
677
677
  if sop_class and sop_instance
678
678
  # Only send the image if its sop_class has been accepted by the receiver:
679
679
  if @approved_syntaxes[sop_class]
@@ -685,11 +685,11 @@ module DICOM
685
685
  selected_transfer_syntax = @approved_syntaxes[sop_class][1]
686
686
  # Encode our DICOM object to a binary string which is split up in pieces, sufficiently small to fit within the specified maximum pdu length:
687
687
  # Set the transfer syntax of the DICOM object equal to the one accepted by the SCP:
688
- obj.transfer_syntax = selected_transfer_syntax
688
+ dcm.transfer_syntax = selected_transfer_syntax
689
689
  # Remove the Meta group, since it doesn't belong in a DICOM file transfer:
690
- obj.remove_group(META_GROUP)
690
+ dcm.delete_group(META_GROUP)
691
691
  max_header_length = 14
692
- data_packages = obj.encode_segments(@max_pdu_length - max_header_length, selected_transfer_syntax)
692
+ data_packages = dcm.encode_segments(@max_pdu_length - max_header_length, selected_transfer_syntax)
693
693
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
694
694
  @link.transmit
695
695
  # Transmit all but the last data strings:
@@ -728,7 +728,7 @@ module DICOM
728
728
  presentation_contexts.each do |pc|
729
729
  # Determine what abstract syntax this particular presentation context's id corresponds to:
730
730
  id = pc[:presentation_context_id]
731
- raise "Error! Even presentation context ID received in the association response. This is not allowed according to the DICOM standard!" if id[0] == 0 # If even number.
731
+ raise "Error! Even presentation context ID received in the association response. This is not allowed according to the DICOM standard!" if id.even?
732
732
  abstract_syntax = find_abstract_syntax(id)
733
733
  if pc[:result] == 0
734
734
  accepted_pc += 1
@@ -1,18 +1,18 @@
1
1
  # === TODO:
2
2
  #
3
- # * The retrieve file network functionality (get_image() in DClient class) has not been tested.
3
+ # * The retrieve file network functionality (#get_image in DClient class) has not been tested.
4
4
  # * Make the networking code more intelligent in its handling of unexpected network communication.
5
5
  # * Full support for compressed image data.
6
6
  # * Read/Write 12 bit image data.
7
- # * Full color support (RGB and PALETTE COLOR with get_object_magick() already implemented).
8
- # * Support for extraction of multiple encapsulated pixel data frames in get_image() and get_image_narray().
7
+ # * Full color support (RGB and PALETTE COLOR with #image already implemented).
8
+ # * Support for extraction of multiple encapsulated pixel data frames in #pixels and #narray.
9
9
  # * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
10
10
  # * More robust and flexible options for reorienting extracted pixel arrays?
11
11
  # * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
12
12
  # * Add these as github issues and remove this list!
13
13
 
14
14
 
15
- # Copyright 2008-2011 Christoffer Lervag
15
+ # Copyright 2008-2012 Christoffer Lervag
16
16
  #
17
17
  # This program is free software: you can redistribute it and/or modify
18
18
  # it under the terms of the GNU General Public License as published by
@@ -77,12 +77,12 @@ module DICOM
77
77
  bin = File.open(file, "rb") { |f| f.read }
78
78
  # Parse the file contents and create the DICOM object:
79
79
  if bin
80
- obj = self.parse(bin)
80
+ dcm = self.parse(bin)
81
81
  else
82
- obj = self.new
83
- obj.read_success = false
82
+ dcm = self.new
83
+ dcm.read_success = false
84
84
  end
85
- return obj
85
+ return dcm
86
86
  end
87
87
 
88
88
  # Creates a DObject instance by parsing an encoded binary DICOM string.
@@ -102,9 +102,9 @@ module DICOM
102
102
  no_header = options[:no_meta]
103
103
  raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
104
104
  raise ArgumentError, "Invalid option :syntax. Expected String, got #{syntax.class}." if syntax && !syntax.is_a?(String)
105
- obj = self.new
106
- obj.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
107
- return obj
105
+ dcm = self.new
106
+ dcm.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
107
+ return dcm
108
108
  end
109
109
 
110
110
  # Creates a DObject instance by reading and parsing a DICOM file.
@@ -136,12 +136,12 @@ module DICOM
136
136
  end
137
137
  # Parse the file contents and create the DICOM object:
138
138
  if bin
139
- obj = self.parse(bin)
139
+ dcm = self.parse(bin)
140
140
  else
141
- obj = self.new
142
- obj.read_success = false
141
+ dcm = self.new
142
+ dcm.read_success = false
143
143
  end
144
- return obj
144
+ return dcm
145
145
  end
146
146
 
147
147
  # A boolean set as false. This attribute is included to provide consistency with other object types for the internal methods which use it.
@@ -180,12 +180,12 @@ module DICOM
180
180
  #
181
181
  # # Load a DICOM file (Deprecated: please use DObject.read() instead):
182
182
  # require 'dicom'
183
- # obj = DICOM::DObject.new("test.dcm")
183
+ # dcm = DICOM::DObject.new("test.dcm")
184
184
  # # Read a DICOM file that has already been loaded into memory in a binary string (with a known transfer syntax):
185
185
  # # (Deprecated: please use DObject.parse() instead)
186
- # obj = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
186
+ # dcm = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
187
187
  # # Create an empty DICOM object
188
- # obj = DICOM::DObject.new
188
+ # dcm = DICOM::DObject.new
189
189
  # # Increasing the log message threshold (default level is INFO):
190
190
  # DICOM.logger.level = Logger::WARN
191
191
  #
@@ -214,6 +214,16 @@ module DICOM
214
214
  end
215
215
  end
216
216
 
217
+ # Returns true if the argument is an instance with attributes equal to self.
218
+ #
219
+ def ==(other)
220
+ if other.respond_to?(:to_dcm)
221
+ other.send(:state) == state
222
+ end
223
+ end
224
+
225
+ alias_method :eql?, :==
226
+
217
227
  # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
218
228
  #
219
229
  # Returns the encoded binary strings in an array.
@@ -225,7 +235,7 @@ module DICOM
225
235
  #
226
236
  # === Examples
227
237
  #
228
- # encoded_strings = obj.encode_segments(16384)
238
+ # encoded_strings = dcm.encode_segments(16384)
229
239
  #
230
240
  def encode_segments(max_size, transfer_syntax=transfer_syntax)
231
241
  raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
@@ -238,6 +248,12 @@ module DICOM
238
248
  return w.segments
239
249
  end
240
250
 
251
+ # Generates a Fixnum hash value for this instance.
252
+ #
253
+ def hash
254
+ state.hash
255
+ end
256
+
241
257
  # Prints information of interest related to the DICOM object.
242
258
  # Calls the print() method of Parent as well as the information() method of DObject.
243
259
  #
@@ -396,6 +412,12 @@ module DICOM
396
412
  return info
397
413
  end
398
414
 
415
+ # Returns self.
416
+ #
417
+ def to_dcm
418
+ self
419
+ end
420
+
399
421
  # Returns the transfer syntax string of the DObject.
400
422
  #
401
423
  # If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
@@ -447,7 +469,7 @@ module DICOM
447
469
  #
448
470
  # === Examples
449
471
  #
450
- # obj.write(path + "test.dcm")
472
+ # dcm.write(path + "test.dcm")
451
473
  #
452
474
  def write(file_name, options={})
453
475
  raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
@@ -483,8 +505,8 @@ module DICOM
483
505
  end
484
506
  # Source Application Entity Title:
485
507
  Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
486
- # Group Length: Remove the old one (if it exists) before creating a new one.
487
- remove("0002,0000")
508
+ # Group Length: Delete the old one (if it exists) before creating a new one.
509
+ delete("0002,0000")
488
510
  Element.new("0002,0000", meta_group_length, :parent => self)
489
511
  end
490
512
 
@@ -507,5 +529,11 @@ module DICOM
507
529
  return group_length
508
530
  end
509
531
 
532
+ # Returns the attributes (children) of this instance (for comparison purposes).
533
+ #
534
+ def state
535
+ @tags
536
+ end
537
+
510
538
  end
511
539
  end
@@ -19,7 +19,7 @@ module DICOM
19
19
  # An array which records any status messages that are generated while parsing the DICOM string.
20
20
  attr_reader :msg
21
21
  # A DObject instance which the parsed data elements will be connected to.
22
- attr_reader :obj
22
+ attr_reader :dcm
23
23
  # A boolean which records whether the DICOM string contained the proper DICOM header signature of 128 bytes + 'DICM'.
24
24
  attr_reader :signature
25
25
  # A boolean which reports whether the DICOM string was parsed successfully (true) or not (false).
@@ -30,7 +30,7 @@ module DICOM
30
30
  #
31
31
  # === Parameters
32
32
  #
33
- # * <tt>obj</tt> -- A DObject instance which the parsed data elements will be connected to.
33
+ # * <tt>dcm</tt> -- A DObject instance which the parsed data elements will be connected to.
34
34
  # * <tt>string</tt> -- A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
35
35
  # * <tt>options</tt> -- A hash of parameters.
36
36
  #
@@ -40,13 +40,13 @@ module DICOM
40
40
  # * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
41
41
  # * <tt>:syntax</tt> -- String. If specified, the decoding of the DICOM string will be forced to use this transfer syntax.
42
42
  #
43
- def initialize(obj, string=nil, options={})
43
+ def initialize(dcm, string=nil, options={})
44
44
  # Set the DICOM object as an instance variable:
45
- @obj = obj
45
+ @dcm = dcm
46
46
  # If a transfer syntax has been specified as an option for a DICOM object, make sure that it makes it into the object:
47
47
  if options[:syntax]
48
48
  @transfer_syntax = options[:syntax]
49
- obj.add(Element.new("0002,0010", options[:syntax])) if obj.is_a?(DObject)
49
+ dcm.add(Element.new("0002,0010", options[:syntax])) if dcm.is_a?(DObject)
50
50
  end
51
51
  # Initiate the variables that are used during file reading:
52
52
  init_variables
@@ -292,7 +292,7 @@ module DICOM
292
292
  length = @stream.decode(bytes, "SL") # (4)
293
293
  end
294
294
  # Check that length is valid (according to the DICOM standard, it must be even):
295
- raise "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.odd? and length > 0
296
296
  return vr, length
297
297
  end
298
298
 
@@ -366,7 +366,7 @@ module DICOM
366
366
  def switch_syntax
367
367
  # Get the transfer syntax string, unless it has already been provided by keyword:
368
368
  unless @transfer_syntax
369
- ts_element = @obj["0002,0010"]
369
+ ts_element = @dcm["0002,0010"]
370
370
  if ts_element
371
371
  @transfer_syntax = ts_element.value
372
372
  else
@@ -411,9 +411,9 @@ module DICOM
411
411
  # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
412
412
  @switched = false
413
413
  # Keeping track of the data element parent status while parsing the DICOM string:
414
- @current_parent = @obj
414
+ @current_parent = @dcm
415
415
  # Keeping track of what is the current data element:
416
- @current_element = @obj
416
+ @current_element = @dcm
417
417
  # Items contained under the pixel data element may contain data directly, so we need a variable to keep track of this:
418
418
  @enc_image = false
419
419
  # Assume header size is zero bytes until otherwise is determined:
@@ -33,6 +33,8 @@ module DICOM
33
33
 
34
34
  # A customized FileHandler class to use instead of the default FileHandler included with Ruby DICOM.
35
35
  attr_accessor :file_handler
36
+ # The hostname that the TCPServer binds to.
37
+ attr_accessor :host
36
38
  # The name of the server (application entity).
37
39
  attr_accessor :host_ae
38
40
  # The maximum allowed size of network packages (in bytes).
@@ -61,6 +63,7 @@ module DICOM
61
63
  # === Options
62
64
  #
63
65
  # * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
66
+ # * <tt>:host</tt> -- String. The hostname that the TCPServer binds to. Defaults to '127.0.0.1'.
64
67
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
65
68
  # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
66
69
  # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
@@ -79,6 +82,7 @@ module DICOM
79
82
  @port = port
80
83
  # Optional parameters (and default values):
81
84
  @file_handler = options[:file_handler] || FileHandler
85
+ @host = options[:host] || '127.0.0.1'
82
86
  @host_ae = options[:host_ae] || "RUBY_DICOM"
83
87
  @max_package_size = options[:max_package_size] || 32768 # 16384
84
88
  @timeout = options[:timeout] || 10 # seconds
@@ -144,14 +148,14 @@ module DICOM
144
148
  end
145
149
  end
146
150
 
147
- # Removes a specific abstract syntax from the list of abstract syntaxes that the server will accept.
151
+ # Deletes a specific abstract syntax from the list of abstract syntaxes that the server will accept.
148
152
  #
149
153
  #
150
154
  # === Parameters
151
155
  #
152
156
  # * <tt>uid</tt> -- An abstract syntax UID string.
153
157
  #
154
- def remove_abstract_syntax(uid)
158
+ def delete_abstract_syntax(uid)
155
159
  if uid.is_a?(String)
156
160
  @accepted_abstract_syntaxes.delete(uid)
157
161
  else
@@ -159,13 +163,13 @@ module DICOM
159
163
  end
160
164
  end
161
165
 
162
- # Removes a specific transfer syntax from the list of transfer syntaxes that the server will accept.
166
+ # Deletes a specific transfer syntax from the list of transfer syntaxes that the server will accept.
163
167
  #
164
168
  # === Parameters
165
169
  #
166
170
  # * <tt>uid</tt> -- A transfer syntax UID string.
167
171
  #
168
- def remove_transfer_syntax(uid)
172
+ def delete_transfer_syntax(uid)
169
173
  if uid.is_a?(String)
170
174
  @accepted_transfer_syntaxes.delete(uid)
171
175
  else
@@ -177,9 +181,9 @@ module DICOM
177
181
  #
178
182
  # === Notes
179
183
  #
180
- # * Following such a removal, the user must ensure to add the specific abstract syntaxes that are to be accepted by the server.
184
+ # * Following such a clearance, the user must ensure to add the specific abstract syntaxes that are to be accepted by the server.
181
185
  #
182
- def remove_all_abstract_syntaxes
186
+ def clear_abstract_syntaxes
183
187
  @accepted_abstract_syntaxes = Hash.new
184
188
  end
185
189
 
@@ -187,9 +191,9 @@ module DICOM
187
191
  #
188
192
  # === Notes
189
193
  #
190
- # * Following such a removal, the user must ensure to add the specific transfer syntaxes that are to be accepted by the server.
194
+ # * Following such a clearance, the user must ensure to add the specific transfer syntaxes that are to be accepted by the server.
191
195
  #
192
- def remove_all_transfer_syntaxes
196
+ def clear_transfer_syntaxes
193
197
  @accepted_transfer_syntaxes = Hash.new
194
198
  end
195
199
 
@@ -209,7 +213,7 @@ module DICOM
209
213
  logger.info("Started DICOM SCP server on port #{@port}.")
210
214
  logger.info("Waiting for incoming transmissions...\n\n")
211
215
  # Initiate server:
212
- @scp = TCPServer.new(@port)
216
+ @scp = TCPServer.new(@host, @port)
213
217
  # Use a loop to listen for incoming messages:
214
218
  loop do
215
219
  Thread.start(@scp.accept) do |session|
@@ -279,7 +283,7 @@ module DICOM
279
283
  #
280
284
  # === Notes
281
285
  #
282
- # Other things can potentionally be checked here too, if we want to make the server more strict with regards to what information is received:
286
+ # Other things can potentially be checked here too, if we want to make the server more strict with regards to what information is received:
283
287
  # * Application context name, calling AE title, called AE title
284
288
  # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.4 (Table 9-21).
285
289
  #