dicom 0.8 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,18 +1,10 @@
1
- # Copyright 2010 Christoffer Lervag
2
-
3
- # This file contains module constants used by the Ruby DICOM library.
4
1
 
5
2
  module DICOM
6
3
 
7
- # Ruby DICOM version string.
8
- VERSION = "0.8"
9
-
10
- # Ruby DICOM's implementation UID.
4
+ # Ruby DICOM's Implementation UID.
11
5
  UID = "1.2.826.0.1.3680043.8.641"
12
6
  # Ruby DICOM name & version (max 16 characters).
13
7
  NAME = "RUBY_DCM_" + DICOM::VERSION
14
- # Application title.
15
- SOURCE_APP_TITLE = "RUBY_DICOM"
16
8
 
17
9
  # Item tag.
18
10
  ITEM_TAG = "FFFE,E000"
@@ -115,7 +107,65 @@ module DICOM
115
107
  # System (CPU) Endianness.
116
108
  CPU_ENDIAN = endian_type[Array(x).pack("L*")]
117
109
 
110
+ # Custom string used for (un)packing big endian signed short.
111
+ CUSTOM_SS = "k*"
112
+ # Custom string used for (un)packing big endian signed long.
113
+ CUSTOM_SL = "r*"
114
+
118
115
  # Ruby DICOM's library (data dictionary).
119
116
  LIBRARY = DICOM::DLibrary.new
120
117
 
121
- end
118
+ # Transfer Syntaxes
119
+ # Taken from DICOM Specification PS 3.5, Chapter 10
120
+
121
+ # General
122
+ TXS_IMPLICIT_LITTLE_ENDIAN = '1.2.840.10008.1.2' # also defined as IMPLICIT_LITTLE_ENDIAN, default transfer syntax
123
+ TXS_EXPLICIT_LITTLE_ENDIAN = '1.2.840.10008.1.2.1' # also defined as EXPLICIT_LITTLE_ENDIAN
124
+ TXS_EXPLICIT_BIG_ENDIAN = '1.2.840.10008.1.2.2' # also defined as EXPLICIT_BIG_ENDIAN
125
+
126
+ # TRANSFER SYNTAXES FOR ENCAPSULATION OF ENCODED PIXEL DATA
127
+ TXS_JPEG_BASELINE = '1.2.840.10008.1.2.4.50'
128
+ TXS_JPEG_EXTENDED = '1.2.840.10008.1.2.4.51'
129
+ TXS_JPEG_LOSSLESS_NH = '1.2.840.10008.1.2.4.57' # NH: non-hirarchical
130
+ TXS_JPEG_LOSSLESS_NH_FOP = '1.2.840.10008.1.2.4.70' # NH: non-hirarchical, FOP: first-order prediction
131
+
132
+ TXS_JPEG_LS_LOSSLESS = '1.2.840.10008.1.2.4.80'
133
+ TXS_JPEG_LS_NEAR_LOSSLESS = '1.2.840.10008.1.2.4.81'
134
+
135
+ TXS_JPEG_2000_PART1_LOSSLESS = '1.2.840.10008.1.2.4.90'
136
+ TXS_JPEG_2000_PART1_LOSSLESS_OR_LOSSY = '1.2.840.10008.1.2.4.91'
137
+ TXS_JPEG_2000_PART2_LOSSLESS = '1.2.840.10008.1.2.4.92'
138
+ TXS_JPEG_2000_PART2_LOSSLESS_OR_LOSSY = '1.2.840.10008.1.2.4.93'
139
+
140
+ TXS_MPEG2_MP_ML = '1.2.840.10008.1.2.4.100'
141
+ TXS_MPEG2_MP_HL = '1.2.840.10008.1.2.4.101'
142
+
143
+ TXS_DEFLATED_LITTLE_ENDIAN = '1.2.840.10008.1.2.1.99' # ZIP Compression
144
+
145
+ TXS_JPIP = '1.2.840.10008.1.2.4.94'
146
+ TXS_JPIP_DEFLATE = '1.2.840.10008.1.2.4.95'
147
+
148
+ TXS_RLE = '1.2.840.10008.1.2.5'
149
+
150
+
151
+ # Photometric Interpretations
152
+ # Taken from DICOM Specification PS 3.3 C.7.6.3.1.2 Photometric Interpretation
153
+
154
+ PI_MONOCHROME1 = 'MONOCHROME1'
155
+ PI_MONOCHROME2 = 'MONOCHROME2'
156
+ PI_PALETTE_COLOR = 'PALETTE COLOR'
157
+ PI_RGB = 'RGB'
158
+ PI_YBR_FULL = 'YBR_FULL'
159
+ PI_YBR_FULL_422 = 'YBR_FULL_422 '
160
+ PI_YBR_PARTIAL_422 = 'YBR_PARTIAL_422'
161
+ PI_YBR_PARTIAL_420 = 'YBR_PARTIAL_420'
162
+ PI_YBR_ICT = 'YBR_ICT'
163
+ PI_YBR_RCT = 'YBR_RCT'
164
+
165
+ # Retired Photometric Interpretations, are those needed to be supported?
166
+ PI_HSV = 'HSV'
167
+ PI_ARGB = 'ARGB'
168
+ PI_CMYK = 'CMYK'
169
+
170
+ end
171
+
@@ -1,9 +1,11 @@
1
- # Copyright 2009-2010 Christoffer Lervag
2
1
 
