dicom 0.6.1 → 0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +42 -20
- data/DOCUMENTATION +117 -71
- data/README +3 -3
- data/lib/dicom.rb +23 -12
- data/lib/{Anonymizer.rb → dicom/Anonymizer.rb} +101 -79
- data/lib/{DClient.rb → dicom/DClient.rb} +12 -11
- data/lib/{DLibrary.rb → dicom/DLibrary.rb} +53 -31
- data/lib/dicom/DObject.rb +1579 -0
- data/lib/{DRead.rb → dicom/DRead.rb} +42 -43
- data/lib/{DServer.rb → dicom/DServer.rb} +34 -20
- data/lib/{DWrite.rb → dicom/DWrite.rb} +27 -31
- data/lib/{Dictionary.rb → dicom/Dictionary.rb} +434 -32
- data/lib/dicom/FileHandler.rb +50 -0
- data/lib/{Link.rb → dicom/Link.rb} +312 -167
- data/lib/{Stream.rb → dicom/Stream.rb} +1 -1
- data/lib/dicom/ruby_extensions.rb +47 -0
- metadata +16 -15
- data/lib/DObject.rb +0 -1194
- data/lib/ruby_extensions.rb +0 -36
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright 2010 Christoffer Lervag
|
2
|
+
|
3
|
+
# The purpose of this file is to make it very easy for users to customise the way
|
4
|
+
# DICOM files are handled when they are received through the network.
|
5
|
+
# The default behaviour is to save the file to disk using a folder structure determined by the file's DICOM tags.
|
6
|
+
# Some suggested alternatives:
|
7
|
+
# - Analyzing tags and/or image data to determine further actions.
|
8
|
+
# - Modify the DICOM object before it is saved to disk.
|
9
|
+
# - Modify the folder structure in which DICOM files are saved to disk.
|
10
|
+
# - Store DICOM contents in a database (highly relevant if you are building a Ruby on Rails application).
|
11
|
+
# - Retransmit the DICOM object to another network destination using the DClient class.
|
12
|
+
# - Write information to a log file.
|
13
|
+
|
14
|
+
module DICOM
|
15
|
+
|
16
|
+
# This class handles DICOM files that have been received through network communication.
|
17
|
+
class FileHandler
|
18
|
+
|
19
|
+
# Handles the reception of a DICOM file.
|
20
|
+
# Default action: Save to disk.
|
21
|
+
# Modify this method if you want a different behaviour!
|
22
|
+
def self.receive_file(obj, path_prefix, transfer_syntax)
|
23
|
+
# Did we receive a valid DICOM file?
|
24
|
+
if obj.read_success
|
25
|
+
# File name is set using the SOP Instance UID
|
26
|
+
file_name = obj.get_value("0008,0018") || "no_SOP_UID.dcm"
|
27
|
+
# File will be saved with the following path:
|
28
|
+
# path_prefix/<PatientID>/<StudyDate>/<Modality>/
|
29
|
+
folders = Array.new(3)
|
30
|
+
folders[0] = obj.get_value("0010,0020") || "PatientID"
|
31
|
+
folders[1] = obj.get_value("0008,0020") || "StudyDate"
|
32
|
+
folders[2] = obj.get_value("0008,0060") || "Modality"
|
33
|
+
local_path = folders.join(File::SEPARATOR) + File::SEPARATOR + file_name
|
34
|
+
full_path = path_prefix + local_path
|
35
|
+
# Save the DICOM object to disk:
|
36
|
+
obj.write(full_path, transfer_syntax)
|
37
|
+
# As the file has been received successfully, set the success boolean and a corresponding 'success string':
|
38
|
+
success = true
|
39
|
+
message = "DICOM file saved to: #{full_path}"
|
40
|
+
else
|
41
|
+
# Received data was not successfully read as a DICOM file.
|
42
|
+
success = false
|
43
|
+
message = "Error: The received file was not successfully parsed as a DICOM object."
|
44
|
+
end
|
45
|
+
# A boolean indicating success/failure, and a message string must be returned:
|
46
|
+
return success, message
|
47
|
+
end
|
48
|
+
|
49
|
+
end # of class
|
50
|
+
end # of module
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2009 Christoffer Lervag
|
1
|
+
# Copyright 2009-2010 Christoffer Lervag
|
2
2
|
|
3
3
|
module DICOM
|
4
4
|
|
@@ -6,15 +6,15 @@ module DICOM
|
|
6
6
|
# as well as network communication.
|
7
7
|
class Link
|
8
8
|
|
9
|
-
attr_accessor :max_package_size, :verbose
|
9
|
+
attr_accessor :max_package_size, :verbose, :file_handler
|
10
10
|
attr_reader :errors, :notices
|
11
11
|
|
12
12
|
# Initialize the instance with a host adress and a port number.
|
13
13
|
def initialize(options={})
|
14
14
|
require 'socket'
|
15
15
|
# Optional parameters (and default values):
|
16
|
+
@file_handler = options[:file_handler] || FileHandler
|
16
17
|
@ae = options[:ae] || "RUBY_DICOM"
|
17
|
-
@lib = options[:lib] || DLibrary.new
|
18
18
|
@host_ae = options[:host_ae] || "DEFAULT"
|
19
19
|
@max_package_size = options[:max_package_size] || 32768 # 16384
|
20
20
|
@max_receive_size = @max_package_size
|
@@ -38,36 +38,47 @@ module DICOM
|
|
38
38
|
|
39
39
|
# Build the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
|
40
40
|
# For the moment: NO REASONS WILL BE PROVIDED. (and source of problems will always be set as client side)
|
41
|
-
def
|
41
|
+
def build_association_abort
|
42
42
|
# Big endian encoding:
|
43
43
|
@outgoing.set_endian(@net_endian)
|
44
44
|
# Clear the outgoing binary string:
|
45
45
|
@outgoing.reset
|
46
|
+
pdu = "07"
|
46
47
|
# Reserved (2 bytes)
|
47
|
-
@outgoing.
|
48
|
+
@outgoing.encode_last("00"*2, "HEX")
|
48
49
|
# Source (1 byte)
|
49
50
|
source = "00" # (client side error)
|
50
|
-
@outgoing.
|
51
|
+
@outgoing.encode_last(source, "HEX")
|
51
52
|
# Reason/Diag. (1 byte)
|
52
53
|
reason = "00" # (Reason not specified)
|
53
|
-
@outgoing.
|
54
|
+
@outgoing.encode_last(reason, "HEX")
|
55
|
+
append_header(pdu)
|
54
56
|
end
|
55
57
|
|
56
58
|
|
57
59
|
# Build the binary string that will be sent as TCP data in the Association accept response.
|
58
|
-
def build_association_accept(
|
60
|
+
def build_association_accept(info, ac_uid, ui, result)
|
59
61
|
# Big endian encoding:
|
60
62
|
@outgoing.set_endian(@net_endian)
|
61
63
|
# Clear the outgoing binary string:
|
62
64
|
@outgoing.reset
|
63
65
|
# Set item types (pdu and presentation context):
|
64
66
|
pdu = "02"
|
65
|
-
|
67
|
+
pc_type = "21"
|
66
68
|
# No abstract syntax in association response:
|
67
|
-
|
69
|
+
abstract_syntax = nil
|
68
70
|
# Note: The order of which these components are built is not arbitrary.
|
69
71
|
append_application_context(ac_uid)
|
70
|
-
|
72
|
+
# Return one presentation context for each of the proposed abstract syntaxes:
|
73
|
+
abstract_syntaxes = Array.new
|
74
|
+
info[:pc].each do |pc|
|
75
|
+
unless abstract_syntaxes.include?(pc[:abstract_syntax])
|
76
|
+
abstract_syntaxes << pc[:abstract_syntax]
|
77
|
+
context_id = pc[:presentation_context_id]
|
78
|
+
transfer_syntax = pc[:ts].first[:transfer_syntax]
|
79
|
+
append_presentation_context(abstract_syntax, pc_type, transfer_syntax, context_id, result)
|
80
|
+
end
|
81
|
+
end
|
71
82
|
append_user_information(ui)
|
72
83
|
# Header must be built last, because we need to know the length of the other components.
|
73
84
|
append_association_header(pdu)
|
@@ -76,7 +87,7 @@ module DICOM
|
|
76
87
|
|
77
88
|
# Build the binary string that will be sent as TCP data in the association rejection.
|
78
89
|
# NB: For the moment, this method will only customize the "reason" value.
|
79
|
-
# For a list of error codes, see the official dicom
|
90
|
+
# For a list of error codes, see the official dicom PS3.8 document, page 41.
|
80
91
|
def build_association_reject(info)
|
81
92
|
# Big endian encoding:
|
82
93
|
@outgoing.set_endian(@net_endian)
|
@@ -107,6 +118,7 @@ module DICOM
|
|
107
118
|
pdu = "01"
|
108
119
|
pc = "20"
|
109
120
|
# Note: The order of which these components are built is not arbitrary.
|
121
|
+
# (The first three are built 'in order of appearance', the header is built last, but is put first in the message)
|
110
122
|
append_application_context(ac_uid)
|
111
123
|
append_presentation_context(as, pc, ts)
|
112
124
|
append_user_information(ui)
|
@@ -161,10 +173,9 @@ module DICOM
|
|
161
173
|
|
162
174
|
|
163
175
|
# Build the binary string that will be sent as TCP data in the query data fragment.
|
164
|
-
#
|
165
|
-
# It might go wrong if an implicit data stream is encountered!
|
176
|
+
# The style of encoding will depend on whether we have an implicit or explicit transfer syntax.
|
166
177
|
def build_data_fragment(data_elements)
|
167
|
-
#
|
178
|
+
# Endianness of data fragment:
|
168
179
|
@outgoing.set_endian(@data_endian)
|
169
180
|
# Clear the outgoing binary string:
|
170
181
|
@outgoing.reset
|
@@ -174,13 +185,19 @@ module DICOM
|
|
174
185
|
# Encode all tags (even tags which are empty):
|
175
186
|
# Tag (4 bytes)
|
176
187
|
@outgoing.add_last(@outgoing.encode_tag(element[0]))
|
177
|
-
#
|
178
|
-
vr =
|
179
|
-
@outgoing.encode_last(vr, "STR")
|
180
|
-
# Encode the value first, so we know its length:
|
188
|
+
# Encode the value in advance of putting it into the message, so we know its length:
|
189
|
+
vr = LIBRARY.get_name_vr(element[0])[1]
|
181
190
|
value = @outgoing.encode_value(element[1], vr)
|
182
|
-
|
183
|
-
|
191
|
+
if @explicit
|
192
|
+
# Type (VR) (2 bytes)
|
193
|
+
@outgoing.encode_last(vr, "STR")
|
194
|
+
# Length (2 bytes)
|
195
|
+
@outgoing.encode_last(value.length, "US")
|
196
|
+
else
|
197
|
+
# Implicit:
|
198
|
+
# Length (4 bytes)
|
199
|
+
@outgoing.encode_last(value.length, "UL")
|
200
|
+
end
|
184
201
|
# Value (variable length)
|
185
202
|
@outgoing.add_last(value)
|
186
203
|
end
|
@@ -247,6 +264,18 @@ module DICOM
|
|
247
264
|
end
|
248
265
|
|
249
266
|
|
267
|
+
# Extracts the abstrax syntax from the first presentation context in the info hash object:
|
268
|
+
def extract_abstract_syntax(info)
|
269
|
+
return info[:pc].first[:abstract_syntax]
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
# Extracts the (first) transfer syntax from the first presentation context in the info hash object:
|
274
|
+
def extract_transfer_syntax(info)
|
275
|
+
return info[:pc].first[:ts].first[:transfer_syntax]
|
276
|
+
end
|
277
|
+
|
278
|
+
|
250
279
|
# Delegates an incoming message to its correct interpreter method, based on pdu type.
|
251
280
|
def forward_to_interpret(message, pdu, file = nil)
|
252
281
|
case pdu
|
@@ -282,10 +311,11 @@ module DICOM
|
|
282
311
|
|
283
312
|
# Handles the association accept.
|
284
313
|
def handle_association_accept(session, info, syntax_result)
|
314
|
+
# Update the variable for calling ae (information gathered in the association request):
|
315
|
+
@ae = info[:calling_ae]
|
285
316
|
application_context = info[:application_context]
|
286
|
-
|
287
|
-
|
288
|
-
build_association_accept(application_context, transfer_syntax, @user_information, syntax_result)
|
317
|
+
set_user_information_array(info)
|
318
|
+
build_association_accept(info, application_context, @user_information, syntax_result)
|
289
319
|
transmit(session)
|
290
320
|
end
|
291
321
|
|
@@ -315,19 +345,19 @@ module DICOM
|
|
315
345
|
end
|
316
346
|
end
|
317
347
|
data = file_data.join
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
return
|
348
|
+
if data.length > 8
|
349
|
+
# Read the received data stream and load it as a DICOM object:
|
350
|
+
obj = DObject.new(data, :bin => true, :syntax => @transfer_syntax)
|
351
|
+
# The actual handling of the DICOM object and (processing, saving, database storage, retransmission, etc)
|
352
|
+
# is handled by the external FileHandler class, in order to make it as easy as possible for users to write
|
353
|
+
# their own customised solutions for handling the incoming DICOM files:
|
354
|
+
success, message = @file_handler.receive_file(obj, path, @transfer_syntax)
|
355
|
+
else
|
356
|
+
# Valid DICOM data not received:
|
357
|
+
success = false
|
358
|
+
message = "Error: Invalid data received (the data was too small to be a valid DICOM file)."
|
359
|
+
end
|
360
|
+
return success, message
|
331
361
|
end
|
332
362
|
|
333
363
|
|
@@ -403,9 +433,14 @@ module DICOM
|
|
403
433
|
fragment = msg.extract(specified_length)
|
404
434
|
info = forward_to_interpret(fragment, pdu, file)
|
405
435
|
info[:pdu] = pdu
|
436
|
+
segments << info
|
437
|
+
# It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information:
|
438
|
+
if info[:rest_string]
|
439
|
+
additional_info = forward_to_interpret(info[:rest_string], pdu, file)
|
440
|
+
segments << additional_info
|
441
|
+
end
|
406
442
|
# The information gathered from the interpretation is appended to a segments array,
|
407
443
|
# and in the case of a recursive call some special logic is needed to build this array in the expected fashion.
|
408
|
-
segments << info
|
409
444
|
remaining_segments = interpret(msg.rest_string, file)
|
410
445
|
remaining_segments.each do |remaining|
|
411
446
|
segments << remaining
|
@@ -416,8 +451,13 @@ module DICOM
|
|
416
451
|
info = forward_to_interpret(fragment, pdu, file)
|
417
452
|
info[:pdu] = pdu
|
418
453
|
segments << info
|
454
|
+
# It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information:
|
455
|
+
if info[:rest_string]
|
456
|
+
additional_info = forward_to_interpret(info[:rest_string], pdu, file)
|
457
|
+
segments << additional_info
|
458
|
+
end
|
419
459
|
else
|
420
|
-
# Length of the message is less than what is specified in the message.
|
460
|
+
# Length of the message is less than what is specified in the message. Need to listen for more. This is hopefully handled properly now.
|
421
461
|
#add_error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.")
|
422
462
|
@first_part = msg.string
|
423
463
|
end
|
@@ -440,34 +480,9 @@ module DICOM
|
|
440
480
|
# Reason/Diag. (1 byte)
|
441
481
|
info[:reason] = msg.decode(1, "HEX")
|
442
482
|
# Analyse the results:
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
add_error("Warning: Connection has been aborted by the service provider because of an error by the service provider (server side).")
|
447
|
-
else
|
448
|
-
add_error("Warning: Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{info[:source]})")
|
449
|
-
end
|
450
|
-
if info[:source] != "00"
|
451
|
-
# Display reason for error:
|
452
|
-
case info[:reason]
|
453
|
-
when "00"
|
454
|
-
add_error("Reason specified for abort: Reason not specified")
|
455
|
-
when "01"
|
456
|
-
add_error("Reason specified for abort: Unrecognized PDU")
|
457
|
-
when "02"
|
458
|
-
add_error("Reason specified for abort: Unexpected PDU")
|
459
|
-
when "04"
|
460
|
-
add_error("Reason specified for abort: Unrecognized PDU parameter")
|
461
|
-
when "05"
|
462
|
-
add_error("Reason specified for abort: Unexpected PDU parameter")
|
463
|
-
when "06"
|
464
|
-
add_error("Reason specified for abort: Invalid PDU parameter value")
|
465
|
-
else
|
466
|
-
add_error("Reason specified for abort: Unknown reason (Error code: #{info[:reason]})")
|
467
|
-
end
|
468
|
-
end
|
469
|
-
@listen = false
|
470
|
-
@receive = false
|
483
|
+
process_source(info[:source])
|
484
|
+
process_reason(info[:reason])
|
485
|
+
stop_receiving
|
471
486
|
@abort = true
|
472
487
|
info[:valid] = true
|
473
488
|
return info
|
@@ -510,21 +525,7 @@ module DICOM
|
|
510
525
|
msg.skip(1)
|
511
526
|
# Result (& Reason) (1 byte)
|
512
527
|
info[:result] = msg.decode(1, "BY")
|
513
|
-
|
514
|
-
unless info[:result] == 0
|
515
|
-
case info[:result]
|
516
|
-
when 1
|
517
|
-
add_error("Warning: DICOM Request was rejected by the host, reason: 'User-rejection'")
|
518
|
-
when 2
|
519
|
-
add_error("Warning: DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
|
520
|
-
when 3
|
521
|
-
add_error("Warning: DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
|
522
|
-
when 4
|
523
|
-
add_error("Warning: DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
|
524
|
-
else
|
525
|
-
add_error("Warning: DICOM Request was rejected by the host, reason: 'UNKNOWN (#{info[:result]})' (Illegal reason provided)")
|
526
|
-
end
|
527
|
-
end
|
528
|
+
process_result(info[:result])
|
528
529
|
# Reserved (1 byte)
|
529
530
|
msg.skip(1)
|
530
531
|
# Transfer syntax sub-item:
|
@@ -537,7 +538,7 @@ module DICOM
|
|
537
538
|
# Transfer syntax name (variable length)
|
538
539
|
info[:transfer_syntax] = msg.decode(info[:transfer_syntax_item_length], "STR")
|
539
540
|
# USER INFORMATION:
|
540
|
-
# Item type (1 byte)
|
541
|
+
# Item type (1 byte) ("50")
|
541
542
|
info[:user_info_item_type] = msg.decode(1, "HEX")
|
542
543
|
# Reserved (1 byte)
|
543
544
|
msg.skip(1)
|
@@ -556,14 +557,21 @@ module DICOM
|
|
556
557
|
@max_receive_size = info[:max_pdu_length]
|
557
558
|
when "52"
|
558
559
|
info[:implementation_class_uid] = msg.decode(item_length, "STR")
|
560
|
+
when "53"
|
561
|
+
# Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
|
562
|
+
info[:maxnum_operations_invoked] = msg.decode(2, "US")
|
563
|
+
info[:maxnum_operations_performed] = msg.decode(2, "US")
|
559
564
|
when "55"
|
560
565
|
info[:implementation_version] = msg.decode(item_length, "STR")
|
561
566
|
else
|
567
|
+
# Value (variable length)
|
568
|
+
value = msg.decode(item_length, "STR")
|
562
569
|
add_error("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
|
563
570
|
end
|
564
571
|
end
|
572
|
+
stop_receiving
|
565
573
|
info[:valid] = true
|
566
|
-
# Update
|
574
|
+
# Update transfer syntax settings for this instance:
|
567
575
|
set_transfer_syntax(info[:transfer_syntax])
|
568
576
|
return info
|
569
577
|
end # of interpret_association_accept
|
@@ -581,7 +589,8 @@ module DICOM
|
|
581
589
|
info[:source] = msg.decode(1, "BY")
|
582
590
|
# Reason (1 byte)
|
583
591
|
info[:reason] = msg.decode(1, "BY")
|
584
|
-
add_error("Warning: ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM
|
592
|
+
add_error("Warning: ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM PS3.8: Table 9-21 for details.)")
|
593
|
+
stop_receiving
|
585
594
|
info[:valid] = true
|
586
595
|
return info
|
587
596
|
end
|
@@ -611,49 +620,71 @@ module DICOM
|
|
611
620
|
# Application context (variable length)
|
612
621
|
info[:application_context] = msg.decode(info[:application_item_length], "STR")
|
613
622
|
# PRESENTATION CONTEXT:
|
614
|
-
#
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
info[:presentation_item_length] = msg.decode(2, "US")
|
620
|
-
# Presentation context ID (1 byte)
|
621
|
-
info[:presentation_context_id] = msg.decode(1, "HEX")
|
622
|
-
# Reserved (3 bytes)
|
623
|
-
msg.skip(3)
|
624
|
-
# ABSTRACT SYNTAX SUB-ITEM:
|
625
|
-
# Abstract syntax item type (1 byte)
|
626
|
-
info[:abstract_syntax_item_type] = msg.decode(1, "HEX") # "30"
|
627
|
-
# Reserved (1 byte)
|
628
|
-
msg.skip(1)
|
629
|
-
# Abstract syntax item length (2 bytes)
|
630
|
-
info[:abstract_syntax_item_length] = msg.decode(2, "US")
|
631
|
-
# Abstract syntax (variable length)
|
632
|
-
info[:abstract_syntax] = msg.decode(info[:abstract_syntax_item_length], "STR")
|
633
|
-
## TRANSFER SYNTAX SUB-ITEM(S):
|
634
|
-
item_type = "40"
|
635
|
-
# As multiple TS may occur, we need a loop to catch them all:
|
636
|
-
# Each TS Hash will be put in an array, which will be put in the info hash.
|
637
|
-
ts_array = Array.new
|
638
|
-
while item_type == "40" do
|
623
|
+
# As multiple presentation contexts may occur, we need a loop to catch them all:
|
624
|
+
# Each presentation context hash will be put in an array, which will be put in the info hash.
|
625
|
+
presentation_contexts = Array.new
|
626
|
+
pc_loop = true
|
627
|
+
while pc_loop do
|
639
628
|
# Item type (1 byte)
|
640
629
|
item_type = msg.decode(1, "HEX")
|
641
|
-
if item_type == "
|
642
|
-
|
643
|
-
|
630
|
+
if item_type == "20"
|
631
|
+
pc = Hash.new
|
632
|
+
pc[:presentation_item_type] = item_type
|
633
|
+
# Reserved (1 byte)
|
634
|
+
msg.skip(1)
|
635
|
+
# Presentation context item length (2 bytes)
|
636
|
+
pc[:presentation_item_length] = msg.decode(2, "US")
|
637
|
+
# Presentation context id (1 byte)
|
638
|
+
pc[:presentation_context_id] = msg.decode(1, "HEX")
|
639
|
+
# Reserved (3 bytes)
|
640
|
+
msg.skip(3)
|
641
|
+
presentation_contexts << pc
|
642
|
+
# A presentation context contains an abstract syntax and one or more transfer syntaxes.
|
643
|
+
# ABSTRACT SYNTAX SUB-ITEM:
|
644
|
+
# Abstract syntax item type (1 byte)
|
645
|
+
pc[:abstract_syntax_item_type] = msg.decode(1, "HEX") # "30"
|
644
646
|
# Reserved (1 byte)
|
645
647
|
msg.skip(1)
|
646
|
-
#
|
647
|
-
|
648
|
-
#
|
649
|
-
|
650
|
-
|
648
|
+
# Abstract syntax item length (2 bytes)
|
649
|
+
pc[:abstract_syntax_item_length] = msg.decode(2, "US")
|
650
|
+
# Abstract syntax (variable length)
|
651
|
+
pc[:abstract_syntax] = msg.decode(pc[:abstract_syntax_item_length], "STR")
|
652
|
+
## TRANSFER SYNTAX SUB-ITEM(S):
|
653
|
+
# As multiple transfer syntaxes may occur, we need a loop to catch them all:
|
654
|
+
# Each transfer syntax hash will be put in an array, which will be put in the presentation context hash.
|
655
|
+
transfer_syntaxes = Array.new
|
656
|
+
ts_loop = true
|
657
|
+
while ts_loop do
|
658
|
+
# Item type (1 byte)
|
659
|
+
item_type = msg.decode(1, "HEX")
|
660
|
+
if item_type == "40"
|
661
|
+
ts = Hash.new
|
662
|
+
ts[:transfer_syntax_item_type] = item_type
|
663
|
+
# Reserved (1 byte)
|
664
|
+
msg.skip(1)
|
665
|
+
# Transfer syntax item length (2 bytes)
|
666
|
+
ts[:transfer_syntax_item_length] = msg.decode(2, "US")
|
667
|
+
# Transfer syntax name (variable length)
|
668
|
+
ts[:transfer_syntax] = msg.decode(ts[:transfer_syntax_item_length], "STR")
|
669
|
+
transfer_syntaxes << ts
|
670
|
+
else
|
671
|
+
# Break the transfer syntax loop, as we have probably reached the next stage,
|
672
|
+
# which is either user info or a new presentation context entry. Rewind:
|
673
|
+
msg.skip(-1)
|
674
|
+
ts_loop = false
|
675
|
+
end
|
676
|
+
end
|
677
|
+
pc[:ts] = transfer_syntaxes
|
651
678
|
else
|
652
|
-
|
679
|
+
# Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind:
|
680
|
+
msg.skip(-1)
|
681
|
+
pc_loop = false
|
653
682
|
end
|
654
683
|
end
|
655
|
-
info[:
|
684
|
+
info[:pc] = presentation_contexts
|
656
685
|
# USER INFORMATION:
|
686
|
+
# Item type (1 byte)
|
687
|
+
info[:user_info_item_type] = msg.decode(1, "HEX")
|
657
688
|
# Reserved (1 byte)
|
658
689
|
msg.skip(1)
|
659
690
|
# User information item length (2 bytes)
|
@@ -671,25 +702,34 @@ module DICOM
|
|
671
702
|
info[:max_pdu_length] = msg.decode(item_length, "UL")
|
672
703
|
when "52"
|
673
704
|
info[:implementation_class_uid] = msg.decode(item_length, "STR")
|
705
|
+
when "53"
|
706
|
+
# Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
|
707
|
+
info[:maxnum_operations_invoked] = msg.decode(2, "US")
|
708
|
+
info[:maxnum_operations_performed] = msg.decode(2, "US")
|
674
709
|
when "55"
|
675
710
|
info[:implementation_version] = msg.decode(item_length, "STR")
|
676
711
|
else
|
677
|
-
|
712
|
+
# Unknown item type:
|
713
|
+
# Value (variable length)
|
714
|
+
value = msg.decode(item_length, "STR")
|
715
|
+
add_error("Notice: Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
|
678
716
|
end
|
679
717
|
end
|
718
|
+
stop_receiving
|
680
719
|
info[:valid] = true
|
681
720
|
return info
|
682
721
|
end # of interpret_association_request
|
683
722
|
|
684
723
|
|
685
|
-
# Decode the
|
686
|
-
#
|
687
|
-
# It might go wrong if an implicit data stream is encountered!
|
724
|
+
# Decode the received command/data binary string, and interpret its content.
|
725
|
+
# Decoding of data fragment will depend on the explicitness of the transmission.
|
688
726
|
def interpret_command_and_data(message, file = nil)
|
689
727
|
info = Hash.new
|
690
728
|
msg = Stream.new(message, @net_endian, @explicit)
|
691
729
|
# Length (of remaining PDV data) (4 bytes)
|
692
730
|
info[:presentation_data_value_length] = msg.decode(4, "UL")
|
731
|
+
# Calculate the last index position of this message element:
|
732
|
+
last_index = info[:presentation_data_value_length] + msg.index
|
693
733
|
# Presentation context ID (1 byte)
|
694
734
|
info[:presentation_context_id] = msg.decode(1, "HEX") # "01" expected
|
695
735
|
# Flags (1 byte)
|
@@ -700,7 +740,7 @@ module DICOM
|
|
700
740
|
results = Hash.new
|
701
741
|
if info[:presentation_context_flag] == "03"
|
702
742
|
# COMMAND, LAST FRAGMENT:
|
703
|
-
while msg.index <
|
743
|
+
while msg.index < last_index do
|
704
744
|
# Tag (4 bytes)
|
705
745
|
tag = msg.decode_tag
|
706
746
|
# Length (2 bytes)
|
@@ -711,7 +751,7 @@ module DICOM
|
|
711
751
|
# Reserved (2 bytes)
|
712
752
|
msg.skip(2)
|
713
753
|
# Type (VR) (from library - not the stream):
|
714
|
-
result =
|
754
|
+
result = LIBRARY.get_name_vr(tag)
|
715
755
|
name = result[0]
|
716
756
|
type = result[1]
|
717
757
|
# Value (variable length)
|
@@ -724,20 +764,8 @@ module DICOM
|
|
724
764
|
# Check if the command fragment indicates that this was the last of the response fragments for this query:
|
725
765
|
status = results["0000,0900"]
|
726
766
|
if status
|
727
|
-
if status
|
728
|
-
|
729
|
-
add_notice("Receipt for successful execution of the desired request has been received. Closing communication.")
|
730
|
-
@listen = false
|
731
|
-
@receive = false
|
732
|
-
elsif status == 65281
|
733
|
-
# Status = "01 ff": More command/data fragments to follow.
|
734
|
-
# (No particular action taken, the program will listen for and receive the coming fragments)
|
735
|
-
elsif status == 65280
|
736
|
-
# Status = "00 ff": Sub-operations are continuing.
|
737
|
-
# (No particular action taken, the program will listen for and receive the coming fragments)
|
738
|
-
else
|
739
|
-
add_error("Error! Something was NOT successful regarding the desired operation. (SCP responded with error code: #{status}) (tag: 0000,0900)")
|
740
|
-
end
|
767
|
+
# Note: This method will also stop the packet receiver if indicated by the status mesasge.
|
768
|
+
process_status(status)
|
741
769
|
end
|
742
770
|
elsif info[:presentation_context_flag] == "00" or info[:presentation_context_flag] == "02"
|
743
771
|
# DATA FRAGMENT:
|
@@ -747,25 +775,33 @@ module DICOM
|
|
747
775
|
info[:bin] = msg.rest_string
|
748
776
|
# Abort the listening if this is last data fragment:
|
749
777
|
if info[:presentation_context_flag] == "02"
|
750
|
-
|
751
|
-
@receive = false
|
778
|
+
stop_receiving
|
752
779
|
end
|
753
780
|
else
|
754
781
|
# Decode data elements:
|
755
|
-
while msg.index <
|
782
|
+
while msg.index < last_index do
|
756
783
|
# Tag (4 bytes)
|
757
784
|
tag = msg.decode_tag
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
785
|
+
if @explicit
|
786
|
+
# Type (VR) (2 bytes):
|
787
|
+
type = msg.decode(2, "STR")
|
788
|
+
# Length (2 bytes)
|
789
|
+
length = msg.decode(2, "US")
|
790
|
+
else
|
791
|
+
# Implicit:
|
792
|
+
type = nil # (needs to be defined as nil here or it will take the value from the previous step in the loop)
|
793
|
+
# Length (4 bytes)
|
794
|
+
length = msg.decode(4, "UL")
|
795
|
+
end
|
762
796
|
if length > msg.rest_length
|
763
797
|
add_error("Error: Specified length of data element value exceeds remaining length of the received message! Something is wrong.")
|
764
798
|
end
|
799
|
+
# Fetch the name (& type if not defined already) for this data element:
|
800
|
+
result = LIBRARY.get_name_vr(tag)
|
801
|
+
name = result[0]
|
802
|
+
type = result[1] unless type
|
765
803
|
# Value (variable length)
|
766
804
|
value = msg.decode(length, type)
|
767
|
-
result = @lib.get_name_vr(tag)
|
768
|
-
name = result[0]
|
769
805
|
# Put tag and value in a hash:
|
770
806
|
results[tag] = value
|
771
807
|
end
|
@@ -775,9 +811,10 @@ module DICOM
|
|
775
811
|
else
|
776
812
|
# Unknown.
|
777
813
|
add_error("Error: Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})")
|
778
|
-
|
779
|
-
@receive = false
|
814
|
+
stop_receiving
|
780
815
|
end
|
816
|
+
# If only parts of the string was read, return the rest:
|
817
|
+
info[:rest_string] = msg.rest_string if last_index < msg.length
|
781
818
|
info[:valid] = true
|
782
819
|
return info
|
783
820
|
end
|
@@ -789,6 +826,7 @@ module DICOM
|
|
789
826
|
msg = Stream.new(message, @net_endian, @explicit)
|
790
827
|
# Reserved (4 bytes)
|
791
828
|
reserved_bytes = msg.decode(4, "HEX")
|
829
|
+
stop_receiving
|
792
830
|
info[:valid] = true
|
793
831
|
return info
|
794
832
|
end
|
@@ -800,6 +838,7 @@ module DICOM
|
|
800
838
|
msg = Stream.new(message, @net_endian, @explicit)
|
801
839
|
# Reserved (4 bytes)
|
802
840
|
reserved_bytes = msg.decode(4, "HEX")
|
841
|
+
stop_receiving
|
803
842
|
info[:valid] = true
|
804
843
|
return info
|
805
844
|
end
|
@@ -908,8 +947,8 @@ module DICOM
|
|
908
947
|
|
909
948
|
|
910
949
|
# Build the binary string that makes up the presentation context part (part of the association request).
|
911
|
-
# For a list of error codes, see the official
|
912
|
-
def append_presentation_context(as, pc, ts, result = "00")
|
950
|
+
# For a list of error codes, see the official DICOM PS3.8 document, (page 39).
|
951
|
+
def append_presentation_context(as, pc, ts, context_id = "01", result = "00")
|
913
952
|
# PRESENTATION CONTEXT:
|
914
953
|
# Presentation context item type (1 byte)
|
915
954
|
@outgoing.encode_last(pc, "HEX") # "20" (request) & "21" (response)
|
@@ -928,7 +967,7 @@ module DICOM
|
|
928
967
|
end
|
929
968
|
@outgoing.encode_last(items_length, "US")
|
930
969
|
# Presentation context ID (1 byte)
|
931
|
-
@outgoing.encode_last(
|
970
|
+
@outgoing.encode_last(context_id, "HEX")
|
932
971
|
# Reserved (1 byte)
|
933
972
|
@outgoing.encode_last("00", "HEX")
|
934
973
|
# (1 byte) Reserved (for association request) & Result/reason (for association accept response)
|
@@ -958,7 +997,7 @@ module DICOM
|
|
958
997
|
# Transfer syntax (variable length)
|
959
998
|
@outgoing.encode_last(t, "STR")
|
960
999
|
end
|
961
|
-
# Update
|
1000
|
+
# Update transfer syntax settings for this instance:
|
962
1001
|
set_transfer_syntax(ts.first)
|
963
1002
|
end
|
964
1003
|
|
@@ -992,6 +1031,102 @@ module DICOM
|
|
992
1031
|
end
|
993
1032
|
|
994
1033
|
|
1034
|
+
# Process the value of the reason byte (in an association abort).
|
1035
|
+
# This will provide information on what is the reason for the error.
|
1036
|
+
def process_reason(reason)
|
1037
|
+
case reason
|
1038
|
+
when "00"
|
1039
|
+
add_error("Reason specified for abort: Reason not specified")
|
1040
|
+
when "01"
|
1041
|
+
add_error("Reason specified for abort: Unrecognized PDU")
|
1042
|
+
when "02"
|
1043
|
+
add_error("Reason specified for abort: Unexpected PDU")
|
1044
|
+
when "04"
|
1045
|
+
add_error("Reason specified for abort: Unrecognized PDU parameter")
|
1046
|
+
when "05"
|
1047
|
+
add_error("Reason specified for abort: Unexpected PDU parameter")
|
1048
|
+
when "06"
|
1049
|
+
add_error("Reason specified for abort: Invalid PDU parameter value")
|
1050
|
+
else
|
1051
|
+
add_error("Reason specified for abort: Unknown reason (Error code: #{reason})")
|
1052
|
+
end
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
|
1056
|
+
# Process the value of the result byte (in the association response).
|
1057
|
+
# Something is wrong if result is different from 0.
|
1058
|
+
def process_result(result)
|
1059
|
+
unless result == 0
|
1060
|
+
# Analyse the result and report what is wrong:
|
1061
|
+
case result
|
1062
|
+
when 1
|
1063
|
+
add_error("Warning: DICOM Request was rejected by the host, reason: 'User-rejection'")
|
1064
|
+
when 2
|
1065
|
+
add_error("Warning: DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
|
1066
|
+
when 3
|
1067
|
+
add_error("Warning: DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
|
1068
|
+
when 4
|
1069
|
+
add_error("Warning: DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
|
1070
|
+
else
|
1071
|
+
add_error("Warning: DICOM Request was rejected by the host, reason: 'UNKNOWN (#{result})' (Illegal reason provided)")
|
1072
|
+
end
|
1073
|
+
end
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
|
1077
|
+
# Process the value of the source byte (in an association abort).
|
1078
|
+
# This will provide information on who is the source of the error.
|
1079
|
+
def process_source(source)
|
1080
|
+
if source == "00"
|
1081
|
+
add_error("Warning: Connection has been aborted by the service provider because of an error by the service user (client side).")
|
1082
|
+
elsif source == "02"
|
1083
|
+
add_error("Warning: Connection has been aborted by the service provider because of an error by the service provider (server side).")
|
1084
|
+
else
|
1085
|
+
add_error("Warning: Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{source})")
|
1086
|
+
end
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
|
1090
|
+
# Process the value of the status tag 0000,0900 received in the command fragment.
|
1091
|
+
# Note: The status tag has vr 'US', and the status as reported here is therefore a number.
|
1092
|
+
# In the official DICOM documents however, the value of the various status options is given in hex format.
|
1093
|
+
# Resources: DICOM PS3.4 Annex Q 2.1.1.4, DICOM PS3.7 Annex C 4.
|
1094
|
+
def process_status(status)
|
1095
|
+
case status
|
1096
|
+
when 0 # "0000"
|
1097
|
+
# Last fragment (Break the while loop that listens continuously for incoming packets):
|
1098
|
+
add_notice("Receipt for successful execution of the desired request has been received. Closing communication.")
|
1099
|
+
stop_receiving
|
1100
|
+
when 42752 # "a700"
|
1101
|
+
# Failure: Out of resources. Related fields: 0000,0902
|
1102
|
+
add_error("Failure! SCP has given the following reason: 'Out of Resources'.")
|
1103
|
+
when 43264 # "a900"
|
1104
|
+
# Failure: Identifier Does Not Match SOP Class. Related fields: 0000,0901, 0000,0902
|
1105
|
+
add_error("Failure! SCP has given the following reason: 'Identifier Does Not Match SOP Class'.")
|
1106
|
+
when 49152 # "c000"
|
1107
|
+
# Failure: Unable to process. Related fields: 0000,0901, 0000,0902
|
1108
|
+
add_error("Failure! SCP has given the following reason: 'Unable to process'.")
|
1109
|
+
when 49408 # "c100"
|
1110
|
+
# Failure: More than one match found. Related fields: 0000,0901, 0000,0902
|
1111
|
+
add_error("Failure! SCP has given the following reason: 'More than one match found'.")
|
1112
|
+
when 49664 # "c200"
|
1113
|
+
# Failure: Unable to support requested template. Related fields: 0000,0901, 0000,0902
|
1114
|
+
add_error("Failure! SCP has given the following reason: 'Unable to support requested template'.")
|
1115
|
+
when 65024 # "fe00"
|
1116
|
+
# Cancel: Matching terminated due to Cancel request.
|
1117
|
+
add_notice("Cancel! SCP has given the following reason: 'Matching terminated due to Cancel request'.")
|
1118
|
+
when 65280 # "ff00"
|
1119
|
+
# Sub-operations are continuing.
|
1120
|
+
# (No particular action taken, the program will listen for and receive the coming fragments)
|
1121
|
+
when 65281 # "ff01"
|
1122
|
+
# More command/data fragments to follow.
|
1123
|
+
# (No particular action taken, the program will listen for and receive the coming fragments)
|
1124
|
+
else
|
1125
|
+
add_error("Error! Something was NOT successful regarding the desired operation. SCP responded with error code: #{status} (tag: 0000,0900). See DICOM PS3.7, Annex C for details.")
|
1126
|
+
end
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
|
995
1130
|
# Handles an incoming transmission.
|
996
1131
|
# Optional: Specify a minimum length of the incoming transmission. (If a message is received
|
997
1132
|
# which is shorter than this limit, the method will keep listening for more incoming packets to append)
|
@@ -1026,8 +1161,7 @@ module DICOM
|
|
1026
1161
|
if (Time.now.to_f - t1) > @timeout
|
1027
1162
|
Thread.kill(thr)
|
1028
1163
|
add_error("No answer was received within the specified timeout period. Aborting.")
|
1029
|
-
|
1030
|
-
@receive = false
|
1164
|
+
stop_receiving
|
1031
1165
|
end
|
1032
1166
|
end
|
1033
1167
|
return data
|
@@ -1040,20 +1174,18 @@ module DICOM
|
|
1040
1174
|
@net_endian = true
|
1041
1175
|
# Default endianness of data is little endian:
|
1042
1176
|
@data_endian = false
|
1043
|
-
#
|
1177
|
+
# It may turn out to be unncessary to define the following values at this early stage.
|
1178
|
+
# Explicitness
|
1044
1179
|
@explicit = true
|
1045
1180
|
# Transfer syntax (Implicit, little endian):
|
1046
1181
|
set_transfer_syntax("1.2.840.10008.1.2")
|
1047
|
-
# Version information:
|
1048
|
-
@implementation_uid = "1.2.826.0.1.3680043.8.641"
|
1049
|
-
@implementation_name = "RUBY_DICOM_0.6"
|
1050
1182
|
end
|
1051
1183
|
|
1052
1184
|
|
1053
1185
|
# Set instance variables related to the transfer syntax.
|
1054
1186
|
def set_transfer_syntax(value)
|
1055
1187
|
# Query the library with our particular transfer syntax string:
|
1056
|
-
result =
|
1188
|
+
result = LIBRARY.process_transfer_syntax(value)
|
1057
1189
|
# Result is a 3-element array: [Validity of ts, explicitness, endianness]
|
1058
1190
|
unless result[0]
|
1059
1191
|
add_error("Warning: Invalid/unknown transfer syntax encountered! Will try to continue, but errors may occur.")
|
@@ -1066,14 +1198,27 @@ module DICOM
|
|
1066
1198
|
|
1067
1199
|
|
1068
1200
|
# Set user information [item type code, vr/type, value]
|
1069
|
-
def set_user_information_array
|
1201
|
+
def set_user_information_array(info = nil)
|
1070
1202
|
@user_information = [
|
1071
1203
|
["51", "UL", @max_package_size], # Max PDU Length
|
1072
|
-
["52", "STR",
|
1073
|
-
["55", "STR",
|
1204
|
+
["52", "STR", UID],
|
1205
|
+
["55", "STR", NAME]
|
1074
1206
|
]
|
1207
|
+
# A bit of a hack to include "asynchronous operations window negotiation", if this has been included in the association request:
|
1208
|
+
if info
|
1209
|
+
@user_information.insert(2, ["53", "HEX", "00010001"]) if info[:maxnum_operations_invoked]
|
1210
|
+
end
|
1211
|
+
end
|
1212
|
+
|
1213
|
+
|
1214
|
+
# Breaks the loops that listen for incoming packets by changing a couple of instance variables.
|
1215
|
+
# This method is called by the various methods that interpret incoming data when they have verified that
|
1216
|
+
# the entire message has been received, or when a timeout is reached.
|
1217
|
+
def stop_receiving
|
1218
|
+
@listen = false
|
1219
|
+
@receive = false
|
1075
1220
|
end
|
1076
1221
|
|
1077
1222
|
|
1078
|
-
end
|
1079
|
-
end
|
1223
|
+
end # of class
|
1224
|
+
end # of module
|