dicom 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,11 +7,12 @@ module DICOM
7
7
  #
8
8
  # @param [String] bin an encoded binary string containing DICOM information
9
9
  # @param [String] syntax the transfer syntax to use when decoding the DICOM string
10
+ # @param [Boolean] switched indicating whether the transfer syntax 'switch' has occured in the data stream of this object
10
11
  #
11
- def parse(bin, syntax)
12
+ def parse(bin, syntax, switched=false, explicit=true)
12
13
  raise ArgumentError, "Invalid argument 'bin'. Expected String, got #{bin.class}." unless bin.is_a?(String)
13
14
  raise ArgumentError, "Invalid argument 'syntax'. Expected String, got #{syntax.class}." unless syntax.is_a?(String)
14
- read(bin, signature=false, :syntax => syntax)
15
+ read(bin, signature=false, :syntax => syntax, :switched => switched, :explicit => explicit)
15
16
  end
16
17
 
17
18
 
@@ -69,6 +70,11 @@ module DICOM
69
70
  #
70
71
  def process_data_element
71
72
  # FIXME: This method has grown a bit messy and isn't very pleasant to read. Cleanup possible?
73
+ # After having been into a possible unknown sequence with undefined length, we may need to reset
74
+ # explicitness from implicit to explicit:
75
+ if !@original_explicit.nil? && @explicitness_reset_parent == @current_parent
76
+ @explicit = @original_explicit
77
+ end
72
78
  # STEP 1:
73
79
  # Attempt to read data element tag:
74
80
  tag = read_tag
@@ -112,9 +118,16 @@ module DICOM
112
118
  end
113
119
  end
114
120
  # Create an Element from the gathered data:
115
- if level_vr == "SQ" or tag == ITEM_TAG
116
- if level_vr == "SQ"
121
+ # if vr is UN ("unknown") and length is -1, treat as a sequence (sec. 6.2.2 of DICOM standard)
122
+ if level_vr == "SQ" or tag == ITEM_TAG or (level_vr == "UN" and length == -1)
123
+ if level_vr == "SQ" or (level_vr == "UN" and length == -1)
117
124
  check_duplicate(tag, 'Sequence')
125
+ # If we get an unknown sequence with undefined length, we must switch to implicit for decoding its content:
126
+ if level_vr == "UN" and length == -1
127
+ @original_explicit = @explicit
128
+ @explicit = false
129
+ @explicitness_reset_parent = @current_parent
130
+ end
118
131
  unless @current_parent[tag] and !@overwrite
119
132
  @current_element = Sequence.new(tag, :length => length, :name => name, :parent => @current_parent, :vr => vr)
120
133
  else
@@ -143,7 +156,7 @@ module DICOM
143
156
  # If length is specified (no delimitation items), load a new DRead instance to read these child elements
144
157
  # and load them into the current sequence. The exception is when we have a pixel data item.
145
158
  if length > 0 and not @enc_image
146
- @current_element.parse(bin, @transfer_syntax)
159
+ @current_element.parse(bin, @transfer_syntax, switched=@switched, @explicit)
147
160
  @current_parent = @current_parent.parent
148
161
  return false unless @read_success
149
162
  end
@@ -177,8 +190,8 @@ module DICOM
177
190
  @overwrite = options[:overwrite]
178
191
  # Presence of the official DICOM signature:
179
192
  @signature = false
180
- # Default explicitness of start of DICOM string:
181
- @explicit = true
193
+ # Default explicitness of start of DICOM string (if undefined it defaults to true):
194
+ @explicit = options[:explicit].nil? ? true : options[:explicit]
182
195
  # Default endianness of start of DICOM string is little endian:
183
196
  @str_endian = false
184
197
  # A switch of endianness may occur after the initial meta group, an this needs to be monitored:
@@ -188,7 +201,7 @@ module DICOM
188
201
  # Endianness of the remaining groups after the first group:
189
202
  @rest_endian = false
190
203
  # When the string switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
191
- @switched = false
204
+ @switched = options[:switched] ? options[:switched] : false
192
205
  # Keeping track of the data element parent status while parsing the DICOM string:
193
206
  @current_parent = self