3
2
  module DICOM
4
3
 
5
4
  # This class contains code for handling the client side of DICOM TCP/IP network communication.
6
5
  #
6
+ # === Resources
7
+ #
8
+ # * For information regarding query, such as required/optional attributes and value matching, refer to the DICOM standard, PS3.4, C 2.2.
7
9
  #--
8
10
  # FIXME: The code which waits for incoming network packets seems to be very CPU intensive. Perhaps there is a more elegant way to wait for incoming messages?
9
11
  #
@@ -15,7 +17,7 @@ module DICOM
15
17
  attr_accessor :host_ae
16
18
  # The IP adress of the server.
17
19
  attr_accessor :host_ip
18
- # The maximum allowed size of network packages (in bytes).
20
+ # The maximum allowed size of network packages (in bytes).
19
21
  attr_accessor :max_package_size
20
22
  # The network port to be used.
21
23
  attr_accessor :port
@@ -44,7 +46,7 @@ module DICOM
44
46
  #
45
47
  # * <tt>:ae</tt> -- String. The name of this client (application entity).
46
48
  # * <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).
49
+ # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
48
50
  # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
49
51
  # * <tt>:verbose</tt> -- Boolean. If set to false, the DClient instance will run silently and not output warnings and error messages to the screen. Defaults to true.
50
52
  #
@@ -73,11 +75,11 @@ module DICOM
73
75
  @association = nil # DICOM Association status
74
76
  @request_approved = nil # Status of our DICOM request
75
77
  @release = nil # Status of received, valid release response
78
+ @data_elements = []
76
79
  # Results from a query:
77
80
  @command_results = Array.new
78
81
  @data_results = Array.new
79
- # Set default values like transfer syntax, user information, endianness:
80
- set_default_values
82
+ # Setup the user information used in the association request::
81
83
  set_user_information_array
82
84
  # Initialize the network package handler:
83
85
  @link = Link.new(:ae => @ae, :host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout)
@@ -87,34 +89,46 @@ module DICOM
87
89
  #
88
90
  def echo
89
91
  # Verification SOP Class:
90
- @abstract_syntaxes = [VERIFICATION_SOP]
92
+ set_default_presentation_context(VERIFICATION_SOP)
91
93
  perform_echo
92
94
  end
93
95
 
94
- # Queries a service class provider for images that match the specified criteria.
96
+ # Queries a service class provider for images (composite object instances) that match the specified criteria.
95
97
  #
96
98
  # === Parameters
97
99
  #
98
- # * <tt>options</tt> -- A hash of parameters.
100
+ # * <tt>query_params</tt> -- A hash of query parameters.
99
101
  #
100
102
  # === Options
