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.
@@ -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