dicom 0.8 → 0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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