101
103
  #
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
104
+ # * <tt>"0008,0018"</tt> -- SOP Instance UID
106
105
  # * <tt>"0020,0013"</tt> -- Instance Number
107
106
  #
107
+ # === Notes
108
+ #
109
+ # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL images in the database!
110
+ # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
111
+ # * For a general list of optional object instance level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.5, Table C.6-4.
112
+ #
108
113
  # === Examples
109
114
  #
110
115
  # node.find_images("0020,000D" => "1.2.840.1145.342", "0020,000E" => "1.3.6.1.4.1.2452.6.687844")
111
116
  #
112
- def find_images(options={})
117
+ def find_images(query_params={})
113
118
  # 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)
119
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.1")
120
+ # These query attributes will always be present in the dicom query:
121
+ default_query_params = {
122
+ "0008,0018" => "", # SOP Instance UID
123
+ "0008,0052" => "IMAGE", # Query/Retrieve Level: "IMAGE"
124
+ "0020,0013" => "" # Instance Number
125
+ }
126
+ # Raising an error if a non-tag query attribute is used:
127
+ query_params.keys.each do |tag|
128
+ raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
129
+ end
130
+ # Set up the query parameters and carry out the C-FIND:
131
+ set_data_elements(default_query_params.merge(query_params))
118
132
  perform_find
119
133
  return @data_results
120
134
  end
@@ -123,7 +137,7 @@ module DICOM
123
137
  #
124
138
  # === Parameters
125
139
  #
126
- # * <tt>options</tt> -- A hash of parameters.
140
+ # * <tt>query_params</tt> -- A hash of parameters.
127
141
  #
128
142
  # === Options
129
143
  #
@@ -133,16 +147,32 @@ module DICOM
133
147
  # * <tt>"0010,0030"</tt> -- Patient's Birth Date
134
148
  # * <tt>"0010,0040"</tt> -- Patient's Sex
135
149
  #
150
+ # === Notes
151
+ #
152
+ # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL patients in the database!
153
+ # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
154
+ # * For a general list of optional patient level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.2, Table C.6-1.
155
+ #
136
156
  # === Examples
137
157
  #
138
158
  # node.find_patients("0010,0010" => "James*")
139
159
  #
140
- def find_patients(options={})
160
+ def find_patients(query_params={})
141
161
  # 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)
162
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.1.1")
163
+ # Every query attribute with a value != nil (required) will be sent in the dicom query.
164
+ # The query parameters with nil-value (optional) are left out unless specified.
165
+ default_query_params = {
166
+ "0008,0052" => "PATIENT", # Query/Retrieve Level: "PATIENT"
167
+ "0010,0010" => "", # Patient's Name
168
+ "0010,0020" => "" # Patient's ID
169
+ }
170
+ # Raising an error if a non-tag query attribute is used:
171
+ query_params.keys.each do |tag|
172
+ raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
173
+ end
174
+ # Set up the query parameters and carry out the C-FIND:
175
+ set_data_elements(default_query_params.merge(query_params))
146
176
  perform_find
147
177
  return @data_results
148
178
  end
@@ -151,27 +181,41 @@ module DICOM
151
181
  #
152
182
  # === Parameters
153
183
  #
154
- # * <tt>options</tt> -- A hash of parameters.
184
+ # * <tt>query_params</tt> -- A hash of query parameters.
155
185
  #
156
186
  # === Options
157
187
  #
158
- # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
159
188
  # * <tt>"0008,0060"</tt> -- Modality
160
- # * <tt>"0008,103E"</tt> -- Series Description
161
- # * <tt>"0020,000D"</tt> -- Study Instance UID
162
189
  # * <tt>"0020,000E"</tt> -- Series Instance UID
163
190
  # * <tt>"0020,0011"</tt> -- Series Number
164
191
  #
192
+ # === Notes
193
+ #
194
+ # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL series in the database!
195
+ # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
196
+ # * For a general list of optional series level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.4, Table C.6-3.
197
+ #
165
198
  # === Examples
166
199
  #
167
200
  # node.find_series("0020,000D" => "1.2.840.1145.342")
168
201
  #
