dicom 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,16 @@
1
+ = 0.6
2
+
3
+ === 13th August, 2009
4
+
5
+ * Complete rewrite of encoding/decoding to reduce code duplication and simplify the code.
6
+ * General optimizations to improve speed and reduce code complexity.
7
+ * Rewrote Dictionary to use Hash instead of Array. This has significantly improved the read spead of the library.
8
+ * Ruby DICOM now automatically strips away trailing whitespace when decoding string values.
9
+ * Introducing basic, experimental network functionality.
10
+ * The new DClient class enables features such as query, move and send.
11
+ * The new DServer class presents the ability to set up a simple storage server (Service Class Provider).
12
+
13
+
1
14
  = 0.5
2
15
 
3
16
  === 4th May, 2009
@@ -14,7 +27,7 @@
14
27
  * Several methods have been made more robust.
15
28
  * Added new keyword :partial to the get_pos() method, which will enable search for partial string matches.
16
29
  * Added new keyword :array to the get_value() and get_raw() methods which enables easy extraction of
17
- multiple values of a given tag to an array (relevant for tags present at multiple places in a DICOM file)
30
+ multiple values of a given tag to an array (relevant for tags present at multiple places in a DICOM file).
18
31
  * Fixed a bug where exception folders where ignored in the Anonymizer class.
19
32
 
20
33
 
@@ -75,7 +88,9 @@
75
88
  === 20th July, 2008
76
89
 
77
90
  First public release.
78
- The library does have several known issues and lacks some features I would like it to have, but it does offer basic functionality and should be usable for people interested in working with DICOM files in Ruby. The reading algorithm has been tested succesfully with some 40 different DICOM files, so it should be fairly robust.
91
+ The library does have several known issues and lacks some features I would like it to have, but it does
92
+ offer basic functionality and should be usable for people interested in working with DICOM files in Ruby.
93
+ The reading algorithm has been tested succesfully with some 40 different DICOM files, so it should be fairly robust.
79
94
 
80
95
  Features:
81
96
  * Reads DICOM files
@@ -88,8 +103,9 @@ Features:
88
103
 
89
104
 
90
105
  Known issues:
91
- * 12 bit image data not supported
92
- * Color images not supported in NArray and RMagick retrieve methods
106
+ * Network communication is highly experimental.
107
+ * 12 bit image data not supported.
108
+ * Color images not supported in NArray and RMagick retrieve methods.
93
109
  * Unpacking compressed image data has basic support but is not properly tested yet.
94
110
  * Reading Big Endian files has fairly good, but possibly not full, support.
95
111
  * Reading on a Big Endian system is not tested.
@@ -1,4 +1,5 @@
1
- DICOM is a small library for reading, editing and writing DICOM files.
1
+ DICOM is a small and simple library for handling DICOM files (reading, editing
2
+ and writing) and DICOM network communication (both client and server side).
2
3
  It is written completely in Ruby and has no external dependencies.
3
4
 
4
5
  Copyright 2008-2009 Christoffer Lerv�g (chris.lervag [@nospam] @gmail.com)
@@ -251,3 +252,172 @@ PUBLIC INSTANCE METHODS
251
252
  a.remove_tag("0010,0010")
252
253
 
253
254
 
