dicom 0.6.1 → 0.7
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.
- 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
|