169
- def find_series(options={})
202
+ def find_series(query_params={})
170
203
  # 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)
204
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.1")
205
+ # Every query attribute with a value != nil (required) will be sent in the dicom query.
206
+ # The query parameters with nil-value (optional) are left out unless specified.
207
+ default_query_params = {
208
+ "0008,0052" => "SERIES", # Query/Retrieve Level: "SERIES"
209
+ "0008,0060" => "", # Modality
210
+ "0020,000E" => "", # Series Instance UID
211
+ "0020,0011" => "" # Series Number
212
+ }
213
+ # Raising an error if a non-tag query attribute is used:
214
+ query_params.keys.each do |tag|
215
+ raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
216
+ end
217
+ # Set up the query parameters and carry out the C-FIND:
218
+ set_data_elements(default_query_params.merge(query_params))
175
219
  perform_find
176
220
  return @data_results
177
221
  end
@@ -180,34 +224,49 @@ module DICOM
180
224
  #
181
225
  # === Parameters
182
226
  #
183
- # * <tt>options</tt> -- A hash of parameters.
227
+ # * <tt>query_params</tt> -- A hash of query parameters.
184
228
  #
185
229
  # === Options
186
230
  #
187
231
  # * <tt>"0008,0020"</tt> -- Study Date
188
232
  # * <tt>"0008,0030"</tt> -- Study Time
189
233
  # * <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
234
  # * <tt>"0010,0010"</tt> -- Patient's Name
195
235
  # * <tt>"0010,0020"</tt> -- Patient ID
196
- # * <tt>"0010,0030"</tt> -- Patient's Birth Date
197
- # * <tt>"0010,0040"</tt> -- Patient's Sex
198
236
  # * <tt>"0020,000D"</tt> -- Study Instance UID
199
237
  # * <tt>"0020,0010"</tt> -- Study ID
200
238
  #
239
+ # === Notes
240
+ #
241
+ # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL studies in the database!
242
+ # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
243
+ # * For a general list of optional study level attributes, refer to the DICOM standard, PS3.4 C.6.2.1.2, Table C.6-5.
244
+ #
201
245
  # === Examples
202
246
  #
203
247
  # node.find_studies("0008,0020" => "20090604-", "0010,000D" => "123456789")
204
248
  #
205
- def find_studies(options={})
249
+ def find_studies(query_params={})
206
250
  # 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)
251
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.1")
252
+ # Every query attribute with a value != nil (required) will be sent in the dicom query.
253
+ # The query parameters with nil-value (optional) are left out unless specified.
254
+ default_query_params = {
255
+ "0008,0020" => "", # Study Date
256
+ "0008,0030" => "", # Study Time
257
+ "0008,0050" => "", # Accession Number
258
+ "0008,0052" => "STUDY", # Query/Retrieve Level: "STUDY"
259
+ "0010,0010" => "", # Patient's Name
260
+ "0010,0020" => "", # Patient ID
261
+ "0020,000D" => "", # Study Instance UID
262
+ "0020,0010" => "" # Study ID
263
+ }
264
+ # Raising an error if a non-tag query attribute is used:
265
+ query_params.keys.each do |tag|
266
+ raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
267
+ end
268
+ # Set up the query parameters and carry out the C-FIND:
269
+ set_data_elements(default_query_params.merge(query_params))
211
270
  perform_find
212
271
  return @data_results
213
272
  end
@@ -236,7 +295,7 @@ module DICOM
236
295
  #
237
296
  def get_image(path, options={})
238
297
  # Study Root Query/Retrieve Information Model - GET:
239
- @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.3"]
298
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.3")
240
299
  # Transfer the current options to the data_elements hash:
241
300
  set_command_fragment_get
242
301
  # Prepare data elements for this operation:
@@ -269,7 +328,7 @@ module DICOM
269
328
  #
270
329
  def move_image(destination, options={})
271
330
  # Study Root Query/Retrieve Information Model - MOVE:
272
- @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.2"]
331
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.2")
273
332
  # Transfer the current options to the data_elements hash:
274
333
  set_command_fragment_move(destination)