255
+ CLASS DClient
256
+
257
+ PUBLIC CLASS METHODS
258
+
259
+ new(host_ip, port, options={})
260
+ Initialize a new DClient instance.
261
+ Example:
262
+ node = DICOM::DClient.new("10.1.25.200", 104)
263
+
264
+ ACCESSORS (Read & write)
265
+ :ae
266
+ Calling application entity (name of the service class user - client).
267
+ :host_ae
268
+ Called application entity (name of the service class provider - server).
269
+ :host_ip
270
+ The host (server) ip adress.
271
+ :max_package_size
272
+ Maximum size of transferred network packages.
273
+ :port
274
+ Port number to be used in the network communication.
275
+ :timeout
276
+ Timeout (in seconds) to be used in the network communication.
277
+ :verbose
278
+ Verbosity with regards to notices and error messages (true or false).
279
+ ACCESSORS (Read only)
280
+ :command_results
281
+ An array holding any received command results.
282
+ :data_results
283
+ An array holding any received data results.
284
+ :errors
285
+ An array holding any error messages that have occurred in this session.
286
+ :notices
287
+ An array holding any informational messages presented in this session.
288
+
289
+ PUBLIC INSTANCE METHODS
290
+
291
+ get_image(path, options={})
292
+ Retrieve a DICOM image file from a server (C-GET-RQ) (this method is probably not working yet).
293
+ Accepted options:
294
+ "0008,0018" (SOP Instance UID)
295
+ "0008,0052" (Query/Retrieve Level)
296
+ "0020,000D" (Study Instance UID)
297
+ "0020,000E" (Series Instance UID)
298
+ Example:
299
+ node.get_image("/home/dicom/", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
300
+
301
+ find_images(options={})
302
+ Query a server for DICOM images that matches your specified criteria.
303
+ Accepted options:
304
+ "0008,0018" (SOP Instance UID)
305
+ "0008,0052" (Query/Retrieve Level)
306
+ "0020,000D" (Study Instance UID)
307
+ "0020,000E" (Series Instance UID)
308
+ "0020,0013" (Instance Number)
309
+ Example:
310
+ find_images("0010,0020" => patient_id, "0020,000D" => study_uid, "0020,000E" => series_uid)
311
+
312
+ find_patients(options={})
313
+ Query a server for patients that matches your specified criteria.
314
+ Accepted options:
315
+ "0008,0052" (Query/Retrieve Level)
316
+ "0010,0010" (Patient's Name)
317
+ "0010,0020" (Patient ID)
318
+ "0010,0030" (Patient's Birth Date)
319
+ "0010,0040" (Patient's Sex)
320
+ Example:
321
+ find_patients("0010,0010" => "James*")
322
+
323
+ find_series(options={})
324
+ Query a server for series that matches your specified criteria.
325
+ Accepted options:
326
+ "0008,0052" (Query/Retrieve Level)
327
+ "0008,0060" (Modality)
328
+ "0008,103E" (Series Description)
329
+ "0020,000D" (Study Instance UID)
330
+ "0020,000E" (Series Instance UID)
331
+ "0020,0011" (Series Number)
332
+ Example:
333
+ find_series("0010,0020" => patient_id, "0020,000D" => study_uid)
334
+
335
+ find_studies(options={})
336
+ Query a server for studies that matches your specified criteria.
337
+ Accepted options:
338
+ "0008,0020" (Study Date)
339
+ "0008,0030" (Study Time)
340
+ "0008,0050" (Accession Number)
341
+ "0008,0052" (Query/Retrieve Level)
342
+ "0008,0090" (Referring Physician's Name)
343
+ "0008,1030" (Study Description)
344
+ "0008,1060" (Name of Physician(s) Reading Study)
345
+ "0010,0010" (Patient's Name)
346
+ "0010,0020" (Patient ID)
347
+ "0010,0030" (Patient's Birth Date)
348
+ "0010,0040" (Patient's Sex)
349
+ "0020,000D" (Study Instance UID)
350
+ "0020,0010" (Study ID)
351
+ Example:
352
+ find_studies("0008,0020" => study_date, "0010,000D" => patient_id)
353
+
354
+ move_image(destination, options={})
355
+ Move an image to a dicom node other than yourself.
356
+ Accepted options:
357
+ "0008,0018" (SOP Instance UID)
358
+ "0008,0052" (Query/Retrieve Level)
359
+ "0020,000D" (Study Instance UID)
360
+ "0020,000E" (Series Instance UID)
361
+ Example:
362
+ move_image("MYDICOM", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
363
+
364
+ move_study(destination, options={})
365
+ Move an entire study to a dicom node other than yourself.
366
+ Accepted options:
367
+ "0008,0052" (Query/Retrieve Level)
368
+ "0010,0020" (Patient ID)
369
+ "0020,000D" (Study Instance UID)
370
+ Example:
371
+ move_study("MYDICOM", "0010,0020" => patient_id, "0020,000D" => study_uid)
372
+
373
+ send(file_path)
374
+ Send a DICOM file to a service class provider (SCP/PACS).
375
+ Example:
376
+ send("myFile.dcm")
377
+
378
+ test
379
+ Tests a connection with your DICOM server by trying a simple association.
380
+
381
+
382
+ CLASS DServer
383
+
384
+ PUBLIC CLASS METHODS
385
+
386
+ new(port, options={})
387
+ Initialize a new DServer instance.
388
+ Example:
389
+ node = DICOM::DServer.new(104)
390
+
391
+ ACCESSORS (Read & write)
392
+ :host_ae
393
+ Called application entity (name of the service class provider - server).
394
+ :max_package_size
395
+ Maximum size of transferred network packages.
396
+ :port
397
+ Port number to be used in the network communication.
398
+ :timeout
399
+ Timeout (in seconds) to be used in the network communication.
400
+ :verbose
401
+ Verbosity with regards to notices and error messages (true or false).
402
+ ACCESSORS (Read only)
403
+ :errors
404
+ An array holding any error messages that have occurred in this session.
405
+ :notices
406
+ An array holding any informational messages presented in this session.
407
+
408
+ PUBLIC INSTANCE METHODS
409
+
410
+ add_abstract_syntax(value)
411
+ Adds a specified abstract syntax to the list of syntaxes that is accepted by the server.
412
+
413
+ print_syntaxes
414
+ Prints the list of abstract syntaxes that is accepted by the server.
415
+
416
+ remove_abstract_syntax(value)
417
+ Removes a specified abstract syntax from the list of syntaxes that is accepted by the server.
418
+
419
+ remove_all_abstract_syntaxes
420
+ Completely clears the list of syntaxes that the server instance will accept.
421
+
422
+ start_scp(path)
423
+ Launch the simple storage server (Storage Content Provider - SCP)
data/README CHANGED
@@ -4,12 +4,17 @@ RUBY DICOM
4
4
  SUMMARY
5
5
  --------
6
6
 
7
- This is a fairly basic library for handling DICOM files in Ruby. Digital Imaging and Communications in Medicine (DICOM) is a standard for handling, storing, printing, and transmitting information in medical imaging. It includes a file format definition and a network communications protocol. Ruby DICOM supports reading from, editing and writing to this file format.
7
+ This is a small and simple library for handling DICOM in Ruby. DICOM (Digital Imaging
8
+ and Communications in Medicine) is a standard for handling, storing, printing,
9
+ and transmitting information in medical imaging. It includes a file format definition
10
+ and a network communications protocol. Ruby DICOM supports reading from, editing and
11
+ writing to this file format. It also features experimental support for network
12
+ communication modalities like query, move and send.
8
13
 
9
14
  BASIC USAGE
10
15
  -----------
11
16
 
12
- require 'dicom'
17
+ require "dicom"
13
18
  # Read file:
14
19
  dcm = DICOM::DObject.new("myFile.dcm")
15
20
  # Display some key information about the file:
@@ -26,6 +31,9 @@ image[0].display
26
31
  # Load pixel data to a NArray object and display it on screen:
27
32
  image = dcm.get_image_narray()
28
33
  NImage.show image[0,true,true]
34
+ # Send a local file to a server (PACS) over the network:
35
+ node = DICOM::DClient.new("10.1.25.200", 104)
36
+ node.send("myFile.dcm")
29
37
 
30
38
  Tip:
31
39
  When playing around with Ruby DICOM in irb, you may be annoyed
@@ -66,5 +74,5 @@ Oslo, Norway
66
74
 
67
75
  Email:
68
76
  chris.lervag [@nospam] @gmail.com
69
- Please don't hesitate to email me if have any thoughts on this project!
77
+ Please don't hesitate to email me if have any thoughts about this project!
70
78
 
@@ -0,0 +1,579 @@
1
+ # Copyright 2009 Christoffer Lervag
2
+
3
+ module DICOM
4
+
5
+ # This class contains code for handling the client side of DICOM TCP/IP network communication.
6
+ class DClient
7
+
8
+ attr_accessor :ae, :host_ae, :host_ip, :max_package_size, :port, :timeout, :verbose
9
+ attr_reader :command_results, :data_results, :errors, :notices
10
+
11
+ # Initialize the instance with a host adress and a port number.
12
+ def initialize(host_ip, port, options={})
13
+ require 'socket'
14
+ # Required parameters:
15
+ @host_ip = host_ip
16
+ @port = port
17
+ # Optional parameters (and default values):
18
+ @ae = options[:ae] || "RUBY_DICOM"
19
+ @lib = options[:lib] || DLibrary.new
20
+ @host_ae = options[:host_ae] || "DEFAULT"
21
+ @max_package_size = options[:max_package_size] || 32768 # 16384
22
+ @timeout = options[:timeout] || 10 # seconds
23
+ @min_length = 12 # minimum number of bytes to expect in an incoming transmission
24
+ @verbose = options[:verbose]
25
+ @verbose = true if @verbose == nil # Default verbosity is 'on'.
26
+ # Other instance variables:
27
+ @errors = Array.new # errors and warnings are put in this array
28
+ @notices = Array.new # information on successful transmissions are put in this array
29
+ # Variables used for monitoring state of transmission:
30
+ @connection = nil # TCP connection status
31
+ @association = nil # DICOM Association status
32
+ @request_approved = nil # Status of our DICOM request
33
+ @release = nil # Status of received, valid release response
34
+ # Results from a query:
35
+ @command_results = Array.new
36
+ @data_results = Array.new
37
+ # Set default values like transfer syntax, user information, endianness:
38
+ set_default_values
39
+ set_user_information_array
40
+ # Initialize the network package handler:
41
+ @link = Link.new(:ae => @ae, :host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout)
42
+ end
43
+
44
+
45
+ # Retrieve a dicom file from a service class provider (SCP/PACS).
46
+ # Example: get_image("c:/dicom/", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
47
+ def get_image(path, options={})
48
+ # Study Root Query/Retrieve Information Model - GET:
49
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.3"
50
+ # Transfer the current options to the data_elements hash:
51
+ set_command_fragment_get
52
+ # Prepare data elements for this operation:
53
+ set_data_fragment_get_image
54
+ set_data_options(options)
55
+ perform_get(path)
56
+ end
57
+
58
+
59
+ # Query a service class provider for images that match the specified criteria.
60
+ # Example: find_images("0010,0020" => "123456789", "0020,000D" => "1.2.840.1145.342", "0020,000E" => "1.3.6.1.4.1.2452.6.687844") # (Patient ID, Study Instance UID & Series Instance UID)
61
+ def find_images(options={})
62
+ # Study Root Query/Retrieve Information Model - FIND:
63
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.1"
64
+ # Prepare data elements for this operation:
65
+ set_data_fragment_find_images
66
+ set_data_options(options)
67
+ perform_find
68
+ end
69
+
70
+
71
+ # Query a service class provider for patients that match the specified criteria.
72
+ # Example: find_patients("0010,0010" => "James*") # (Patient's Name)
73
+ def find_patients(options={})
74
+ # Patient Root Query/Retrieve Information Model - FIND:
75
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.1.1"
76
+ # Prepare data elements for this operation:
77
+ set_data_fragment_find_patients
78
+ set_data_options(options)
79
+ perform_find
80
+ end
81
+
82
+
83
+ # Query a service class provider for series that match the specified criteria.
84
+ # Example: find_series("0010,0020" => "123456789", "0020,000D" => "1.2.840.1145.342") # (Patient ID & Study Instance UID)
85
+ def find_series(options={})
86
+ # Study Root Query/Retrieve Information Model - FIND:
87
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.1"
88
+ # Prepare data elements for this operation:
89
+ set_data_fragment_find_series
90
+ set_data_options(options)
91
+ perform_find
92
+ end
93
+
94
+
95
+ # Query a service class provider for studies that match the specified criteria.
96
+ # Example: find_studies("0008,0020" => "20090604-", "0010,000D" => "123456789") # (Study Date & Patient ID)
97
+ def find_studies(options={})
98
+ # Study Root Query/Retrieve Information Model - FIND:
99
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.1"
100
+ # Prepare data elements for this operation:
101
+ set_data_fragment_find_studies
102
+ set_data_options(options)
103
+ perform_find
104
+ end
105
+
106
+
107
+ # Move an image to a dicom node other than yourself.
108
+ # Example: move_image("MYDICOM", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
109
+ def move_image(destination, options={})
110
+ # Study Root Query/Retrieve Information Model - MOVE:
111
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.2"
112
+ # Transfer the current options to the data_elements hash:
113
+ set_command_fragment_move(destination)
114
+ # Prepare data elements for this operation:
115
+ set_data_fragment_move_image
116
+ set_data_options(options)
117
+ perform_move
118
+ end
119
+
120
+
121
+ # Move an entire study to a dicom node other than yourself.
122
+ # Example: move_study("MYDICOM", "0010,0020" => pat_id, "0020,000D" => study_uid)
123
+ def move_study(destination, options={})
124
+ # Study Root Query/Retrieve Information Model - MOVE:
125
+ @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.2"
126
+ # Transfer the current options to the data_elements hash:
127
+ set_command_fragment_move(destination)
128
+ # Prepare data elements for this operation:
129
+ set_data_fragment_move_study
130
+ set_data_options(options)
131
+ perform_move
132
+ end
133
+
134
+
135
+ # Send a DICOM file to a service class provider (SCP/PACS).
136
+ def send(file_path)
137
+ # Load the DICOM file from the specified path:
138
+ obj = DObject.new(file_path, :verbose => false, :lib => @lib)
139
+ if obj.read_success
140
+ # Get the SOP Class UID (abstract syntax) from the DICOM obj:
141
+ @abstract_syntax = obj.get_value("0008,0016")
142
+ # Get the Transfer Syntax UID from the DICOM obj,
143
+ # and if not available, set to default: Implicit, Little endian
144
+ @transfer_syntax = [obj.get_value("0002,0010")] || ["1.2.840.10008.1.2"]
145
+ # Open a DICOM link:
146
+ establish_association
147
+ if @association
148
+ if @request_approved
149
+ # Continue with our c-store operation, since our request was accepted.
150
+ # Prepare the DICOM object for transmission:
151
+ obj.encode_segments(@max_pdu_length - 14)
152
+ # Handle the transmission:
153
+ perform_send(obj)
154
+ end
155
+ end
156
+ else
157
+ # Failed to read DICOM file. Can not transmit this file.
158
+ add_error("Error: The supplied file was not recognised as a valid DICOM file. File NOT transmitted. (file: #{file_path}")
159
+ end
160
+ # Close the DICOM link:
161
+ establish_release
162
+ end
163
+
164
+
165
+ # Tests the connection to the specified host by trying to negotiate an association, then releasing it.
166
+ def test
167
+ add_notice("TESTING CONNECTION...")
168
+ success = false
169
+ # Verification SOP Class:
170
+ @abstract_syntax = "1.2.840.10008.1.1"
171
+ # Open a DICOM link:
172
+ establish_association
173
+ if @association
174
+ if @request_approved
175
+ success = true
176
+ end
177
+ # Close the DICOM link:
178
+ establish_release
179
+ end
180
+ if success
181
+ add_notice("TEST SUCCSESFUL!")
182
+ else
183
+ add_error("TEST FAILED!")
184
+ end
185
+ return success
186
+ end
187
+
188
+
189
+ # Following methods are private:
190
+ private
191
+
192
+
193
+ # Adds a warning or error message to the instance array holding messages,
194
+ # and if verbose variable is true, prints the message as well.
195
+ def add_error(error)
196
+ puts error if @verbose
197
+ @errors << error
198
+ end
199
+
200
+
201
+ # Adds a notice (information regarding progress or successful communications) to the instance array,
202
+ # and if verbosity is set for these kinds of messages, prints it to the screen as well.
203
+ def add_notice(notice)
204
+ puts notice if @verbose
205
+ @notices << notice
206
+ end
207
+
208
+
209
+ # Open a TCP session with a specified server, and handle the association request along with its response.
210
+ def establish_association
211
+ # Reset some variables:
212
+ @association = false
213
+ @request_approved = false
214
+ # Initiate the association:
215
+ @link.build_association_request(@application_context_uid, @abstract_syntax, @transfer_syntax, @user_information)
216
+ @connection = TCPSocket.new(@host_ip, @port)
217
+ @link.transmit(@connection)
218
+ info = @link.receive_single_transmission(@connection).first
219
+ # Interpret the results:
220
+ if info[:valid]
221
+ if info[:pdu] == "02"
222
+ # Values of importance are extracted and put into instance variables:
223
+ @association = true
224
+ @max_pdu_length = info[:max_pdu_length]
225
+ @presentation_context_id = info[:presentation_context_id]
226
+ add_notice("Association successfully negotiated with host #{host_ae} (#{host_ip}).")
227
+ else
228
+ add_error("Association was denied from host #{host_ae} (#{host_ip})!")
229
+ end
230
+ if info[:result] == 0
231
+ @request_approved = true
232
+ add_notice("Your request was accepted by host #{host_ae} (#{host_ip}).")
233
+ else
234
+ add_error("Your request was denied by host #{host_ae} (#{host_ip})!")
235
+ end
236
+ end
237
+ end
238
+
239
+
240
+ # Handle a release request and its response, as well as closing the TCP connection.
241
+ def establish_release
242
+ @release = false
243
+ if @abort
244
+ @connection.close unless @connection.closed?
245
+ add_notice("Association has been closed. (#{host_ae}, #{host_ip})")
246
+ else
247
+ unless @connection.closed?
248
+ @link.build_release_request
249
+ @link.transmit(@connection)
250
+ info = @link.receive_single_transmission(@connection).first
251
+ @connection.close
252
+ if info[:pdu] == "06"
253
+ add_notice("Association released properly from host #{host_ae} (#{host_ip}).")
254
+ else
255
+ add_error("Association was NOT released properly for some reason from host #{host_ae} (#{host_ip})!")
256
+ end
257
+ else
258
+ add_error("Connection was closed by the host (for some unknown reason) before the association could be released properly.")
259
+ end
260
+ end
261
+ @abort = false
262
+ end
263
+
264
+
265
+ # Handle the communication involved in DICOM query (C-FIND).
266
+ # Build the necessary strings and send the command and data element that makes up the query.
267
+ # Listens for and interpretes the incoming query responses.
268
+ def perform_find
269
+ # Open a DICOM link:
270
+ establish_association
271
+ if @association
272
+ if @request_approved
273
+ # Continue with our query, since the request was accepted.
274
+ # Set the query command elements array:
275
+ set_command_fragment_find
276
+ pdu="04"
277
+ #context = "01"
278
+ flags = "03"
279
+ @link.build_command_fragment(pdu, @presentation_context_id, flags, @command_elements)
280
+ @link.transmit(@connection)
281
+ @link.build_data_fragment(@data_elements)
282
+ @link.transmit(@connection)
283
+ # A query response will typically be sent in multiple, separate packets.
284
+ # Listen for incoming responses and interpret them individually, until we have received the last command fragment.
285
+ segments = @link.receive_multiple_transmissions(@connection)
286
+ process_returned_data(segments)
287
+ end
288
+ # Close the DICOM link:
289
+ establish_release
290
+ end
291
+ end
292
+
293
+
294
+ # Build and send command & data fragment, then receive the incoming file data.
295
+ def perform_get(path)
296
+ # Open a DICOM link:
297
+ establish_association
298
+ if @association
299
+ if @request_approved
300
+ # Continue with our operation, since the request was accepted.
301
+ pdu="04"
302
+ flags = "03"
303
+ @link.build_command_fragment(pdu, @presentation_context_id, flags, @command_elements)
304
+ @link.transmit(@connection)
305
+ @link.build_data_fragment(@data_elements) # (uses flag = 02)
306
+ @link.transmit(@connection)
307
+ # Listen for incoming file data:
308
+ @link.handle_incoming_data(@connection, path)
309
+ # Send confirmation response:
310
+ @link.handle_response(@connection)
311
+ end
312
+ # Close the DICOM link:
313
+ establish_release
314
+ end
315
+ end
316
+
317
+
318
+ # Handle the communication involved in DICOM move request.
319
+ def perform_move
320
+ # Open a DICOM link:
321
+ establish_association
322
+ if @association
323
+ if @request_approved
324
+ # Continue with our operation, since the request was accepted.
325
+ pdu="04"
326
+ flags = "03"
327
+ @link.build_command_fragment(pdu, @presentation_context_id, flags, @command_elements)
328
+ @link.transmit(@connection)
329
+ flags = "02"
330
+ @link.build_data_fragment(@data_elements)
331
+ @link.transmit(@connection)
332
+ # Receive confirmation response:
333
+ segments = @link.receive_single_transmission(@connection)
334
+ process_returned_data(segments)
335
+ end
336
+ # Close the DICOM link:
337
+ establish_release
338
+ end
339
+ end
340
+
341
+
342
+ # Builds and sends the command fragment, then builds and sends the data fragments that
343
+ # conveys the information from the original DICOM file.
344
+ def perform_send(obj)
345
+ # Set the command array to be used:
346
+ sop_uid = obj.get_value("SOP Instance UID") # 0008,0018
347
+ if sop_uid
348
+ set_command_fragment_store(sop_uid)
349
+ pdu_type = "04"
350
+ flags = "03"
351
+ @link.build_command_fragment(pdu_type, @presentation_context_id, flags, @command_elements)
352
+ @link.transmit(@connection)
353
+ # Transmit all but the last segments:
354
+ flags = "00"
355
+ (0..obj.segments.length-2).each do |i|
356
+ @link.build_storage_fragment(pdu_type, @presentation_context_id, flags, obj.segments[i])
357
+ @link.transmit(@connection)
358
+ end
359
+ # Transmit the last segment:
360
+ flags = "02"
361
+ @link.build_storage_fragment(pdu_type, @presentation_context_id, flags, obj.segments.last)
362
+ @link.transmit(@connection)
363
+ # Receive confirmation response:
364
+ segments = @link.receive_single_transmission(@connection)
365
+ process_returned_data(segments)
366
+ else
367
+ add_error("Error: Unable to extract SOP Instance UID for the given DICOM file. File will not be sent to its destination.")
368
+ end
369
+ end
370
+
371
+
372
+ # Process the data that was returned from the interaction with the SCP and make it available to the user.
373
+ def process_returned_data(segments)
374
+ # Reset command results arrays:
375
+ @command_results = Array.new
376
+ @data_results = Array.new
377
+ # Try to extract data:
378
+ segments.each do |info|
379
+ if info[:valid]
380
+ # Determine if it is command or data:
381
+ if info[:presentation_context_flag] == "03"
382
+ # Command (last fragment):
383
+ @command_results << info[:results]
384
+ elsif info[:presentation_context_flag] == "02"
385
+ # Data (last fragment)
386
+ @data_results << info[:results]
387
+ end
388
+ end
389
+ end
390
+ end
391
+
392
+
393
+ # Reset the values of a array.
394
+ # It is assumed the arrays elements are an array in itself, where element[1]
395
+ # will be reset to the string value "".
396
+ def reset(array)
397
+ array.each do |element|
398
+ element[1] = ""
399
+ end
400
+ end
401
+
402
+
403
+ # Set command elements used in a C-GET-RQ:
404
+ def set_command_fragment_get
405
+ @command_elements = [
406
+ ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
407
+ ["0000,0100", "US", 16], # Command Field: 16 (C-GET-RQ)
408
+ ["0000,0600", "AE", destination], # Move destination
409
+ ["0000,0700", "US", 0], # Priority: 0: medium
410
+ ["0000,0800", "US", 1] # Data Set Type: 1
411
+ ]
412
+ end
413
+
414
+
415
+ # Command elements used in a C-FIND-RQ.
416
+ # This seems to be the same, regardless of what we want to query.
417
+ def set_command_fragment_find
418
+ @command_elements = [
419
+ ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
420
+ ["0000,0100", "US", 32], # Command Field: 32 (C-FIND-RQ)
421
+ ["0000,0110", "US", 1], # Message ID: 1
422
+ ["0000,0700", "US", 0], # Priority: 0: medium
423
+ ["0000,0800", "US", 1] # Data Set Type: 1
424
+ ]
425
+ end
426
+
427
+
428
+ # Set command elements used in a C-MOVE-RQ:
429
+ def set_command_fragment_move(destination)
430
+ @command_elements = [
431
+ ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
432
+ ["0000,0100", "US", 33], # Command Field: 33 (C-MOVE-RQ)
433
+ ["0000,0110", "US", 1], # Message ID: 1
434
+ ["0000,0600", "AE", destination], # Move destination
435
+ ["0000,0700", "US", 0], # Priority: 0: medium
436
+ ["0000,0800", "US", 1] # Data Set Type: 1
437
+ ]
438
+ end
439
+
440
+
441
+ # Command elements used in a p-data c-store-rq query command:
442
+ def set_command_fragment_store(sop_uid)
443
+ @command_elements = [
444
+ ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
445
+ ["0000,0100", "US", 1], # Command Field: 1 (C-STORE-RQ)
446
+ ["0000,0110", "US", 1], # Message ID: 1
447
+ ["0000,0700", "US", 0], # Priority: 0: medium
448
+ ["0000,0800", "US", 1], # Data Set Type: 1
449
+ ["0000,1000", "UI", sop_uid] # Affected SOP Instance UID
450
+ ]
451
+ end
452
+
453
+
454
+ # Data elements used in a query for the images of a particular series:
455
+ def set_data_fragment_find_images
456
+ @data_elements = [
457
+ ["0008,0018", ""], # SOP Instance UID
458
+ ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
459
+ ["0020,000D", ""], # Study Instance UID
460
+ ["0020,000E", ""], # Series Instance UID
461
+ ["0020,0013", ""] # Instance Number
462
+ ]
463
+ end
464
+
465
+
466
+ # Data elements used in a query for patients:
467
+ def set_data_fragment_find_patients
468
+ @data_elements = [
469
+ ["0008,0052", "PATIENT"], # Query/Retrieve Level: "PATIENT"
470
+ ["0010,0010", ""], # Patient's Name
471
+ ["0010,0020", ""], # Patient ID
472
+ ["0010,0030", ""], # Patient's Birth Date
473
+ ["0010,0040", ""] # Patient's Sex
474
+ ]
475
+ end
476
+
477
+
478
+ # Data elements used in a query for the series of a particular study:
479
+ def set_data_fragment_find_series
480
+ @data_elements = [
481
+ ["0008,0052", "SERIES"], # Query/Retrieve Level: "SERIES"
482
+ ["0008,0060", ""], # Modality
483
+ ["0008,103E", ""], # Series Description
484
+ ["0020,000D", ""], # Study Instance UID
485
+ ["0020,000E", ""], # Series Instance UID
486
+ ["0020,0011", ""] # Series Number
487
+ ]
488
+ end
489
+
490
+
491
+ # Data elements used in a query for studies:
492
+ def set_data_fragment_find_studies
493
+ @data_elements = [
494
+ ["0008,0020", ""], # Study Date
495
+ ["0008,0030", ""], # Study Time
496
+ ["0008,0050", ""], # Accession Number
497
+ ["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
498
+ ["0008,0090", ""], # Referring Physician's Name
499
+ ["0008,1030", ""], # Study Description
500
+ ["0008,1060", ""], # Name of Physician(s) Reading Study
501
+ ["0010,0010", ""], # Patient's Name
502
+ ["0010,0020", ""], # Patient ID
503
+ ["0010,0030", ""], # Patient's Birth Date
504
+ ["0010,0040", ""], # Patient's Sex
505
+ ["0020,000D", ""], # Study Instance UID
506
+ ["0020,0010", ""] # Study ID
507
+ ]
508
+ end
509
+
510
+
511
+ # Set data elements used for an image C-GET-RQ:
512
+ def set_data_fragment_get_image
513
+ @data_elements = [
514
+ ["0008,0018", ""], # SOP Instance UID
515
+ ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
516
+ ["0020,000D", ""], # Study Instance UID
517
+ ["0020,000E", ""] # Series Instance UID
518
+ ]
519
+ end
520
+
521
+
522
+ # Set data elements used for an image C-MOVE-RQ:
523
+ def set_data_fragment_move_image
524
+ @data_elements = [
525
+ ["0008,0018", ""], # SOP Instance UID
526
+ ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
527
+ ["0020,000D", ""], # Study Instance UID
528
+ ["0020,000E", ""] # Series Instance UID
529
+ ]
530
+ end
531
+
532
+
533
+ # Set data elements used in a study C-MOVE-RQ:
534
+ def set_data_fragment_move_study
535
+ @data_elements = [
536
+ ["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
537
+ ["0010,0020", ""], # Patient ID
538
+ ["0020,000D", ""] # Study Instance UID
539
+ ]
540
+ end
541
+
542
+
543
+ # Transfer the user query options to the data elements array.
544
+ # NB: Only tags which are predefined for the specific query type will be updated
545
+ # (no new tags are allowed stored among the data elements)
546
+ def set_data_options(options)
547
+ options.each_pair do |key, value|
548
+ tags = @data_elements.transpose[0]
549
+ i = tags.index(key)
550
+ if i
551
+ @data_elements[i][1] = value
552
+ end
553
+ end
554
+ end
555
+
556
+
557
+ # Set default values for accepted transfer syntaxes:
558
+ def set_default_values
559
+ # DICOM Application Context Name (unknown if this will vary or is always the same):
560
+ @application_context_uid = "1.2.840.10008.3.1.1.1"
561
+ # Transfer syntax (preferred syntax appearing first)
562
+ @transfer_syntax = ["1.2.840.10008.1.2.1", # Explicit VR Little Endian
563
+ "1.2.840.10008.1.2.2", # Explicit VR Big Endian
564
+ "1.2.840.10008.1.2" # Implicit VR Little Endian
565
+ ]
566
+ end
567
+
568
+
569
+ # Set user information [item type code, vr/type, value]
570
+ def set_user_information_array
571
+ @user_information = [
572
+ ["51", "UL", @max_package_size], # Max PDU Length
573
+ ["52", "STR", "1.2.826.0.1.3680043.8.641"], # Implementation UID
574
+ ["55", "STR", "RUBY_DICOM"] # Implementation Version
575
+ ]
576
+ end
577
+
578
+ end # of class
579
+ end # of module