dicom 0.6.1 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 build_abort(message)
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.encode_first("00"*2, "HEX")
48
+ @outgoing.encode_last("00"*2, "HEX")
48
49
  # Source (1 byte)
49
50
  source = "00" # (client side error)
50
- @outgoing.encode_first(source, "HEX")
51
+ @outgoing.encode_last(source, "HEX")
51
52
  # Reason/Diag. (1 byte)
52
53
  reason = "00" # (Reason not specified)
53
- @outgoing.encode_first(reason, "HEX")
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(ac_uid, ts, ui, result)
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
- pc = "21"
67
+ pc_type = "21"
66
68
  # No abstract syntax in association response:
67
- as = nil
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
- append_presentation_context(as, pc, ts, result)
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 08_08.pdf, page 41.
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
- # NB!! This method does not (yet) take explicitness into consideration when building content.
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
- # Little endian encoding:
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
- # Type (VR) (2 bytes)
178
- vr = @lib.get_name_vr(element[0])[1]
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
- # Length (2 bytes)
183
- @outgoing.encode_last(value.length, "US")
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
- abstract_syntax = info[:abstract_syntax]
287
- transfer_syntax = info[:ts].first[:transfer_syntax]
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
- # Read the received data stream and load it as a DICOM object:
319
- obj = DObject.new(data, :bin => true, :syntax => @transfer_syntax)
320
- # File will be saved with the following path:
321
- # original_path/<PatientID>/<StudyDate>/<Modality>/
322
- # File name will be equal to the SOP Instance UID
323
- file_name = obj.get_value("0008,0018")
324
- folders = Array.new(3)
325
- folders[0] = obj.get_value("0010,0020") || "PatientID"
326
- folders[1] = obj.get_value("0008,0020") || "StudyDate"
327
- folders[2] = obj.get_value("0008,0060") || "Modality"
328
- full_path = path + folders.join(File::SEPARATOR) + File::SEPARATOR + file_name
329
- obj.write(full_path, @transfer_syntax)
330
- return full_path
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. Throw error:
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
- if info[:source] == "00"
444
- add_error("Warning: Connection has been aborted by the service provider because of an error by the service user (client side).")
445
- elsif info[:source] == "02"
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
- # Analyse the results:
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 key values for this instance:
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 08_08, page 41: Table 9-21 for details.)")
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
- # Item type (1 byte)
615
- info[:presentation_item_type] = msg.decode(1, "HEX") # "20"
616
- # Reserved (1 byte)
617
- msg.skip(1)
618
- # Presentation context item length (2 bytes)
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 == "40"
642
- ts = Hash.new
643
- ts[:transfer_syntax_item_type] = item_type
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
- # Transfer syntax item length (2 bytes)
647
- ts[:transfer_syntax_item_length] = msg.decode(2, "US")
648
- # Transfer syntax name (variable length)
649
- ts[:transfer_syntax] = msg.decode(ts[:transfer_syntax_item_length], "STR")
650
- ts_array << ts
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
- info[:user_info_item_type] = item_type # "50"
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[:ts] = ts_array
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
- add_error("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
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 binary string received in the query response, and interpret its content.
686
- # NB!! This method does not (yet) take explicitness into consideration when decoding content.
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 < msg.length do
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 = @lib.get_name_vr(tag)
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 == 0
728
- # Last fragment (Break the while loop that listens continuously for incoming packets):
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
- @listen = false
751
- @receive = false
778
+ stop_receiving
752
779
  end
753
780
  else
754
781
  # Decode data elements:
755
- while msg.index < msg.length do
782
+ while msg.index < last_index do
756
783
  # Tag (4 bytes)
757
784
  tag = msg.decode_tag
758
- # Type (VR) (2 bytes):
759
- type = msg.decode(2, "STR")
760
- # Length (2 bytes)
761
- length = msg.decode(2, "US")
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
- @listen = false
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 dicom 08_08.pdf, page 39.
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("01", "HEX")
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 key values for this instance:
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
- @listen = false
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
- # Explicitness (this may turn out not to be necessary...)
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 = @lib.process_transfer_syntax(value)
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", @implementation_uid],
1073
- ["55", "STR", @implementation_name]
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