275
334
  # Prepare data elements for this operation:
@@ -301,7 +360,7 @@ module DICOM
301
360
  #
302
361
  def move_study(destination, options={})
303
362
  # Study Root Query/Retrieve Information Model - MOVE:
304
- @abstract_syntaxes = ["1.2.840.10008.5.1.4.1.2.2.2"]
363
+ set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.2")
305
364
  # Transfer the current options to the data_elements hash:
306
365
  set_command_fragment_move(destination)
307
366
  # Prepare data elements for this operation:
@@ -322,12 +381,12 @@ module DICOM
322
381
  #
323
382
  def send(files)
324
383
  # Prepare the DICOM object(s):
325
- objects, @abstract_syntaxes, success, message = load_files(files)
384
+ objects, success, message = load_files(files)
326
385
  if success
327
386
  # Open a DICOM link:
328
387
  establish_association
329
- if @association
330
- if @request_approved
388
+ if association_established?
389
+ if request_approved?
331
390
  # Continue with our c-store operation, since our request was accepted.
332
391
  # Handle the transmission:
333
392
  perform_send(objects)
@@ -347,11 +406,11 @@ module DICOM
347
406
  add_notice("TESTING CONNECTION...")
348
407
  success = false
349
408
  # Verification SOP Class:
350
- @abstract_syntaxes = [VERIFICATION_SOP]
409
+ set_default_presentation_context(VERIFICATION_SOP)
351
410
  # Open a DICOM link:
352
411
  establish_association
353
- if @association
354
- if @request_approved
412
+ if association_established?
413
+ if request_approved?
355
414
  success = true
356
415
  end
357
416
  # Close the DICOM link:
@@ -394,6 +453,22 @@ module DICOM
394
453
  @notices << notice
395
454
  end
396
455
 
456
+ # Returns an array of supported transfer syntaxes for the specified transfer syntax.
457
+ # For compressed transfer syntaxes, we currently do not support reencoding these to other syntaxes.
458
+ #
459
+ def available_transfer_syntaxes(transfer_syntax)
460
+ case transfer_syntax
461
+ when IMPLICIT_LITTLE_ENDIAN
462
+ return [IMPLICIT_LITTLE_ENDIAN, EXPLICIT_LITTLE_ENDIAN]
463
+ when EXPLICIT_LITTLE_ENDIAN
464
+ return [EXPLICIT_LITTLE_ENDIAN, IMPLICIT_LITTLE_ENDIAN]
465
+ when EXPLICIT_BIG_ENDIAN
466
+ return [EXPLICIT_BIG_ENDIAN, IMPLICIT_LITTLE_ENDIAN]
467
+ else # Compression:
468
+ return [transfer_syntax]
469
+ end
470
+ end
471
+
397
472
  # Opens a TCP session with the server, and handles the association request as well as the response.
398
473
  #
399
474
  def establish_association
@@ -401,12 +476,12 @@ module DICOM
401
476
  @association = false
402
477
  @request_approved = false
403
478
  # Initiate the association:
404
- @link.build_association_request(@application_context_uid, @abstract_syntaxes, @transfer_syntax, @user_information)
479
+ @link.build_association_request(@presentation_contexts, @user_information)
405
480
  @link.start_session(@host_ip, @port)
406
481
  @link.transmit
407
482
  info = @link.receive_multiple_transmissions.first
408
483
  # Interpret the results:
409
- if info[:valid]
484
+ if info && info[:valid]
410
485
  if info[:pdu] == PDU_ASSOCIATION_ACCEPT
411
486
  # Values of importance are extracted and put into instance variables:
412
487
  @association = true
@@ -445,6 +520,14 @@ module DICOM
445
520
  @abort = false
446
521
  end
447
522
 
523
+ # Finds and retuns the abstract syntax that is associated with the specified context id.
524
+ #
525
+ def find_abstract_syntax(id)
526
+ @presentation_contexts.each_pair do |abstract_syntax, context_ids|
527
+ return abstract_syntax if context_ids[id]
528
+ end
529
+ end
530
+
448
531
  # Loads one or more DICOM files.