194
207
  # Keeping track of what is the current data element:
@@ -1,329 +1,329 @@
1
- module DICOM
2
-
3
- # This class contains code for setting up a Service Class Provider (SCP),
4
- # which will act as a simple storage node (a DICOM server that receives images).
5
- #
6
- class DServer
7
- include Logging
8
-
9
- # Runs the server and takes a block for initializing.
10
- #
11
- # @param [Integer] port the network port to be used (defaults to 104)
12
- # @param [String] path the directory where incoming DICOM files will be stored (defaults to './received/')
13
- # @param [&block] block a block of code that will be run on the DServer instance, between creation and the launch of the SCP itself
14
- #
15
- # @example Run a server instance with a custom file handler
16
- # require 'dicom'
17
- # require 'my_file_handler'
18
- # include DICOM
19
- # DServer.run(104, 'c:/temp/') do |s|
20
- # s.timeout = 100
21
- # s.file_handler = MyFileHandler
22
- # end
23
- #
24
- def self.run(port=104, path='./received/', &block)
25
- server = DServer.new(port)
26
- server.instance_eval(&block)
27
- server.start_scp(path)
28
- end
29
-
30
- # A customized FileHandler class to use instead of the default FileHandler included with ruby-dicom.
31
- attr_accessor :file_handler
32
- # The hostname that the TCPServer binds to.
33
- attr_accessor :host
34
- # The name of the server (application entity).
35
- attr_accessor :host_ae
36
- # The maximum allowed size of network packages (in bytes).
37
- attr_accessor :max_package_size
38
- # The network port to be used.
39
- attr_accessor :port
40
- # The maximum period the server will wait on an answer from a client before aborting the communication.
41
- attr_accessor :timeout
42
-
43
- # A hash containing the abstract syntaxes that will be accepted.
44
- attr_reader :accepted_abstract_syntaxes
45
- # A hash containing the transfer syntaxes that will be accepted.
46
- attr_reader :accepted_transfer_syntaxes
47
-
48
- # Creates a DServer instance.
49
- #
50
- # @note To customize logging behaviour, refer to the Logging module documentation.
51
- #
52
- # @param [Integer] port the network port to be used
53
- # @param [Hash] options the options to use for the DICOM server
54
- # @option options [String] :file_handler a customized FileHandler class to use instead of the default FileHandler
55
- # @option options [String] :host the hostname that the TCPServer binds to (defaults to '0.0.0.0')
56
- # @option options [String] :host_ae the name of the server (application entity)
57
- # @option options [String] :max_package_size the maximum allowed size of network packages (in bytes)
58
- # @option options [String] :timeout the number of seconds the server will wait on an answer from a client before aborting the communication
59
- #
60
- # @example Create a server using default settings
61
- # s = DICOM::DServer.new
62
- # @example Create a server with a specific host name and a custom buildt file handler
63
- # require_relative 'my_file_handler'
64
- # server = DICOM::DServer.new(104, :host_ae => "RUBY_SERVER", :file_handler => DICOM::MyFileHandler)
65
- #
66
- def initialize(port=104, options={})
67
- require 'socket'
68
- # Required parameters:
69
- @port = port
70
- # Optional parameters (and default values):
71
- @file_handler = options[:file_handler] || FileHandler
72
- @host = options[:host] || '0.0.0.0'
73
- @host_ae = options[:host_ae] || "RUBY_DICOM"
74
- @max_package_size = options[:max_package_size] || 32768 # 16384
75
- @timeout = options[:timeout] || 10 # seconds
76
- @min_length = 12 # minimum number of bytes to expect in an incoming transmission
77
- # Variables used for monitoring state of transmission:
78
- @connection = nil # TCP connection status
79
- @association = nil # DICOM Association status
80
- @request_approved = nil # Status of our DICOM request
81
- @release = nil # Status of received, valid release response
82
- set_default_accepted_syntaxes
83
- end
84
-
85
- # Adds an abstract syntax to the list of abstract syntaxes that the server will accept.
86
- #
87
- # @param [String] uid an abstract syntax UID
88
- #
89
- def add_abstract_syntax(uid)
90
- lib_uid = LIBRARY.uid(uid)
91
- raise "Invalid/unknown UID: #{uid}" unless lib_uid
92
- @accepted_abstract_syntaxes[uid] = lib_uid.name
93
- end
94
-
95
- # Adds a transfer syntax to the list of transfer syntaxes that the server will accept.
96
- #
97
- # @param [String] uid a transfer syntax UID
98
- #
99
- def add_transfer_syntax(uid)
100
- lib_uid = LIBRARY.uid(uid)
101
- raise "Invalid/unknown UID: #{uid}" unless lib_uid
102
- @accepted_transfer_syntaxes[uid] = lib_uid.name
103
- end
104
-
105
- # Prints the list of accepted abstract syntaxes to the screen.
106
- #
107
- def print_abstract_syntaxes
108
- # Determine length of longest key to ensure pretty print:
109
- max_uid = @accepted_abstract_syntaxes.keys.collect{|k| k.length}.max
110
- puts "Abstract syntaxes which are accepted by this SCP:"
111
- @accepted_abstract_syntaxes.sort.each do |pair|
112
- puts "#{pair[0]}#{' '*(max_uid-pair[0].length)} #{pair[1]}"
113
- end
114
- end
115
-
116
- # Prints the list of accepted transfer syntaxes to the screen.
117
- #
118
- def print_transfer_syntaxes
119
- # Determine length of longest key to ensure pretty print:
120
- max_uid = @accepted_transfer_syntaxes.keys.collect{|k| k.length}.max
121
- puts "Transfer syntaxes which are accepted by this SCP:"
122
- @accepted_transfer_syntaxes.sort.each do |pair|
123
- puts "#{pair[0]}#{' '*(max_uid-pair[0].length)} #{pair[1]}"
124
- end
125
- end
126
-
127
- # Deletes a specific abstract syntax from the list of abstract syntaxes
128
- # that the server will accept.
129
- #
130
- # @param [String] uid an abstract syntax UID
131
- #
132
- def delete_abstract_syntax(uid)
133
- if uid.is_a?(String)
134
- @accepted_abstract_syntaxes.delete(uid)
135
- else
136
- raise "Invalid type of UID. Expected String, got #{uid.class}!"
137
- end
138
- end
139
-
140
- # Deletes a specific transfer syntax from the list of transfer syntaxes
141
- # that the server will accept.
142
- #
143
- # @param [String] uid a transfer syntax UID
144
- #
145
- def delete_transfer_syntax(uid)
146
- if uid.is_a?(String)
147
- @accepted_transfer_syntaxes.delete(uid)
148
- else
149
- raise "Invalid type of UID. Expected String, got #{uid.class}!"
150
- end
151
- end
152
-
153
- # Completely clears the list of abstract syntaxes that the server will accept.
154
- #
155
- # Following such a clearance, the user must ensure to add the specific
156
- # abstract syntaxes that are to be accepted by the server.
157
- #
158
- def clear_abstract_syntaxes
159
- @accepted_abstract_syntaxes = Hash.new
160
- end
161
-
162
- # Completely clears the list of transfer syntaxes that the server will accept.
163
- #
164
- # Following such a clearance, the user must ensure to add the specific
165
- # transfer syntaxes that are to be accepted by the server.
166
- #
167
- def clear_transfer_syntaxes
168
- @accepted_transfer_syntaxes = Hash.new
169
- end
170
-
171
- # Starts the Service Class Provider (SCP).
172
- #
173
- # This service acts as a simple storage node, which receives DICOM files
174
- # and stores them in the specified folder.
175
- #
176
- # Customized storage actions can be set my modifying or replacing the FileHandler class.
177
- #
178
- # @param [String] path the directory where incoming files are to be saved
179
- #
180
- def start_scp(path='./received/')
181
- if @accepted_abstract_syntaxes.size > 0 and @accepted_transfer_syntaxes.size > 0
182
- logger.info("Started DICOM SCP server on port #{@port}.")
183
- logger.info("Waiting for incoming transmissions...\n\n")
184
- # Initiate server:
185
- @scp = TCPServer.new(@host, @port)
186
- # Use a loop to listen for incoming messages:
187
- loop do
188
- Thread.start(@scp.accept) do |session|
189
- # Initialize the network package handler for this session:
190
- link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :file_handler => @file_handler)
191
- link.set_session(session)
192
- # Note who has contacted us:
193
- logger.info("Connection established with: #{session.peeraddr[2]} (IP: #{session.peeraddr[3]})")
194
- # Receive an incoming message:
195
- segments = link.receive_multiple_transmissions
196
- info = segments.first
197
- # Interpret the received message:
198
- if info[:valid]
199
- association_error = check_association_request(info)
200
- unless association_error
201
- info, approved, rejected = process_syntax_requests(info)
202
- link.handle_association_accept(info)
203
- context = (LIBRARY.uid(info[:pc].first[:abstract_syntax]) ? LIBRARY.uid(info[:pc].first[:abstract_syntax]).name : 'Unknown UID!')
204
- if approved > 0
205
- if approved == 1
206
- logger.info("Accepted the association request with context: #{context}")
207
- else
208
- if rejected == 0
209
- logger.info("Accepted all #{approved} proposed contexts in the association request.")
210
- else
211
- logger.warn("Accepted only #{approved} of #{approved+rejected} of the proposed contexts in the association request.")
212
- end
213
- end
214
- # Process the incoming data. This method will also take care of releasing the association:
215
- success, messages = link.handle_incoming_data(path)
216
- # Pass along any messages that has been recorded:
217
- messages.each { |m| logger.public_send(m.first, m.last) } if messages.first
218
- else
219
- # No abstract syntaxes in the incoming request were accepted:
220
- if rejected == 1
221
- logger.warn("Rejected the association request with proposed context: #{context}")
222
- else
223
- logger.warn("Rejected all #{rejected} proposed contexts in the association request.")
224
- end
225
- # Since the requested abstract syntax was not accepted, the association must be released.
226
- link.await_release
227
- end
228
- else
229
- # The incoming association was not formally correct.
230
- link.handle_rejection
231
- end
232
- else
233
- # The incoming message was not recognised as a valid DICOM message. Abort:
234
- link.handle_abort
235
- end
236
- # Terminate the connection:
237
- link.stop_session
238
- logger.info("Connection closed.\n\n")
239
- end
240
- end
241
- else
242
- raise "Unable to start SCP server as no accepted abstract syntaxes have been set!" if @accepted_abstract_syntaxes.length == 0
243
- raise "Unable to start SCP server as no accepted transfer syntaxes have been set!" if @accepted_transfer_syntaxes.length == 0
244
- end
245
- end
246
-
247
-
248
- private
249
-
250
-
251
- # Checks if the association request is formally correct, by matching against an exact application context UID.
252
- # Returns nil if valid, and an error code if it is not approved.
253
- #
254
- # === Notes
255
- #
256
- # Other things can potentially be checked here too, if we want to make the server more strict with regards to what information is received:
257
- # * Application context name, calling AE title, called AE title
258
- # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.4 (Table 9-21).
259
- #
260
- # === Parameters
261
- #
262
- # * <tt>info</tt> -- An information hash from the received association request.
263
- #
264
- def check_association_request(info)
265
- unless info[:application_context] == APPLICATION_CONTEXT
266
- error = 2 # (application context name not supported)
267
- logger.error("The application context in the incoming association request was not recognized: (#{info[:application_context]})")
268
- else
269
- error = nil
270
- end
271
- return error
272
- end
273
-
274
- # Checks if the requested abstract syntax & its transfer syntax(es) are supported by this server instance,
275
- # and inserts a corresponding result code for each presentation context.
276
- # Returns the modified association information hash, as well as the number of abstract syntaxes that were accepted and rejected.
277
- #
278
- # === Notes
279
- #
280
- # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.3.2 (Table 9-18).
281
- #
282
- # === Parameters
283
- #
284
- # * <tt>info</tt> -- An information hash from the received association request.
285
- #
286
- def process_syntax_requests(info)
287
- # A couple of variables used to analyse the properties of the association:
288
- approved = 0
289
- rejected = 0
290
- # Loop through the presentation contexts:
291
- info[:pc].each do |pc|
292
- if @accepted_abstract_syntaxes[pc[:abstract_syntax]]
293
- # Abstract syntax accepted. Proceed to check its transfer syntax(es):
294
- proposed_transfer_syntaxes = pc[:ts].collect{|t| t[:transfer_syntax]}.sort
295
- # Choose the first proposed transfer syntax that exists in our list of accepted transfer syntaxes:
296
- accepted_transfer_syntax = nil
297
- proposed_transfer_syntaxes.each do |proposed_ts|
298
- if @accepted_transfer_syntaxes.include?(proposed_ts)
299
- accepted_transfer_syntax = proposed_ts
300
- break
301
- end
302
- end
303
- if accepted_transfer_syntax
304
- # Both abstract and transfer syntax has been approved:
305
- pc[:result] = ACCEPTANCE
306
- pc[:selected_transfer_syntax] = accepted_transfer_syntax
307
- # Update our status variables:
308
- approved += 1
309
- else
310
- # No transfer syntax was accepted for this particular presentation context:
311
- pc[:result] = TRANSFER_SYNTAX_REJECTED
312
- rejected += 1
313
- end
314
- else
315
- # Abstract syntax rejected:
316
- pc[:result] = ABSTRACT_SYNTAX_REJECTED
317
- end
318
- end
319
- return info, approved, rejected
320
- end
321
-
322
- # Sets the default accepted abstract syntaxes and transfer syntaxes for this SCP.
323
- #
324
- def set_default_accepted_syntaxes
325
- @accepted_transfer_syntaxes, @accepted_abstract_syntaxes = LIBRARY.extract_transfer_syntaxes_and_sop_classes
326
- end
327
-
328
- end
329
- end
1
+ module DICOM
2
+
3
+ # This class contains code for setting up a Service Class Provider (SCP),
4
+ # which will act as a simple storage node (a DICOM server that receives images).
5
+ #
6
+ class DServer
7
+ include Logging
8
+
9
+ # Runs the server and takes a block for initializing.
10
+ #
11
+ # @param [Integer] port the network port to be used (defaults to 104)
12
+ # @param [String] path the directory where incoming DICOM files will be stored (defaults to './received/')
13
+ # @param [&block] block a block of code that will be run on the DServer instance, between creation and the launch of the SCP itself
14
+ #
15
+ # @example Run a server instance with a custom file handler
16
+ # require 'dicom'
17
+ # require 'my_file_handler'
18
+ # include DICOM
19
+ # DServer.run(104, 'c:/temp/') do |s|
20
+ # s.timeout = 100
21
+ # s.file_handler = MyFileHandler
22
+ # end
23
+ #
24
+ def self.run(port=104, path='./received/', &block)
25
+ server = DServer.new(port)
26
+ server.instance_eval(&block)
27
+ server.start_scp(path)
28
+ end
29
+
30
+ # A customized FileHandler class to use instead of the default FileHandler included with ruby-dicom.
31
+ attr_accessor :file_handler
32
+ # The hostname that the TCPServer binds to.
33
+ attr_accessor :host
34
+ # The name of the server (application entity).
35
+ attr_accessor :host_ae
36
+ # The maximum allowed size of network packages (in bytes).
37
+ attr_accessor :max_package_size
38
+ # The network port to be used.
39
+ attr_accessor :port
40
+ # The maximum period the server will wait on an answer from a client before aborting the communication.
41
+ attr_accessor :timeout
42
+
43
+ # A hash containing the abstract syntaxes that will be accepted.
44
+ attr_reader :accepted_abstract_syntaxes
45
+ # A hash containing the transfer syntaxes that will be accepted.
46
+ attr_reader :accepted_transfer_syntaxes
47
+
48
+ # Creates a DServer instance.
49
+ #
50
+ # @note To customize logging behaviour, refer to the Logging module documentation.
51
+ #
52
+ # @param [Integer] port the network port to be used
53
+ # @param [Hash] options the options to use for the DICOM server
54
+ # @option options [String] :file_handler a customized FileHandler class to use instead of the default FileHandler
55
+ # @option options [String] :host the hostname that the TCPServer binds to (defaults to '0.0.0.0')
56
+ # @option options [String] :host_ae the name of the server (application entity)
57
+ # @option options [String] :max_package_size the maximum allowed size of network packages (in bytes)
58
+ # @option options [String] :timeout the number of seconds the server will wait on an answer from a client before aborting the communication
59
+ #
60
+ # @example Create a server using default settings
61
+ # s = DICOM::DServer.new
62
+ # @example Create a server with a specific host name and a custom buildt file handler
63
+ # require_relative 'my_file_handler'
64
+ # server = DICOM::DServer.new(104, :host_ae => "RUBY_SERVER", :file_handler => DICOM::MyFileHandler)
65
+ #
66
+ def initialize(port=104, options={})
67
+ require 'socket'
68
+ # Required parameters:
69
+ @port = port
70
+ # Optional parameters (and default values):
71
+ @file_handler = options[:file_handler] || FileHandler
72
+ @host = options[:host] || '0.0.0.0'
73
+ @host_ae = options[:host_ae] || "RUBY_DICOM"
74
+ @max_package_size = options[:max_package_size] || 32768 # 16384
75
+ @timeout = options[:timeout] || 10 # seconds
76
+ @min_length = 12 # minimum number of bytes to expect in an incoming transmission
77
+ # Variables used for monitoring state of transmission:
78
+ @connection = nil # TCP connection status
79
+ @association = nil # DICOM Association status
80
+ @request_approved = nil # Status of our DICOM request
81
+ @release = nil # Status of received, valid release response
82
+ set_default_accepted_syntaxes
83
+ end
84
+
85
+ # Adds an abstract syntax to the list of abstract syntaxes that the server will accept.
86
+ #
87
+ # @param [String] uid an abstract syntax UID
88
+ #
89
+ def add_abstract_syntax(uid)
90
+ lib_uid = LIBRARY.uid(uid)
91
+ raise "Invalid/unknown UID: #{uid}" unless lib_uid
92
+ @accepted_abstract_syntaxes[uid] = lib_uid.name
93
+ end
94
+
95
+ # Adds a transfer syntax to the list of transfer syntaxes that the server will accept.
96
+ #
97
+ # @param [String] uid a transfer syntax UID
98
+ #
99
+ def add_transfer_syntax(uid)
100
+ lib_uid = LIBRARY.uid(uid)
101
+ raise "Invalid/unknown UID: #{uid}" unless lib_uid
102
+ @accepted_transfer_syntaxes[uid] = lib_uid.name
103
+ end
104
+
105
+ # Prints the list of accepted abstract syntaxes to the screen.
106
+ #
107
+ def print_abstract_syntaxes
108
+ # Determine length of longest key to ensure pretty print:
109
+ max_uid = @accepted_abstract_syntaxes.keys.collect{|k| k.length}.max
110
+ puts "Abstract syntaxes which are accepted by this SCP:"
111
+ @accepted_abstract_syntaxes.sort.each do |pair|
112
+ puts "#{pair[0]}#{' '*(max_uid-pair[0].length)} #{pair[1]}"
113
+ end
114
+ end
115
+
116
+ # Prints the list of accepted transfer syntaxes to the screen.
117
+ #
118
+ def print_transfer_syntaxes
119
+ # Determine length of longest key to ensure pretty print:
120
+ max_uid = @accepted_transfer_syntaxes.keys.collect{|k| k.length}.max
121
+ puts "Transfer syntaxes which are accepted by this SCP:"
122
+ @accepted_transfer_syntaxes.sort.each do |pair|
123
+ puts "#{pair[0]}#{' '*(max_uid-pair[0].length)} #{pair[1]}"
124
+ end
125
+ end
126
+
127
+ # Deletes a specific abstract syntax from the list of abstract syntaxes
128
+ # that the server will accept.
129
+ #
130
+ # @param [String] uid an abstract syntax UID
131
+ #
132
+ def delete_abstract_syntax(uid)
133
+ if uid.is_a?(String)
134
+ @accepted_abstract_syntaxes.delete(uid)
135
+ else
136
+ raise "Invalid type of UID. Expected String, got #{uid.class}!"
137
+ end
138
+ end
139
+
140
+ # Deletes a specific transfer syntax from the list of transfer syntaxes
141
+ # that the server will accept.
142
+ #
143
+ # @param [String] uid a transfer syntax UID
144
+ #
145
+ def delete_transfer_syntax(uid)
146
+ if uid.is_a?(String)
147
+ @accepted_transfer_syntaxes.delete(uid)
148
+ else
149
+ raise "Invalid type of UID. Expected String, got #{uid.class}!"
150
+ end
151
+ end
152
+
153
+ # Completely clears the list of abstract syntaxes that the server will accept.
154
+ #
155
+ # Following such a clearance, the user must ensure to add the specific
156
+ # abstract syntaxes that are to be accepted by the server.
157
+ #
158
+ def clear_abstract_syntaxes
159
+ @accepted_abstract_syntaxes = Hash.new
160
+ end
161
+
162
+ # Completely clears the list of transfer syntaxes that the server will accept.
163
+ #
164
+ # Following such a clearance, the user must ensure to add the specific
165
+ # transfer syntaxes that are to be accepted by the server.
166
+ #
167
+ def clear_transfer_syntaxes
168
+ @accepted_transfer_syntaxes = Hash.new
169
+ end
170
+
171
+ # Starts the Service Class Provider (SCP).
172
+ #
173
+ # This service acts as a simple storage node, which receives DICOM files
174
+ # and stores them in the specified folder.
175
+ #
176
+ # Customized storage actions can be set my modifying or replacing the FileHandler class.
177
+ #
178
+ # @param [String] path the directory where incoming files are to be saved
179
+ #
180
+ def start_scp(path='./received/')
181
+ if @accepted_abstract_syntaxes.size > 0 and @accepted_transfer_syntaxes.size > 0
182
+ logger.info("Started DICOM SCP server on port #{@port}.")
183
+ logger.info("Waiting for incoming transmissions...\n\n")
184
+ # Initiate server:
185
+ @scp = TCPServer.new(@host, @port)
186
+ # Use a loop to listen for incoming messages:
187
+ loop do
188
+ Thread.start(@scp.accept) do |session|
189
+ # Initialize the network package handler for this session:
190
+ link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :file_handler => @file_handler)
191
+ link.set_session(session)
192
+ # Note who has contacted us:
193
+ logger.info("Connection established with: #{session.peeraddr[2]} (IP: #{session.peeraddr[3]})")
194
+ # Receive an incoming message:
195
+ segments = link.receive_multiple_transmissions
196
+ info = segments.first
197
+ # Interpret the received message:
198
+ if info[:valid]
199
+ association_error = check_association_request(info)
200
+ unless association_error
201
+ info, approved, rejected = process_syntax_requests(info)
202
+ link.handle_association_accept(info)
203
+ context = (LIBRARY.uid(info[:pc].first[:abstract_syntax]) ? LIBRARY.uid(info[:pc].first[:abstract_syntax]).name : 'Unknown UID!')
204
+ if approved > 0
205
+ if approved == 1
206
+ logger.info("Accepted the association request with context: #{context}")
207
+ else
208
+ if rejected == 0
209
+ logger.info("Accepted all #{approved} proposed contexts in the association request.")
210
+ else
211
+ logger.warn("Accepted only #{approved} of #{approved+rejected} of the proposed contexts in the association request.")
212
+ end
213
+ end
214
+ # Process the incoming data. This method will also take care of releasing the association:
215
+ success, messages = link.handle_incoming_data(path)
216
+ # Pass along any messages that has been recorded:
217
+ messages.each { |m| logger.public_send(m.first, m.last) } if messages.first
218
+ else
219
+ # No abstract syntaxes in the incoming request were accepted:
220
+ if rejected == 1
221
+ logger.warn("Rejected the association request with proposed context: #{context}")
222
+ else
223
+ logger.warn("Rejected all #{rejected} proposed contexts in the association request.")
224
+ end
225
+ # Since the requested abstract syntax was not accepted, the association must be released.
226
+ link.await_release
227
+ end
228
+ else
229
+ # The incoming association was not formally correct.
230
+ link.handle_rejection
231
+ end
232
+ else
233
+ # The incoming message was not recognised as a valid DICOM message. Abort:
234
+ link.handle_abort
235
+ end
236
+ # Terminate the connection:
237
+ link.stop_session
238
+ logger.info("Connection closed.\n\n")
239
+ end
240
+ end
241
+ else
242
+ raise "Unable to start SCP server as no accepted abstract syntaxes have been set!" if @accepted_abstract_syntaxes.length == 0
243
+ raise "Unable to start SCP server as no accepted transfer syntaxes have been set!" if @accepted_transfer_syntaxes.length == 0
244
+ end
245
+ end
246
+
247
+
248
+ private
249
+
250
+
251
+ # Checks if the association request is formally correct, by matching against an exact application context UID.
252
+ # Returns nil if valid, and an error code if it is not approved.
253
+ #
254
+ # === Notes
255
+ #
256
+ # Other things can potentially be checked here too, if we want to make the server more strict with regards to what information is received:
257
+ # * Application context name, calling AE title, called AE title
258
+ # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.4 (Table 9-21).
259
+ #
260
+ # === Parameters
261
+ #
262
+ # * <tt>info</tt> -- An information hash from the received association request.
263
+ #
264
+ def check_association_request(info)
265
+ unless info[:application_context] == APPLICATION_CONTEXT
266
+ error = 2 # (application context name not supported)
267
+ logger.error("The application context in the incoming association request was not recognized: (#{info[:application_context]})")
268
+ else
269
+ error = nil
270
+ end
271
+ return error
272
+ end
273
+
274
+ # Checks if the requested abstract syntax & its transfer syntax(es) are supported by this server instance,
275
+ # and inserts a corresponding result code for each presentation context.
276
+ # Returns the modified association information hash, as well as the number of abstract syntaxes that were accepted and rejected.
277
+ #
278
+ # === Notes
279
+ #
280
+ # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.3.2 (Table 9-18).
281
+ #
282
+ # === Parameters
283
+ #
284
+ # * <tt>info</tt> -- An information hash from the received association request.
285
+ #
286
+ def process_syntax_requests(info)
287
+ # A couple of variables used to analyse the properties of the association:
288
+ approved = 0
289
+ rejected = 0
290
+ # Loop through the presentation contexts:
291
+ info[:pc].each do |pc|
292
+ if @accepted_abstract_syntaxes[pc[:abstract_syntax]]
293
+ # Abstract syntax accepted. Proceed to check its transfer syntax(es):
294
+ proposed_transfer_syntaxes = pc[:ts].collect{|t| t[:transfer_syntax]}.sort
295
+ # Choose the first proposed transfer syntax that exists in our list of accepted transfer syntaxes:
296
+ accepted_transfer_syntax = nil
297
+ proposed_transfer_syntaxes.each do |proposed_ts|
298
+ if @accepted_transfer_syntaxes.include?(proposed_ts)
299
+ accepted_transfer_syntax = proposed_ts
300
+ break
301
+ end
302
+ end
303
+ if accepted_transfer_syntax
304
+ # Both abstract and transfer syntax has been approved:
305
+ pc[:result] = ACCEPTANCE
306
+ pc[:selected_transfer_syntax] = accepted_transfer_syntax
307
+ # Update our status variables:
308
+ approved += 1
309
+ else
310
+ # No transfer syntax was accepted for this particular presentation context:
311
+ pc[:result] = TRANSFER_SYNTAX_REJECTED
312
+ rejected += 1
313
+ end
314
+ else
315
+ # Abstract syntax rejected:
316
+ pc[:result] = ABSTRACT_SYNTAX_REJECTED
317
+ end
318
+ end
319
+ return info, approved, rejected
320
+ end
321
+
322
+ # Sets the default accepted abstract syntaxes and transfer syntaxes for this SCP.
323
+ #
324
+ def set_default_accepted_syntaxes
325
+ @accepted_transfer_syntaxes, @accepted_abstract_syntaxes = LIBRARY.extract_transfer_syntaxes_and_sop_classes
326
+ end
327
+
328
+ end
329
+ end