dicom 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,121 @@
1
+ # Copyright 2010 Christoffer Lervag
2
+
3
+ # This file contains module constants used by the Ruby DICOM library.
4
+
5
+ module DICOM
6
+
7
+ # Ruby DICOM version string.
8
+ VERSION = "0.8"
9
+
10
+ # Ruby DICOM's implementation UID.
11
+ UID = "1.2.826.0.1.3680043.8.641"
12
+ # Ruby DICOM name & version (max 16 characters).
13
+ NAME = "RUBY_DCM_" + DICOM::VERSION
14
+ # Application title.
15
+ SOURCE_APP_TITLE = "RUBY_DICOM"
16
+
17
+ # Item tag.
18
+ ITEM_TAG = "FFFE,E000"
19
+ # All Item related tags (includes both types of delimitation items).
20
+ ITEM_TAGS = ["FFFE,E000", "FFFE,E00D", "FFFE,E0DD"]
21
+ # Item delimiter tag.
22
+ ITEM_DELIMITER = "FFFE,E00D"
23
+ # Sequence delimiter tag.
24
+ SEQUENCE_DELIMITER = "FFFE,E0DD"
25
+ # All delimiter tags.
26
+ DELIMITER_TAGS = ["FFFE,E00D", "FFFE,E0DD"]
27
+
28
+ # The VR used for the item elements.
29
+ ITEM_VR = " "
30
+
31
+ # Pixel tag.
32
+ PIXEL_TAG = "7FE0,0010"
33
+ # Name of the pixel tag when holding encapsulated data.
34
+ ENCAPSULATED_PIXEL_NAME = "Encapsulated Pixel Data"
35
+ # Name of encapsulated items.
36
+ PIXEL_ITEM_NAME = "Pixel Data Item"
37
+
38
+ # File meta group.
39
+ META_GROUP = "0002"
40
+
41
+ # Group length element.
42
+ GROUP_LENGTH = "0000"
43
+
44
+ # Implicit, little endian (the default transfer syntax).
45
+ IMPLICIT_LITTLE_ENDIAN = "1.2.840.10008.1.2"
46
+ # Explicit, little endian transfer syntax.
47
+ EXPLICIT_LITTLE_ENDIAN = "1.2.840.10008.1.2.1"
48
+ # Explicit, big endian transfer syntax.
49
+ EXPLICIT_BIG_ENDIAN = "1.2.840.10008.1.2.2"
50
+
51
+ # Verification SOP class UID.
52
+ VERIFICATION_SOP = "1.2.840.10008.1.1"
53
+ # Application context SOP class UID.
54
+ APPLICATION_CONTEXT = "1.2.840.10008.3.1.1.1"
55
+
56
+ # Network transmission successful.
57
+ SUCCESS = 0
58
+ # Network proposition accepted.
59
+ ACCEPTANCE = 0
60
+ # Presentation context rejected by abstract syntax.
61
+ ABSTRACT_SYNTAX_REJECTED = 3
62
+ # Presentation context rejected by transfer syntax.
63
+ TRANSFER_SYNTAX_REJECTED = 4
64
+
65
+ # Some network command element codes:
66
+ C_STORE_RQ = 1 # (encodes to 0001H as US)
67
+ C_GET_RQ = 16 # (encodes to 0010H as US)
68
+ C_FIND_RQ = 32 # (encodes to 0020H as US)
69
+ C_MOVE_RQ = 33 # (encodes to 0021H as US)
70
+ C_ECHO_RQ = 48 # (encodes to 0030 as US)
71
+ C_CANCEL_RQ = 4095 # (encodes to 0FFFH as US)
72
+ C_STORE_RSP = 32769 # (encodes to 8001H as US)
73
+ C_GET_RSP = 32784 # (encodes to 8010H as US)
74
+ C_FIND_RSP = 32800 # (encodes to 8020H as US)
75
+ C_MOVE_RSP = 32801 # (encodes to 8021H as US)
76
+ C_ECHO_RSP = 32816 # (encodes to 8030H as US)
77
+ NO_DATA_SET_PRESENT = 257 # (encodes to 0101H as US)
78
+ DATA_SET_PRESENT = 1
79
+ DEFAULT_MESSAGE_ID = 1
80
+
81
+ # The network communication flags:
82
+ DATA_MORE_FRAGMENTS = "00"
83
+ COMMAND_MORE_FRAGMENTS = "01"
84
+ DATA_LAST_FRAGMENT = "02"
85
+ COMMAND_LAST_FRAGMENT = "03"
86
+
87
+ # Network communication PDU types:
88
+ PDU_ASSOCIATION_REQUEST = "01"
89
+ PDU_ASSOCIATION_ACCEPT = "02"
90
+ PDU_ASSOCIATION_REJECT = "03"
91
+ PDU_DATA = "04"
92
+ PDU_RELEASE_REQUEST = "05"
93
+ PDU_RELEASE_RESPONSE = "06"
94
+ PDU_ABORT = "07"
95
+
96
+ # Network communication item types:
97
+ ITEM_APPLICATION_CONTEXT = "10"
98
+ ITEM_PRESENTATION_CONTEXT_REQUEST = "20"
99
+ ITEM_PRESENTATION_CONTEXT_RESPONSE = "21"
100
+ ITEM_ABSTRACT_SYNTAX = "30"
101
+ ITEM_TRANSFER_SYNTAX = "40"
102
+ ITEM_USER_INFORMATION = "50"
103
+ ITEM_MAX_LENGTH = "51"
104
+ ITEM_IMPLEMENTATION_UID = "52"
105
+ ITEM_MAX_OPERATIONS_INVOKED = "53"
106
+ ITEM_ROLE_NEGOTIATION = "54"
107
+ ITEM_IMPLEMENTATION_VERSION = "55"
108
+
109
+ # Varaibles used to determine endianness.
110
+ x = 0xdeadbeef
111
+ endian_type = {
112
+ Array(x).pack("V*") => false, # Little
113
+ Array(x).pack("N*") => true # Big
114
+ }
115
+ # System (CPU) Endianness.
116
+ CPU_ENDIAN = endian_type[Array(x).pack("L*")]
117
+
118
+ # Ruby DICOM's library (data dictionary).
119
+ LIBRARY = DICOM::DLibrary.new
120
+
121
+ end
@@ -0,0 +1,888 @@
1
+ # Copyright 2009-2010 Christoffer Lervag
2
+
3
+ module DICOM
4
+
5
+ # This class contains code for handling the client side of DICOM TCP/IP network communication.
6
+ #
7
+ #--
8
+ # 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?
9
+ #
10
+ class DClient
11
+
12
+ # The name of this client (application entity).
13
+ attr_accessor :ae
14
+ # The name of the server (application entity).
15
+ attr_accessor :host_ae
16
+ # The IP adress of the server.
17
+ attr_accessor :host_ip
18
+ # The maximum allowed size of network packages (in bytes).
19
+ attr_accessor :max_package_size
20
+ # The network port to be used.
21
+ attr_accessor :port
22
+ # The maximum period the client will wait on an answer from a server before aborting the communication.
23
+ attr_accessor :timeout
24
+ # A boolean which defines if notices/warnings/errors will be printed to the screen (true) or not (false).
25
+ attr_accessor :verbose
26
+ # An array, where each index contains a hash with the data elements received in a command response (with tags as keys).
27
+ attr_reader :command_results
28
+ # An array, where each index contains a hash with the data elements received in a data response (with tags as keys).
29
+ attr_reader :data_results
30
+ # An array containing any error messages recorded.
31
+ attr_reader :errors
32
+ # An array containing any status messages recorded.
33
+ attr_reader :notices
34
+
35
+ # Creates a DClient instance.
36
+ #
37
+ # === Parameters
38
+ #
39
+ # * <tt>host_ip</tt> -- String. The IP adress of the server which you are going to communicate with.
40
+ # * <tt>port</tt> -- Fixnum. The network port to be used.
41
+ # * <tt>options</tt> -- A hash of parameters.
42
+ #
43
+ # === Options
44
+ #
45
+ # * <tt>:ae</tt> -- String. The name of this client (application entity).
46
+ # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
47
+ # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
48
+ # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
49
+ # * <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.
50
+ #
51
+ # === Examples
52
+ #
53
+ # # Create a client instance using default settings:
54
+ # node = DICOM::DClient.new("10.1.25.200", 104)
55
+ #
56
+ def initialize(host_ip, port, options={})
57
+ require 'socket'
58
+ # Required parameters:
59
+ @host_ip = host_ip
60
+ @port = port
61
+ # Optional parameters (and default values):
62
+ @ae = options[:ae] || "RUBY_DICOM"
63
+ @host_ae = options[:host_ae] || "DEFAULT"
64
+ @max_package_size = options[:max_package_size] || 32768 # 16384
65
+ @timeout = options[:timeout] || 10 # seconds
66
+ @min_length = 12 # minimum number of bytes to expect in an incoming transmission
67
+ @verbose = options[:verbose]
68
+ @verbose = true if @verbose == nil # Default verbosity is 'on'.
69
+ # Other instance variables:
70
+ @errors = Array.new # errors and warnings are put in this array
71
+ @notices = Array.new # information on successful transmissions are put in this array
72
+ # Variables used for monitoring state of transmission:
73
+ @association = nil # DICOM Association status
74
+ @request_approved = nil # Status of our DICOM request
75
+ @release = nil # Status of received, valid release response
76
+ # Results from a query:
77
+ @command_results = Array.new
78
+ @data_results = Array.new
79
+ # Set default values like transfer syntax, user information, endianness:
80
+ set_default_values
81
+ set_user_information_array
82
+ # Initialize the network package handler:
83
+ @link = Link.new(:ae => @ae, :host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout)
84
+ end
85
+
86
+ # Tests the connection to the server by performing a C-ECHO.
87
+ #
88
+ def echo
89
+ # Verification SOP Class:
90
+ @abstract_syntaxes = [VERIFICATION_SOP]
91
+ perform_echo
92
+ end
93
+
94
+ # Queries a service class provider for images that match the specified criteria.
95
+ #
96
+ # === Parameters
97
+ #
98
+ # * <tt>options</tt> -- A hash of parameters.
99
+ #
100
+ # === Options
101
+ #
102
+ # * <tt>"0008,0018"</tt> -- SOP Instance UID
103
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
104
+ # * <tt>"0020,000D"</tt> -- Study Instance UID
105
+ # * <tt>"0020,000E"</tt> -- Series Instance UID
106
+ # * <tt>"0020,0013"</tt> -- Instance Number
107
+ #
108
+ # === Examples
109
+ #
110
+ # node.find_images("0020,000D" => "1.2.840.1145.342", "0020,000E" => "1.3.6.1.4.1.2452.6.687844")
111
+ #
112
+ def find_images(options={})
113
+ # Study Root Query/Retrieve Information Model - FIND:
114
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.1"]
115
+ # Prepare data elements for this operation:
116
+ set_data_fragment_find_images
117
+ set_data_options(options)
118
+ perform_find
119
+ return @data_results
120
+ end
121
+
122
+ # Queries a service class provider for patients that match the specified criteria.
123
+ #
124
+ # === Parameters
125
+ #
126
+ # * <tt>options</tt> -- A hash of parameters.
127
+ #
128
+ # === Options
129
+ #
130
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
131
+ # * <tt>"0010,0010"</tt> -- Patient's Name
132
+ # * <tt>"0010,0020"</tt> -- Patient ID
133
+ # * <tt>"0010,0030"</tt> -- Patient's Birth Date
134
+ # * <tt>"0010,0040"</tt> -- Patient's Sex
135
+ #
136
+ # === Examples
137
+ #
138
+ # node.find_patients("0010,0010" => "James*")
139
+ #
140
+ def find_patients(options={})
141
+ # Patient Root Query/Retrieve Information Model - FIND:
142
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.1.1"]
143
+ # Prepare data elements for this operation:
144
+ set_data_fragment_find_patients
145
+ set_data_options(options)
146
+ perform_find
147
+ return @data_results
148
+ end
149
+
150
+ # Queries a service class provider for series that match the specified criteria.
151
+ #
152
+ # === Parameters
153
+ #
154
+ # * <tt>options</tt> -- A hash of parameters.
155
+ #
156
+ # === Options
157
+ #
158
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
159
+ # * <tt>"0008,0060"</tt> -- Modality
160
+ # * <tt>"0008,103E"</tt> -- Series Description
161
+ # * <tt>"0020,000D"</tt> -- Study Instance UID
162
+ # * <tt>"0020,000E"</tt> -- Series Instance UID
163
+ # * <tt>"0020,0011"</tt> -- Series Number
164
+ #
165
+ # === Examples
166
+ #
167
+ # node.find_series("0020,000D" => "1.2.840.1145.342")
168
+ #
169
+ def find_series(options={})
170
+ # Study Root Query/Retrieve Information Model - FIND:
171
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.1"]
172
+ # Prepare data elements for this operation:
173
+ set_data_fragment_find_series
174
+ set_data_options(options)
175
+ perform_find
176
+ return @data_results
177
+ end
178
+
179
+ # Queries a service class provider for studies that match the specified criteria.
180
+ #
181
+ # === Parameters
182
+ #
183
+ # * <tt>options</tt> -- A hash of parameters.
184
+ #
185
+ # === Options
186
+ #
187
+ # * <tt>"0008,0020"</tt> -- Study Date
188
+ # * <tt>"0008,0030"</tt> -- Study Time
189
+ # * <tt>"0008,0050"</tt> -- Accession Number
190
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
191
+ # * <tt>"0008,0090"</tt> -- Referring Physician's Name
192
+ # * <tt>"0008,1030"</tt> -- Study Description
193
+ # * <tt>"0008,1060"</tt> -- Name of Physician(s) Reading Study
194
+ # * <tt>"0010,0010"</tt> -- Patient's Name
195
+ # * <tt>"0010,0020"</tt> -- Patient ID
196
+ # * <tt>"0010,0030"</tt> -- Patient's Birth Date
197
+ # * <tt>"0010,0040"</tt> -- Patient's Sex
198
+ # * <tt>"0020,000D"</tt> -- Study Instance UID
199
+ # * <tt>"0020,0010"</tt> -- Study ID
200
+ #
201
+ # === Examples
202
+ #
203
+ # node.find_studies("0008,0020" => "20090604-", "0010,000D" => "123456789")
204
+ #
205
+ def find_studies(options={})
206
+ # Study Root Query/Retrieve Information Model - FIND:
207
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.1"]
208
+ # Prepare data elements for this operation:
209
+ set_data_fragment_find_studies
210
+ set_data_options(options)
211
+ perform_find
212
+ return @data_results
213
+ end
214
+
215
+ # Retrieves a DICOM file from a service class provider (SCP/PACS).
216
+ #
217
+ # === Restrictions
218
+ #
219
+ # * This method has never actually been tested, and as such, it is probably not working! Feedback is welcome.
220
+ #
221
+ # === Parameters
222
+ #
223
+ # * <tt>path</tt> -- The path where incoming files will be saved.
224
+ # * <tt>options</tt> -- A hash of parameters.
225
+ #
226
+ # === Options
227
+ #
228
+ # * <tt>"0008,0018"</tt> -- SOP Instance UID
229
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
230
+ # * <tt>"0020,000D"</tt> -- Study Instance UID
231
+ # * <tt>"0020,000E"</tt> -- Series Instance UID
232
+ #
233
+ # === Examples
234
+ #
235
+ # node.get_image("c:/dicom/", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
236
+ #
237
+ def get_image(path, options={})
238
+ # Study Root Query/Retrieve Information Model - GET:
239
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.3"]
240
+ # Transfer the current options to the data_elements hash:
241
+ set_command_fragment_get
242
+ # Prepare data elements for this operation:
243
+ set_data_fragment_get_image
244
+ set_data_options(options)
245
+ perform_get(path)
246
+ end
247
+
248
+ # Moves a single image to a DICOM server.
249
+ #
250
+ # === Notes
251
+ #
252
+ # * This DICOM node must be a third party (not yourself).
253
+ #
254
+ # === Parameters
255
+ #
256
+ # * <tt>destination</tt> -- String. The name (AE title) of the DICOM server which will receive the file.
257
+ # * <tt>options</tt> -- A hash of parameters.
258
+ #
259
+ # === Options
260
+ #
261
+ # * <tt>"0008,0018"</tt> -- SOP Instance UID
262
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
263
+ # * <tt>"0020,000D"</tt> -- Study Instance UID
264
+ # * <tt>"0020,000E"</tt> -- Series Instance UID
265
+ #
266
+ # === Examples
267
+ #
268
+ # node.move_image("SOME_SERVER", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
269
+ #
270
+ def move_image(destination, options={})
271
+ # Study Root Query/Retrieve Information Model - MOVE:
272
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.2"]
273
+ # Transfer the current options to the data_elements hash:
274
+ set_command_fragment_move(destination)
275
+ # Prepare data elements for this operation:
276
+ set_data_fragment_move_image
277
+ set_data_options(options)
278
+ perform_move
279
+ end
280
+
281
+ # Move an entire study to a DICOM server.
282
+ #
283
+ # === Notes
284
+ #
285
+ # * This DICOM node must be a third party (not yourself).
286
+ #
287
+ # === Parameters
288
+ #
289
+ # * <tt>destination</tt> -- String. The name (AE title) of the DICOM server which will receive the files.
290
+ # * <tt>options</tt> -- A hash of parameters.
291
+ #
292
+ # === Options
293
+ #
294
+ # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
295
+ # * <tt>"0010,0020"</tt> -- Patient ID
296
+ # * <tt>"0020,000D"</tt> -- Study Instance UID
297
+ #
298
+ # === Examples
299
+ #
300
+ # node.move_study("SOME_SERVER", "0010,0020" => pat_id, "0020,000D" => study_uid)
301
+ #
302
+ def move_study(destination, options={})
303
+ # Study Root Query/Retrieve Information Model - MOVE:
304
+ @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.2"]
305
+ # Transfer the current options to the data_elements hash:
306
+ set_command_fragment_move(destination)
307
+ # Prepare data elements for this operation:
308
+ set_data_fragment_move_study
309
+ set_data_options(options)
310
+ perform_move
311
+ end
312
+
313
+ # Sends one or more DICOM files to a service class provider (SCP/PACS).
314
+ #
315
+ # === Parameters
316
+ #
317
+ # * <tt>files</tt> -- A single file path or an array of paths, or a DObject or an array of DObject instances.
318
+ #
319
+ # === Examples
320
+ #
321
+ # node.send("my_file.dcm")
322
+ #
323
+ def send(files)
324
+ # Prepare the DICOM object(s):
325
+ objects, @abstract_syntaxes, success, message = load_files(files)
326
+ if success
327
+ # Open a DICOM link:
328
+ establish_association
329
+ if @association
330
+ if @request_approved
331
+ # Continue with our c-store operation, since our request was accepted.
332
+ # Handle the transmission:
333
+ perform_send(objects)
334
+ end
335
+ end
336
+ # Close the DICOM link:
337
+ establish_release
338
+ else
339
+ # Failed when loading the specified parameter as DICOM file(s). Will not transmit.
340
+ add_error(message)
341
+ end
342
+ end
343
+
344
+ # Tests the connection to the server in a very simple way by negotiating an association and then releasing it.
345
+ #
346
+ def test
347
+ add_notice("TESTING CONNECTION...")
348
+ success = false
349
+ # Verification SOP Class:
350
+ @abstract_syntaxes = [VERIFICATION_SOP]
351
+ # Open a DICOM link:
352
+ establish_association
353
+ if @association
354
+ if @request_approved
355
+ success = true
356
+ end
357
+ # Close the DICOM link:
358
+ establish_release
359
+ end
360
+ if success
361
+ add_notice("TEST SUCCSESFUL!")
362
+ else
363
+ add_error("TEST FAILED!")
364
+ end
365
+ return success
366
+ end
367
+
368
+
369
+ # Following methods are private:
370
+ private
371
+
372
+
373
+ # Adds a warning or error message to the instance array holding messages,
374
+ # and prints the information to the screen if verbose is set.
375
+ #
376
+ # === Parameters
377
+ #
378
+ # * <tt>error</tt> -- A single error message or an array of error messages.
379
+ #
380
+ def add_error(error)
381
+ puts error if @verbose
382
+ @errors << error
383
+ end
384
+
385
+ # Adds a notice (information regarding progress or successful communications) to the instance array,
386
+ # and prints the information to the screen if verbose is set.
387
+ #
388
+ # === Parameters
389
+ #
390
+ # * <tt>notice</tt> -- A single status message or an array of status messages.
391
+ #
392
+ def add_notice(notice)
393
+ puts notice if @verbose
394
+ @notices << notice
395
+ end
396
+
397
+ # Opens a TCP session with the server, and handles the association request as well as the response.
398
+ #
399
+ def establish_association
400
+ # Reset some variables:
401
+ @association = false
402
+ @request_approved = false
403
+ # Initiate the association:
404
+ @link.build_association_request(@application_context_uid, @abstract_syntaxes, @transfer_syntax, @user_information)
405
+ @link.start_session(@host_ip, @port)
406
+ @link.transmit
407
+ info = @link.receive_multiple_transmissions.first
408
+ # Interpret the results:
409
+ if info[:valid]
410
+ if info[:pdu] == PDU_ASSOCIATION_ACCEPT
411
+ # Values of importance are extracted and put into instance variables:
412
+ @association = true
413
+ @max_pdu_length = info[:max_pdu_length]
414
+ add_notice("Association successfully negotiated with host #{@host_ae} (#{@host_ip}).")
415
+ # Check if all our presentation contexts was accepted by the host:
416
+ process_presentation_context_response(info[:pc])
417
+ else
418
+ add_error("Association was denied from host #{@host_ae} (#{@host_ip})!")
419
+ end
420
+ end
421
+ end
422
+
423
+ # Handles the release request along with the response, as well as closing the TCP connection.
424
+ #
425
+ def establish_release
426
+ @release = false
427
+ if @abort
428
+ @link.stop_session
429
+ add_notice("Association has been closed. (#{@host_ae}, #{@host_ip})")
430
+ else
431
+ unless @link.session.closed?
432
+ @link.build_release_request
433
+ @link.transmit
434
+ info = @link.receive_single_transmission.first
435
+ @link.stop_session
436
+ if info[:pdu] == PDU_RELEASE_RESPONSE
437
+ add_notice("Association released properly from host #{@host_ae}.")
438
+ else
439
+ add_error("Association released from host #{@host_ae}, but a release response was not registered.")
440
+ end
441
+ else
442
+ add_error("Connection was closed by the host (for some unknown reason) before the association could be released properly.")
443
+ end
444
+ end
445
+ @abort = false
446
+ end
447
+
448
+ # Loads one or more DICOM files.
449
+ # Returns an array of DObject instances, an array of unique abstract syntaxes found among the files, a status boolean and a message string.
450
+ #
451
+ # === Parameters
452
+ #
453
+ # * <tt>files</tt> -- A single file path or an array of paths, or a DObject or an array of DObject instances.
454
+ #
455
+ def load_files(files)
456
+ status = true
457
+ message = ""
458
+ objects = Array.new
459
+ abstracts = Array.new
460
+ files = [files] unless files.is_a?(Array)
461
+ files.each do |file|
462
+ if file.is_a?(String)
463
+ obj = DObject.new(file, :verbose => false)
464
+ if obj.read_success
465
+ # Load the DICOM object and its abstract syntax:
466
+ objects << obj
467
+ abstracts << obj.value("0008,0016")
468
+ else
469
+ status = false
470
+ message = "Failed to successfully parse a DObject for the following string: #{file}"
471
+ end
472
+ elsif file.is_a?(DObject)
473
+ # Load the DICOM object and its abstract syntax:
474
+ objects << obj
475
+ abstracts << obj.value("0008,0016")
476
+ else
477
+ status = false
478
+ message = "Array contains invalid object #{file}."
479
+ end
480
+ end
481
+ return objects, abstracts.uniq, status, message
482
+ end
483
+
484
+ # Handles the communication involved in a DICOM C-ECHO.
485
+ # Build the necessary strings and send the command and data element that makes up the echo request.
486
+ # Listens for and interpretes the incoming echo response.
487
+ #
488
+ def perform_echo
489
+ # Open a DICOM link:
490
+ establish_association
491
+ if @association
492
+ if @request_approved
493
+ # Continue with our echo, since the request was accepted.
494
+ # Set the query command elements array:
495
+ set_command_fragment_echo
496
+ presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
497
+ @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
498
+ @link.transmit
499
+ # Listen for incoming responses and interpret them individually, until we have received the last command fragment.
500
+ segments = @link.receive_multiple_transmissions
501
+ process_returned_data(segments)
502
+ # Print stuff to screen?
503
+ end
504
+ # Close the DICOM link:
505
+ establish_release
506
+ end
507
+ end
508
+
509
+ # Handles the communication involved in a DICOM query (C-FIND).
510
+ # Build the necessary strings and send the command and data element that makes up the query.
511
+ # Listens for and interpretes the incoming query responses.
512
+ #
513
+ def perform_find
514
+ # Open a DICOM link:
515
+ establish_association
516
+ if @association
517
+ if @request_approved
518
+ # Continue with our query, since the request was accepted.
519
+ # Set the query command elements array:
520
+ set_command_fragment_find
521
+ presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
522
+ @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
523
+ @link.transmit
524
+ @link.build_data_fragment(@data_elements, presentation_context_id)
525
+ @link.transmit
526
+ # A query response will typically be sent in multiple, separate packets.
527
+ # Listen for incoming responses and interpret them individually, until we have received the last command fragment.
528
+ segments = @link.receive_multiple_transmissions
529
+ process_returned_data(segments)
530
+ end
531
+ # Close the DICOM link:
532
+ establish_release
533
+ end
534
+ end
535
+
536
+ # Handles the communication involved in a DICOM C-GET.
537
+ # Builds and sends command & data fragment, then receives the incoming file data.
538
+ #
539
+ #--
540
+ # FIXME: This method has never actually been tested, since it is difficult to find a host that accepts a c-get-rq.
541
+ #
542
+ def perform_get(path)
543
+ # Open a DICOM link:
544
+ establish_association
545
+ if @association
546
+ if @request_approved
547
+ # Continue with our operation, since the request was accepted.
548
+ presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
549
+ @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
550
+ @link.transmit
551
+ @link.build_data_fragment(@data_elements, presentation_context_id)
552
+ @link.transmit
553
+ # Listen for incoming file data:
554
+ success = @link.handle_incoming_data(path)
555
+ if success
556
+ # Send confirmation response:
557
+ @link.handle_response
558
+ end
559
+ end
560
+ # Close the DICOM link:
561
+ establish_release
562
+ end
563
+ end
564
+
565
+ # Handles the communication involved in DICOM C-MOVE.
566
+ # Build the necessary strings and sends the command element that makes up the move request.
567
+ # Listens for and interpretes the incoming move response.
568
+ #
569
+ def perform_move
570
+ # Open a DICOM link:
571
+ establish_association
572
+ if @association
573
+ if @request_approved
574
+ # Continue with our operation, since the request was accepted.
575
+ presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
576
+ @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
577
+ @link.transmit
578
+ @link.build_data_fragment(@data_elements, presentation_context_id)
579
+ @link.transmit
580
+ # Receive confirmation response:
581
+ segments = @link.receive_single_transmission
582
+ process_returned_data(segments)
583
+ end
584
+ # Close the DICOM link:
585
+ establish_release
586
+ end
587
+ end
588
+
589
+ # Handles the communication involved in DICOM C-STORE.
590
+ # For each file, builds and sends command fragment, then builds and sends the data fragments that
591
+ # conveys the information from the selected DICOM file.
592
+ #
593
+ def perform_send(objects)
594
+ objects.each_with_index do |obj, index|
595
+ # Gather necessary information from the object (SOP Class & Instance UID):
596
+ modality = obj.value("0008,0016")
597
+ instance = obj.value("0008,0018")
598
+ if modality and instance
599
+ # Only send the image if its modality has been accepted by the receiver:
600
+ if @approved_syntaxes[modality]
601
+ # Set the command array to be used:
602
+ message_id = index + 1
603
+ set_command_fragment_store(modality, instance, message_id)
604
+ # Find context id and transfer syntax:
605
+ presentation_context_id = @approved_syntaxes[modality][0]
606
+ selected_transfer_syntax = @approved_syntaxes[modality][1]
607
+ # Encode our DICOM object to a binary string which is split up in pieces, sufficiently small to fit within the specified maximum pdu length:
608
+ # Set the transfer syntax of the DICOM object equal to the one accepted by the SCP:
609
+ obj.transfer_syntax = selected_transfer_syntax
610
+ max_header_length = 14
611
+ data_packages = obj.encode_segments(@max_pdu_length - max_header_length)
612
+ @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
613
+ @link.transmit
614
+ # Transmit all but the last data strings:
615
+ last_data_package = data_packages.pop
616
+ data_packages.each do |data_package|
617
+ @link.build_storage_fragment(PDU_DATA, presentation_context_id, DATA_MORE_FRAGMENTS, data_package)
618
+ @link.transmit
619
+ end
620
+ # Transmit the last data string:
621
+ @link.build_storage_fragment(PDU_DATA, presentation_context_id, DATA_LAST_FRAGMENT, last_data_package)
622
+ @link.transmit
623
+ # Receive confirmation response:
624
+ segments = @link.receive_single_transmission
625
+ process_returned_data(segments)
626
+ end
627
+ else
628
+ 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.")
629
+ end
630
+ end
631
+ end
632
+
633
+ # Processes the presentation contexts that are received in the association response
634
+ # to extract the transfer syntaxes which have been accepted for the various abstract syntaxes.
635
+ #
636
+ # === Parameters
637
+ #
638
+ # * <tt>presentation_contexts</tt> -- An array where each index contains a presentation context hash.
639
+ #
640
+ def process_presentation_context_response(presentation_contexts)
641
+ # Storing approved syntaxes in an Hash with the syntax as key and the value being an array with presentation context ID and the transfer syntax chosen by the SCP.
642
+ @approved_syntaxes = Hash.new
643
+ rejected = Hash.new
644
+ # Reset the presentation context instance variable:
645
+ @link.presentation_contexts = Hash.new
646
+ presentation_contexts.each do |pc|
647
+ # Determine what abstract syntax this particular presentation context's id corresponds to:
648
+ id = pc[:presentation_context_id]
649
+ 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.
650
+ index = (id-1)/2
651
+ abstract_syntax = @abstract_syntaxes[index]
652
+ if pc[:result] == 0
653
+ @approved_syntaxes[abstract_syntax] = [id, pc[:transfer_syntax]]
654
+ @link.presentation_contexts[id] = pc[:transfer_syntax]
655
+ else
656
+ rejected[abstract_syntax] = [id, pc[:transfer_syntax]]
657
+ end
658
+ end
659
+ if rejected.length == 0
660
+ @request_approved = true
661
+ if @approved_syntaxes.length == 1
662
+ add_notice("The presentation context was accepted by host #{@host_ae}.")
663
+ else
664
+ add_notice("All #{@approved_syntaxes.length} presentation contexts were accepted by host #{@host_ae} (#{@host_ip}).")
665
+ end
666
+ else
667
+ @request_approved = false
668
+ add_error("One or more of your presentation contexts were denied by host #{@host_ae}!")
669
+ @approved_syntaxes.each_key {|a| add_error("APPROVED: #{LIBRARY.get_syntax_description(a)}")}
670
+ rejected.each_key {|r| add_error("REJECTED: #{LIBRARY.get_syntax_description(r)}")}
671
+ end
672
+ end
673
+
674
+ # Processes the array of information hashes that was returned from the interaction with the SCP
675
+ # and transfers it to the instance variables where command and data results are stored.
676
+ #
677
+ def process_returned_data(segments)
678
+ # Reset command results arrays:
679
+ @command_results = Array.new
680
+ @data_results = Array.new
681
+ # Try to extract data:
682
+ segments.each do |info|
683
+ if info[:valid]
684
+ # Determine if it is command or data:
685
+ if info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
686
+ @command_results << info[:results]
687
+ elsif info[:presentation_context_flag] == DATA_LAST_FRAGMENT
688
+ @data_results << info[:results]
689
+ end
690
+ end
691
+ end
692
+ end
693
+
694
+ # Sets the command elements used in a C-ECHO-RQ.
695
+ #
696
+ def set_command_fragment_echo
697
+ @command_elements = [
698
+ ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
699
+ ["0000,0100", "US", C_ECHO_RQ],
700
+ ["0000,0110", "US", DEFAULT_MESSAGE_ID],
701
+ ["0000,0800", "US", NO_DATA_SET_PRESENT]
702
+ ]
703
+ end
704
+
705
+ # Sets the command elements used in a C-FIND-RQ.
706
+ #
707
+ # === Notes
708
+ #
709
+ # * This setup is used for all types of queries.
710
+ #
711
+ def set_command_fragment_find
712
+ @command_elements = [
713
+ ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
714
+ ["0000,0100", "US", C_FIND_RQ],
715
+ ["0000,0110", "US", DEFAULT_MESSAGE_ID],
716
+ ["0000,0700", "US", 0], # Priority: 0: medium
717
+ ["0000,0800", "US", DATA_SET_PRESENT]
718
+ ]
719
+ end
720
+
721
+ # Sets the command elements used in a C-GET-RQ.
722
+ #
723
+ def set_command_fragment_get
724
+ @command_elements = [
725
+ ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
726
+ ["0000,0100", "US", C_GET_RQ],
727
+ ["0000,0600", "AE", @ae], # Destination is ourselves
728
+ ["0000,0700", "US", 0], # Priority: 0: medium
729
+ ["0000,0800", "US", DATA_SET_PRESENT]
730
+ ]
731
+ end
732
+
733
+ # Sets the command elements used in a C-MOVE-RQ.
734
+ #
735
+ def set_command_fragment_move(destination)
736
+ @command_elements = [
737
+ ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
738
+ ["0000,0100", "US", C_MOVE_RQ],
739
+ ["0000,0110", "US", DEFAULT_MESSAGE_ID],
740
+ ["0000,0600", "AE", destination],
741
+ ["0000,0700", "US", 0], # Priority: 0: medium
742
+ ["0000,0800", "US", DATA_SET_PRESENT]
743
+ ]
744
+ end
745
+
746
+ # Sets the command elements used in a C-STORE-RQ.
747
+ #
748
+ def set_command_fragment_store(modality, instance, message_id)
749
+ @command_elements = [
750
+ ["0000,0002", "UI", modality], # Affected SOP Class UID
751
+ ["0000,0100", "US", C_STORE_RQ],
752
+ ["0000,0110", "US", message_id],
753
+ ["0000,0700", "US", 0], # Priority: 0: medium
754
+ ["0000,0800", "US", DATA_SET_PRESENT],
755
+ ["0000,1000", "UI", instance] # Affected SOP Instance UID
756
+ ]
757
+ end
758
+
759
+ # Sets the data elements used in a query for the images of a particular series.
760
+ #
761
+ def set_data_fragment_find_images
762
+ @data_elements = [
763
+ ["0008,0018", ""], # SOP Instance UID
764
+ ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
765
+ ["0020,000D", ""], # Study Instance UID
766
+ ["0020,000E", ""], # Series Instance UID
767
+ ["0020,0013", ""] # Instance Number
768
+ ]
769
+ end
770
+
771
+ # Sets the data elements used in a query for patients.
772
+ #
773
+ def set_data_fragment_find_patients
774
+ @data_elements = [
775
+ ["0008,0052", "PATIENT"], # Query/Retrieve Level: "PATIENT"
776
+ ["0010,0010", ""], # Patient's Name
777
+ ["0010,0020", ""], # Patient ID
778
+ ["0010,0030", ""], # Patient's Birth Date
779
+ ["0010,0040", ""] # Patient's Sex
780
+ ]
781
+ end
782
+
783
+ # Sets the data elements used in a query for the series of a particular study.
784
+ #
785
+ def set_data_fragment_find_series
786
+ @data_elements = [
787
+ ["0008,0052", "SERIES"], # Query/Retrieve Level: "SERIES"
788
+ ["0008,0060", ""], # Modality
789
+ ["0008,103E", ""], # Series Description
790
+ ["0020,000D", ""], # Study Instance UID
791
+ ["0020,000E", ""], # Series Instance UID
792
+ ["0020,0011", ""] # Series Number
793
+ ]
794
+ end
795
+
796
+ # Sets the data elements used in a query for studies.
797
+ #
798
+ def set_data_fragment_find_studies
799
+ @data_elements = [
800
+ ["0008,0020", ""], # Study Date
801
+ ["0008,0030", ""], # Study Time
802
+ ["0008,0050", ""], # Accession Number
803
+ ["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
804
+ ["0008,0090", ""], # Referring Physician's Name
805
+ ["0008,1030", ""], # Study Description
806
+ ["0008,1060", ""], # Name of Physician(s) Reading Study
807
+ ["0010,0010", ""], # Patient's Name
808
+ ["0010,0020", ""], # Patient ID
809
+ ["0010,0030", ""], # Patient's Birth Date
810
+ ["0010,0040", ""], # Patient's Sex
811
+ ["0020,000D", ""], # Study Instance UID
812
+ ["0020,0010", ""] # Study ID
813
+ ]
814
+ end
815
+
816
+ # Sets the data elements used for an image C-GET-RQ.
817
+ #
818
+ def set_data_fragment_get_image
819
+ @data_elements = [
820
+ ["0008,0018", ""], # SOP Instance UID
821
+ ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
822
+ ["0020,000D", ""], # Study Instance UID
823
+ ["0020,000E", ""] # Series Instance UID
824
+ ]
825
+ end
826
+
827
+ # Sets the data elements used for an image C-MOVE-RQ.
828
+ #
829
+ def set_data_fragment_move_image
830
+ @data_elements = [
831
+ ["0008,0018", ""], # SOP Instance UID
832
+ ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
833
+ ["0020,000D", ""], # Study Instance UID
834
+ ["0020,000E", ""] # Series Instance UID
835
+ ]
836
+ end
837
+
838
+ # Sets the data elements used in a study C-MOVE-RQ.
839
+ #
840
+ def set_data_fragment_move_study
841
+ @data_elements = [
842
+ ["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
843
+ ["0010,0020", ""], # Patient ID
844
+ ["0020,000D", ""] # Study Instance UID
845
+ ]
846
+ end
847
+
848
+ # Transfers the user query options to the @data_elements instance array.
849
+ #
850
+ # === Restrictions
851
+ #
852
+ # * Only tag & value pairs for tags which are predefined for the specific query type will be stored!
853
+ #
854
+ def set_data_options(options)
855
+ options.each_pair do |key, value|
856
+ tags = @data_elements.transpose[0]
857
+ i = tags.index(key)
858
+ if i
859
+ @data_elements[i][1] = value
860
+ end
861
+ end
862
+ end
863
+
864
+ # Sets the default values for proposed transfer syntaxes.
865
+ #
866
+ def set_default_values
867
+ # DICOM Application Context Name (unknown if this will vary or is always the same):
868
+ @application_context_uid = APPLICATION_CONTEXT
869
+ # Transfer syntax string array (preferred syntax appearing first):
870
+ @transfer_syntax = [IMPLICIT_LITTLE_ENDIAN, EXPLICIT_LITTLE_ENDIAN, EXPLICIT_BIG_ENDIAN]
871
+ end
872
+
873
+ # Sets the @user_information items instance array.
874
+ #
875
+ # === Notes
876
+ #
877
+ # Each user information item is a three element array consisting of: item type code, VR & value.
878
+ #
879
+ def set_user_information_array
880
+ @user_information = [
881
+ [ITEM_MAX_LENGTH, "UL", @max_package_size],
882
+ [ITEM_IMPLEMENTATION_UID, "STR", UID],
883
+ [ITEM_IMPLEMENTATION_VERSION, "STR", NAME]
884
+ ]
885
+ end
886
+
887
+ end
888
+ end