449
532
  # Returns an array of DObject instances, an array of unique abstract syntaxes found among the files, a status boolean and a message string.
450
533
  #
@@ -457,28 +540,54 @@ module DICOM
457
540
  message = ""
458
541
  objects = Array.new
459
542
  abstracts = Array.new
543
+ id = 1
544
+ @presentation_contexts = Hash.new
460
545
  files = [files] unless files.is_a?(Array)
461
546
  files.each do |file|
462
547
  if file.is_a?(String)
463
548
  obj = DObject.new(file, :verbose => false)
464
549
  if obj.read_success
465
- # Load the DICOM object and its abstract syntax:
550
+ # Load the DICOM object:
466
551
  objects << obj
467
- abstracts << obj.value("0008,0016")
468
552
  else
469
553
  status = false
470
554
  message = "Failed to successfully parse a DObject for the following string: #{file}"
471
555
  end
472
556
  elsif file.is_a?(DObject)
473
557
  # Load the DICOM object and its abstract syntax:
474
- objects << obj
475
- abstracts << obj.value("0008,0016")
558
+ abstracts << file.value("0008,0016")
559
+ objects << file
476
560
  else
477
561
  status = false
478
562
  message = "Array contains invalid object #{file}."
479
563
  end
480
564
  end
481
- return objects, abstracts.uniq, status, message
565
+ # Extract available transfer syntaxes for the various sop classes found amongst these objects
566
+ syntaxes = Hash.new
567
+ objects.each do |obj|
568
+ sop_class = obj.value("0008,0016")
569
+ if sop_class
570
+ transfer_syntaxes = available_transfer_syntaxes(obj.transfer_syntax)
571
+ if syntaxes[sop_class]
572
+ syntaxes[sop_class] << transfer_syntaxes
573
+ else
574
+ syntaxes[sop_class] = transfer_syntaxes
575
+ end
576
+ else
577
+ status = false
578
+ message = "Missing SOP Class UID. Unable to transmit DICOM object"
579
+ end
580
+ # Extract the unique variations of SOP Class and syntaxes and construct the presentation context hash:
581
+ syntaxes.each_pair do |sop_class, ts|
582
+ selected_transfer_syntaxes = ts.flatten.uniq
583
+ @presentation_contexts[sop_class] = Hash.new
584
+ selected_transfer_syntaxes.each do |syntax|
585
+ @presentation_contexts[sop_class][id] = {:transfer_syntaxes => [syntax]}
586
+ id += 2
587
+ end
588
+ end
589
+ end
590
+ return objects, status, message
482
591
  end
483
592
 
484
593
  # Handles the communication involved in a DICOM C-ECHO.
@@ -488,12 +597,11 @@ module DICOM
488
597
  def perform_echo
489
598
  # Open a DICOM link:
490
599
  establish_association
491
- if @association
492
- if @request_approved
600
+ if association_established?
601
+ if request_approved?
493
602
  # Continue with our echo, since the request was accepted.
494
603
  # Set the query command elements array:
495
604
  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
605
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
498
606
  @link.transmit
499
607
  # Listen for incoming responses and interpret them individually, until we have received the last command fragment.
@@ -513,12 +621,11 @@ module DICOM
513
621
  def perform_find
514
622
  # Open a DICOM link:
515
623
  establish_association
516
- if @association
517
- if @request_approved
624
+ if association_established?
625
+ if request_approved?
518
626
  # Continue with our query, since the request was accepted.
519
627
  # Set the query command elements array:
520
628
  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
629
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
523
630
  @link.transmit
524
631
  @link.build_data_fragment(@data_elements, presentation_context_id)
@@ -542,10 +649,9 @@ module DICOM
542
649
  def perform_get(path)
543
650
  # Open a DICOM link:
544
651
  establish_association
545
- if @association
546
- if @request_approved
652
+ if association_established?
653
+ if request_approved?
547
654
  # 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
655
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
550
656
  @link.transmit
551
657
  @link.build_data_fragment(@data_elements, presentation_context_id)
@@ -569,10 +675,9 @@ module DICOM
569
675
  def perform_move
570
676
  # Open a DICOM link:
571
677
  establish_association
572
- if @association
573
- if @request_approved
678
+ if association_established?
679
+ if request_approved?
574
680
  # 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
681
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
577
682
  @link.transmit
578
683
  @link.build_data_fragment(@data_elements, presentation_context_id)
@@ -593,22 +698,24 @@ module DICOM
593
698
  def perform_send(objects)
594
699
  objects.each_with_index do |obj, index|
595
700
  # 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]
701
+ sop_class = obj.value("0008,0016")
702
+ sop_instance = obj.value("0008,0018")
703
+ if sop_class and sop_instance
704
+ # Only send the image if its sop_class has been accepted by the receiver:
705
+ if @approved_syntaxes[sop_class]
601
706
  # Set the command array to be used:
602
707
  message_id = index + 1
603
- set_command_fragment_store(modality, instance, message_id)
708
+ set_command_fragment_store(sop_class, sop_instance, message_id)
604
709
  # Find context id and transfer syntax:
605
- presentation_context_id = @approved_syntaxes[modality][0]
606
- selected_transfer_syntax = @approved_syntaxes[modality][1]
710
+ presentation_context_id = @approved_syntaxes[sop_class][0]
711
+ selected_transfer_syntax = @approved_syntaxes[sop_class][1]
607
712
  # 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
713
  # Set the transfer syntax of the DICOM object equal to the one accepted by the SCP:
609
714
  obj.transfer_syntax = selected_transfer_syntax
715
+ # Remove the Meta group, since it doesn't belong in a DICOM file transfer:
716
+ obj.remove_group(META_GROUP)
610
717
  max_header_length = 14
611
- data_packages = obj.encode_segments(@max_pdu_length - max_header_length)
718
+ data_packages = obj.encode_segments(@max_pdu_length - max_header_length, selected_transfer_syntax)
612
719
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
613
720
  @link.transmit
614
721
  # Transmit all but the last data strings:
@@ -643,13 +750,14 @@ module DICOM
643
750
  rejected = Hash.new
644
751
  # Reset the presentation context instance variable:
645
752
  @link.presentation_contexts = Hash.new
753
+ accepted_pc = 0
646
754
  presentation_contexts.each do |pc|
647
755
  # Determine what abstract syntax this particular presentation context's id corresponds to:
648
756
  id = pc[:presentation_context_id]
649
757
  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]
758
+ abstract_syntax = find_abstract_syntax(id)
652
759
  if pc[:result] == 0
760
+ accepted_pc += 1
653
761
  @approved_syntaxes[abstract_syntax] = [id, pc[:transfer_syntax]]
654
762
  @link.presentation_contexts[id] = pc[:transfer_syntax]
655
763
  else
@@ -658,16 +766,17 @@ module DICOM
658
766
  end
659
767
  if rejected.length == 0
660
768
  @request_approved = true
661
- if @approved_syntaxes.length == 1
769
+ if @approved_syntaxes.length == 1 and presentation_contexts.length == 1
662
770
  add_notice("The presentation context was accepted by host #{@host_ae}.")
663
771
  else
664
- add_notice("All #{@approved_syntaxes.length} presentation contexts were accepted by host #{@host_ae} (#{@host_ip}).")
772
+ add_notice("All #{presentation_contexts.length} presentation contexts were accepted by host #{@host_ae} (#{@host_ip}).")
665
773
  end
666
774
  else
667
- @request_approved = false
775
+ # We still consider the request 'approved' if at least one context were accepted:
776
+ @request_approved = true if @approved_syntaxes.length > 0
668
777
  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)}")}
778
+ @approved_syntaxes.each_pair {|key, value| add_error("APPROVED: #{LIBRARY.get_syntax_description(key)} (#{LIBRARY.get_syntax_description(value[1])})")}
779
+ rejected.each_pair {|key, value| add_error("REJECTED: #{LIBRARY.get_syntax_description(key)} (#{LIBRARY.get_syntax_description(value[1])})")}
671
780
  end
672
781
  end
673
782
 
@@ -695,7 +804,7 @@ module DICOM
695
804
  #
696
805
  def set_command_fragment_echo
697
806
  @command_elements = [
698
- ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
807
+ ["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
699
808
  ["0000,0100", "US", C_ECHO_RQ],
700
809
  ["0000,0110", "US", DEFAULT_MESSAGE_ID],
701
810
  ["0000,0800", "US", NO_DATA_SET_PRESENT]
@@ -710,7 +819,7 @@ module DICOM
710
819
  #
711
820
  def set_command_fragment_find
712
821
  @command_elements = [
713
- ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
822
+ ["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
714
823
  ["0000,0100", "US", C_FIND_RQ],
715
824
  ["0000,0110", "US", DEFAULT_MESSAGE_ID],
716
825
  ["0000,0700", "US", 0], # Priority: 0: medium
@@ -722,7 +831,7 @@ module DICOM
722
831
  #
723
832
  def set_command_fragment_get
724
833
  @command_elements = [
725
- ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
834
+ ["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
726
835
  ["0000,0100", "US", C_GET_RQ],
727
836
  ["0000,0600", "AE", @ae], # Destination is ourselves
728
837
  ["0000,0700", "US", 0], # Priority: 0: medium
@@ -734,7 +843,7 @@ module DICOM
734
843
  #
735
844
  def set_command_fragment_move(destination)
736
845
  @command_elements = [
737
- ["0000,0002", "UI", @abstract_syntaxes.first], # Affected SOP Class UID
846
+ ["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
738
847
  ["0000,0100", "US", C_MOVE_RQ],
739
848
  ["0000,0110", "US", DEFAULT_MESSAGE_ID],
740
849
  ["0000,0600", "AE", destination],
@@ -756,63 +865,6 @@ module DICOM
756
865
  ]
757
866
  end
758
867
 
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
868
  # Sets the data elements used for an image C-GET-RQ.
817
869
  #
818
870
  def set_data_fragment_get_image
@@ -845,11 +897,11 @@ module DICOM
845
897
  ]
846
898
  end
847
899
 
848
- # Transfers the user query options to the @data_elements instance array.
900
+ # Transfers the user-specified options to the @data_elements instance array.
849
901
  #
850
902
  # === Restrictions
851
903
  #
852
- # * Only tag & value pairs for tags which are predefined for the specific query type will be stored!
904
+ # * Only tag & value pairs for tags which are predefined for the specific request type will be stored!
853
905
  #
854
906
  def set_data_options(options)
855
907
  options.each_pair do |key, value|
@@ -861,13 +913,15 @@ module DICOM
861
913
  end
862
914
  end
863
915
 
864
- # Sets the default values for proposed transfer syntaxes.
916
+ # Creates the presentation context used for the non-file-transmission association requests..
865
917
  #
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]
918
+ def set_default_presentation_context(abstract_syntax)
919
+ raise ArgumentError, "Expected String, got #{abstract_syntax.class}" unless abstract_syntax.is_a?(String)
920
+ id = 1
921
+ transfer_syntaxes = [IMPLICIT_LITTLE_ENDIAN, EXPLICIT_LITTLE_ENDIAN, EXPLICIT_BIG_ENDIAN]
922
+ item = {:transfer_syntaxes => transfer_syntaxes}
923
+ pc = {id => item}
924
+ @presentation_contexts = {abstract_syntax => pc}
871
925
  end
872
926
 
873
927
  # Sets the @user_information items instance array.
@@ -884,5 +938,24 @@ module DICOM
884
938
  ]
885
939
  end
886
940
 
941
+ def association_established?
942
+ @association == true
943
+ end
944
+
945
+ def request_approved?
946
+ @request_approved == true
947
+ end
948
+
949
+ def presentation_context_id
950
+ @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
951
+ end
952
+
953
+ def set_data_elements(options)
954
+ @data_elements = []
955
+ options.keys.sort.each do |tag|
956
+ @data_elements << [ tag, options[tag] ] unless options[tag].nil?
957
+ end
958
+ end
959
+
887
960
  end
888
961
  end