dicom 0.8 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,17 +1,15 @@
1
- # Copyright 2008-2010 Christoffer Lervag
2
-
3
1
  module DICOM
4
2
 
5
3
  # Super class which contains common code for all parent elements.
6
4
  #
7
5
  # === Inheritance
8
6
  #
9
- # Since all parent elements inherit from this class, these methods are available to instances of the following classes:
7
+ # Since all parents inherit from this class, these methods are available to instances of the following classes:
10
8
  # * DObject
11
9
  # * Item
12
10
  # * Sequence
13
11
  #
14
- class SuperParent
12
+ class Parent
15
13
 
16
14
  # Returns the specified child element.
17
15
  # If the requested data element isn't found, nil is returned.
@@ -35,7 +33,7 @@ module DICOM
35
33
  return @tags[tag]
36
34
  end
37
35
 
38
- # Adds a DataElement or Sequence instance to self (where self can be either a DObject or Item instance).
36
+ # Adds a Element or Sequence instance to self (where self can be either a DObject or an Item).
39
37
  #
40
38
  # === Restrictions
41
39
  #
@@ -43,28 +41,37 @@ module DICOM
43
41
  #
44
42
  # === Parameters
45
43
  #
46
- # * <tt>element</tt> -- An element (DataElement or Sequence).
44
+ # * <tt>element</tt> -- An element (Element or Sequence).
45
+ # * <tt>options</tt> -- A hash of parameters.
46
+ #
47
+ # === Options
48
+ #
49
+ # * <tt>:no_follow</tt> -- Boolean. If true, the method does not update the parent attribute of the child that is added.
47
50
  #
48
51
  # === Examples
49
52
  #
50
53
  # # Set a new patient's name to the DICOM object:
51
- # obj.add(DataElement.new("0010,0010", "John_Doe"))
54
+ # obj.add(Element.new("0010,0010", "John_Doe"))
52
55
  # # Add a previously defined element roi_name to the first item in the following sequence:
53
- # obj["3006,0020"][1].add(roi_name)
56
+ # obj["3006,0020"][0].add(roi_name)
54
57
  #
55
- def add(element)
58
+ def add(element, options={})
56
59
  unless element.is_a?(Item)
57
60
  unless self.is_a?(Sequence)
61
+ # Does the element's binary value need to be reencoded?
62
+ reencode = true if element.is_a?(Element) && element.endian != stream.str_endian
58
63
  # If we are replacing an existing Element, we need to make sure that this Element's parent value is erased before proceeding.
59
64
  self[element.tag].parent = nil if exists?(element.tag)
60
65
  # Add the element, and set its parent attribute:
61
66
  @tags[element.tag] = element
62
- element.parent = self
67
+ element.parent = self unless options[:no_follow]
68
+ # As the element has been moved in place, perform re-encode if indicated:
69
+ element.value = element.value if reencode
63
70
  else
64
- raise "A Sequence is not allowed to have elements added to it. Use the method add_item() instead if the intention is to add an Item."
71
+ raise "A Sequence is only allowed to have Item elements added to it. Use add_item() instead if the intention is to add an Item."
65
72
  end
66
73
  else
67
- raise "An Item is not allowed as a parameter to the add() method. Use add_item() instead."
74
+ raise ArgumentError, "An Item is not allowed as a parameter to the add() method. Use add_item() instead."
68
75
  end
69
76
  end
70
77
 
@@ -72,7 +79,8 @@ module DICOM
72
79
  # If no existing Item is specified, an empty item will be added.
73
80
  #
74
81
  # === Notes
75
- # * Items are specified by index (starting at 1) instead of a tag string!
82
+ #
83
+ # * Items are specified by index (starting at 0) instead of a tag string!
76
84
  #
77
85
  # === Parameters
78
86
  #
@@ -82,6 +90,7 @@ module DICOM
82
90
  # === Options
83
91
  #
84
92
  # * <tt>:index</tt> -- Fixnum. If the Item is to be inserted at a specific index (Item number), this option parameter needs to set.
93
+ # * <tt>:no_follow</tt> -- Boolean. If true, the method does not update the parent attribute of the child that is added.
85
94
  #
86
95
  # === Examples
87
96
  #
@@ -97,9 +106,9 @@ module DICOM
97
106
  if options[:index]
98
107
  # This Item will take a specific index, and all existing Items with index higher or equal to this number will have their index increased by one.
99
108
  # Check if index is valid (must be an existing index):
100
- if options[:index] >= 1
109
+ if options[:index] >= 0
101
110
  # If the index value is larger than the max index present, we dont need to modify the existing items.
102
- unless options[:index] > @tags.length
111
+ if options[:index] < @tags.length
103
112
  # Extract existing Hash entries to an array:
104
113
  pairs = @tags.sort
105
114
  @tags = Hash.new
@@ -114,27 +123,29 @@ module DICOM
114
123
  end
115
124
  else
116
125
  # Set the index value one higher than the already existing max value:
117
- options[:index] = @tags.length + 1
126
+ options[:index] = @tags.length
118
127
  end
119
128
  #,Add the new Item and set its index:
120
129
  @tags[options[:index]] = item
121
130
  item.index = options[:index]
122
131
  else
123
- raise "The specified index (#{options[:index]}) is out of range (Minimum allowed index value is 1)."
132
+ raise ArgumentError, "The specified index (#{options[:index]}) is out of range (Must be a positive integer)."
124
133
  end
125
134
  else
126
135
  # Add the existing Item to this Sequence:
127
- index = @tags.length + 1
136
+ index = @tags.length
128
137
  @tags[index] = item
129
138
  # Let the Item know what index key it's got in it's parent's Hash:
130
139
  item.index = index
131
140
  end
141
+ # Set ourself as this item's new parent:
142
+ item.set_parent(self) unless options[:no_follow]
132
143
  else
133
- raise "The specified parameter is not an Item. Only Items are allowed to be added to a Sequence."
144
+ raise ArgumentError, "The specified parameter is not an Item. Only Items are allowed to be added to a Sequence."
134
145
  end
135
146
  else
136
147
  # Create an empty Item with self as parent.
137
- index = @tags.length + 1
148
+ index = @tags.length
138
149
  item = Item.new(:parent => self)
139
150
  end
140
151
  else
@@ -193,7 +204,50 @@ module DICOM
193
204
  return total_count
194
205
  end
195
206
 
196
- # Re-encodes the binary data strings of all child DataElement instances.
207
+ # Iterates the children of this parent, calling <tt>block</tt> for each child.
208
+ #
209
+ def each(&block)
210
+ children.each_with_index(&block)
211
+ end
212
+
213
+ # Iterates the child elements of this parent, calling <tt>block</tt> for each element.
214
+ #
215
+ def each_element(&block)
216
+ elements.each_with_index(&block) if children?
217
+ end
218
+
219
+ # Iterates the child items of this parent, calling <tt>block</tt> for each item.
220
+ #
221
+ def each_item(&block)
222
+ items.each_with_index(&block) if children?
223
+ end
224
+
225
+ # Iterates the child sequences of this parent, calling <tt>block</tt> for each sequence.
226
+ #
227
+ def each_sequence(&block)
228
+ sequences.each_with_index(&block) if children?
229
+ end
230
+
231
+ # Iterates the child tags of this parent, calling <tt>block</tt> for each tag.
232
+ #
233
+ def each_tag(&block)
234
+ @tags.each_key(&block)
235
+ end
236
+
237
+ # Returns all child elements of this parent in an array.
238
+ # If no child elements exists, returns an empty array.
239
+ #
240
+ def elements
241
+ children.select { |child| child.is_a?(Element)}
242
+ end
243
+
244
+ # A boolean which indicates whether the parent has any child elements.
245
+ #
246
+ def elements?
247
+ elements.any?
248
+ end
249
+
250
+ # Re-encodes the binary data strings of all child Element instances.
197
251
  # This also includes all the elements contained in any possible child elements.
198
252
  #
199
253
  # === Notes
@@ -210,7 +264,7 @@ module DICOM
210
264
  children.each do |element|
211
265
  if element.children?
212
266
  element.encode_children(old_endian)
213
- elsif element.is_a?(DataElement)
267
+ elsif element.is_a?(Element)
214
268
  encode_child(element, old_endian)
215
269
  end
216
270
  end
@@ -221,7 +275,7 @@ module DICOM
221
275
  #
222
276
  # === Parameters
223
277
  #
224
- # * <tt>tag</tt> -- A tag string which identifies the data element that is queried (Exception: In the case of an Item query, an index (Fixnum) is used instead).
278
+ # * <tt>tag</tt> -- A tag string which identifies the data element that is queried (Exception: In the case of an Item query, an index integer is used instead).
225
279
  #
226
280
  # === Examples
227
281
  #
@@ -243,6 +297,7 @@ module DICOM
243
297
  # * <tt>group_string</tt> -- A group string (the first 4 characters of a tag string).
244
298
  #
245
299
  def group(group_string)
300
+ raise ArgumentError, "Expected String, got #{group_string.class}." unless group_string.is_a?(String)
246
301
  found = Array.new
247
302
  children.each do |child|
248
303
  found << child if child.tag.group == group_string
@@ -290,7 +345,7 @@ module DICOM
290
345
  # Formatting: Name (and Tag)
291
346
  if element.tag == ITEM_TAG
292
347
  # Add index numbers to the Item names:
293
- name = "#{element.name} (\##{i+1})"
348
+ name = "#{element.name} (\##{i})"
294
349
  else
295
350
  name = element.name
296
351
  end
@@ -301,7 +356,7 @@ module DICOM
301
356
  # Formatting: Length
302
357
  l_s = s*(max_length-element.length.to_s.length)
303
358
  # Formatting Value:
304
- if element.is_a?(DataElement)
359
+ if element.is_a?(Element)
305
360
  value = element.value.to_s
306
361
  else
307
362
  value = ""
@@ -344,6 +399,12 @@ module DICOM
344
399
  return elements.flatten, index
345
400
  end
346
401
 
402
+ # Returns a string containing a human-readable hash representation of the element.
403
+ #
404
+ def inspect
405
+ to_hash.inspect
406
+ end
407
+
347
408
  # Checks if an element is a parent.
348
409
  # Returns true for all parent elements.
349
410
  #
@@ -351,8 +412,74 @@ module DICOM
351
412
  return true
352
413
  end
353
414
 
415
+ # Returns all child items of this parent in an array.
416
+ # If no child items exists, returns an empty array.
417
+ #
418
+ def items
419
+ children.select { |child| child.is_a?(Item)}
420
+ end
421
+
422
+ # A boolean which indicates whether the parent has any child items.
423
+ #
424
+ def items?
425
+ items.any?
426
+ end
427
+
428
+ # Handles missing methods, which in our case is intended to be dynamic
429
+ # method names matching DICOM elements in the dictionary.
430
+ #
431
+ # === Notes
432
+ #
433
+ # * When a dynamic method name is matched against a DICOM element, this method:
434
+ # * Returns the element if the method name suggests an element retrieval, and the element exists.
435
+ # * Returns nil if the method name suggests an element retrieval, but the element doesn't exist.
436
+ # * Returns a boolean, if the method name suggests a query (?), based on whether the matched element exists or not.
437
+ # * When the method name suggests assignment (=), an element is created with the supplied arguments, or if the argument is nil, the element is removed.
438
+ #
439
+ # * When a dynamic method name is not matched against a DICOM element, and the method is not defined by the parent, a NoMethodError is raised.
440
+ #
441
+ # === Parameters
442
+ #
443
+ # * <tt>sym</tt> -- Symbol. A method name.
444
+ #
445
+ def method_missing(sym, *args, &block)
446
+ # Try to match the method against a tag from the dictionary:
447
+ tag = LIBRARY.as_tag(sym.to_s) || LIBRARY.as_tag(sym.to_s[0..-2])
448
+ if tag
449
+ if sym.to_s[-1..-1] == '?'
450
+ # Query:
451
+ return self.exists?(tag)
452
+ elsif sym.to_s[-1..-1] == '='
453
+ # Assignment:
454
+ unless args.length==0 || args[0].nil?
455
+ # What kind of element to create?
456
+ if tag == "FFFE,E000"
457
+ return self.add_item
458
+ elsif LIBRARY.tags[tag][0][0] == "SQ"
459
+ return self.add(Sequence.new(tag))
460
+ else
461
+ return self.add(Element.new(tag, *args))
462
+ end
463
+ else
464
+ return self.remove(tag)
465
+ end
466
+ else
467
+ # Retrieval:
468
+ return self[tag] rescue nil
469
+ end
470
+ end
471
+ # Forward to Object#method_missing:
472
+ super
473
+ end
474
+
354
475
  # Sets the length of a Sequence or Item.
355
476
  #
477
+ # === Notes
478
+ #
479
+ # Currently, Ruby DICOM does not use sequence/item lengths when writing DICOM files
480
+ # (it sets the length to -1, which means UNDEFINED). Therefore, in practice, it isn't
481
+ # necessary to use this method, at least as far as writing (valid) DICOM files is concerned.
482
+ #
356
483
  # === Parameters
357
484
  #
358
485
  # * <tt>new_length</tt> -- Fixnum. The new length to assign to the Sequence/Item.
@@ -368,6 +495,7 @@ module DICOM
368
495
  # Prints all child elements of this particular parent.
369
496
  # Information such as tag, parent-child relationship, name, vr, length and value is gathered for each data element
370
497
  # and processed to produce a nicely formatted output.
498
+ # Returns an array of formatted data elements.
371
499
  #
372
500
  # === Parameters
373
501
  #
@@ -392,6 +520,7 @@ module DICOM
392
520
  # FIXME: Speed. The new print algorithm may seem to be slower than the old one (observed on complex, hiearchical DICOM files). Perhaps it can be optimized?
393
521
  #
394
522
  def print(options={})
523
+ elements = Array.new
395
524
  # We first gather some properties that is necessary to produce a nicely formatted printout (max_lengths, count_all),
396
525
  # then the actual information is gathered (handle_print),
397
526
  # and lastly, we pass this information on to the methods which print the output (print_file or print_screen).
@@ -408,6 +537,7 @@ module DICOM
408
537
  else
409
538
  puts "Notice: Object #{self} is empty (contains no data elements)!"
410
539
  end
540
+ return elements
411
541
  end
412
542
 
413
543
  # Finds and returns the maximum character lengths of name and length which occurs for any child element,
@@ -446,23 +576,42 @@ module DICOM
446
576
  # === Parameters
447
577
  #
448
578
  # * <tt>tag</tt> -- A tag string which specifies the element to be removed (Exception: In the case of an Item removal, an index (Fixnum) is used instead).
579
+ # * <tt>options</tt> -- A hash of parameters.
580
+ #
581
+ # === Options
582
+ #
583
+ # * <tt>:no_follow</tt> -- Boolean. If true, the method does not update the parent attribute of the child that is removed.
449
584
  #
450
585
  # === Examples
451
586
  #
452
- # # Remove a DataElement from a DObject instance:
587
+ # # Remove a Element from a DObject instance:
453
588
  # obj.remove("0008,0090")
454
589
  # # Remove Item 1 from a specific Sequence:
455
590
  # obj["3006,0020"].remove(1)
456
591
  #
457
- def remove(tag)
592
+ def remove(tag, options={})
593
+ if tag.is_a?(String) or tag.is_a?(Integer)
594
+ raise ArgumentError, "Argument (#{tag}) is not a valid tag string." if tag.is_a?(String) && !tag.tag?
595
+ raise ArgumentError, "Negative Integer argument (#{tag}) is not allowed." if tag.is_a?(Integer) && tag < 0
596
+ else
597
+ raise ArgumentError, "Expected String or Integer, got #{tag.class}."
598
+ end
458
599
  # We need to delete the specified child element's parent reference in addition to removing it from the tag Hash.
459
600
  element = @tags[tag]
460
601
  if element
461
- element.parent = nil
602
+ element.parent = nil unless options[:no_follow]
462
603
  @tags.delete(tag)
463
604
  end
464
605
  end
465
606
 
607
+ # Removes all child elements from this parent.
608
+ #
609
+ def remove_children
610
+ @tags.each_key do |tag|
611
+ remove(tag)
612
+ end
613
+ end
614
+
466
615
  # Removes all data elements of the specified group from this parent.
467
616
  #
468
617
  # === Parameters
@@ -509,29 +658,112 @@ module DICOM
509
658
  end
510
659
  end
511
660
 
512
- # Returns the value of a specific DataElement child of this parent.
661
+ # Returns true if the parent responds to the given method (symbol) (method is defined).
662
+ # Returns false if the method is not defined.
663
+ #
664
+ # === Parameters
665
+ #
666
+ # * <tt>method</tt> -- Symbol. A method name who's response is tested.
667
+ # * <tt>include_private</tt> -- (Not used by ruby-dicom) Boolean. If true, private methods are included in the search.
668
+ #
669
+ def respond_to?(method, include_private=false)
670
+ # Check the library for a tag corresponding to the given method name symbol:
671
+ return true unless LIBRARY.as_tag(method.to_s).nil?
672
+ # In case of a query (xxx?) or assign (xxx=), remove last character and try again:
673
+ return true unless LIBRARY.as_tag(method.to_s[0..-2]).nil?
674
+ # Forward to Object#respond_to?:
675
+ super
676
+ end
677
+
678
+ # Returns all child sequences of this parent in an array.
679
+ # If no child sequences exists, returns an empty array.
680
+ #
681
+ def sequences
682
+ children.select { |child| child.is_a?(Sequence) }
683
+ end
684
+
685
+ # A boolean which indicates whether the parent has any child sequences.
686
+ #
687
+ def sequences?
688
+ sequences.any?
689
+ end
690
+
691
+ # Builds and returns a nested hash containing all children of this parent.
692
+ # Keys are determined by the key_representation attribute, and data element values are used as values.
693
+ #
694
+ # === Notes
695
+ #
696
+ # * For private elements, the tag is used for key instead of the key representation, as private tags lacks names.
697
+ # * For child-less parents, the key_representation attribute is used as value.
698
+ #
699
+ def to_hash
700
+ as_hash = Hash.new
701
+ unless children?
702
+ if self.is_a?(DObject)
703
+ as_hash = {}
704
+ else
705
+ as_hash[(self.tag.private?) ? self.tag : self.send(DICOM.key_representation)] = nil
706
+ end
707
+ else
708
+ children.each do |child|
709
+ if child.tag.private?
710
+ hash_key = child.tag
711
+ elsif child.is_a?(Item)
712
+ hash_key = "Item #{child.index}"
713
+ else
714
+ hash_key = child.send(DICOM.key_representation)
715
+ end
716
+ if child.is_a?(Element)
717
+ as_hash[hash_key] = child.to_hash[hash_key]
718
+ else
719
+ as_hash[hash_key] = child.to_hash
720
+ end
721
+ end
722
+ end
723
+ return as_hash
724
+ end
725
+
726
+ # Returns a json string containing a human-readable representation of the element.
727
+ #
728
+ def to_json
729
+ to_hash.to_json
730
+ end
731
+
732
+ # Returns a yaml string containing a human-readable representation of the element.
733
+ #
734
+ def to_yaml
735
+ to_hash.to_yaml
736
+ end
737
+
738
+ # Returns the value of a specific Element child of this parent.
513
739
  # Returns nil if the child element does not exist.
514
740
  #
515
741
  # === Notes
516
742
  #
517
- # * Only DataElement instances have values. Parent elements like Sequence and Item have no value themselves.
743
+ # * Only Element instances have values. Parent elements like Sequence and Item have no value themselves.
518
744
  # If the specified <tt>tag</tt> is that of a parent element, <tt>value()</tt> will raise an exception.
519
745
  #
520
746
  # === Parameters
521
747
  #
522
- # * <tt>tag</tt> -- A tag string which identifies the child DataElement.
748
+ # * <tt>tag</tt> -- A tag string which identifies the child Element.
523
749
  #
524
750
  # === Examples
525
751
  #
526
752
  # # Get the patient's name value:
527
753
  # name = obj.value("0010,0010")
528
754
  # # Get the Frame of Reference UID from the first item in the Referenced Frame of Reference Sequence:
529
- # uid = obj["3006,0010"][1].value("0020,0052")
755
+ # uid = obj["3006,0010"][0].value("0020,0052")
530
756
  #
531
757
  def value(tag)
758
+ if tag.is_a?(String) or tag.is_a?(Integer)
759
+ raise ArgumentError, "Argument (#{tag}) is not a valid tag string." if tag.is_a?(String) && !tag.tag?
760
+ raise ArgumentError, "Negative Integer argument (#{tag}) is not allowed." if tag.is_a?(Integer) && tag < 0
761
+ else
762
+ raise ArgumentError, "Expected String or Integer, got #{tag.class}."
763
+ end
532
764
  if exists?(tag)
533
765
  if @tags[tag].is_parent?
534
- raise "Illegal parameter '#{tag}'. Parent elements, like the referenced '#{@tags[tag].class}', have no value. Only DataElement tags are valid."
766
+ raise ArgumentError, "Illegal parameter '#{tag}'. Parent elements, like the referenced '#{@tags[tag].class}', have no value. Only Element tags are valid."
535
767
  else
536
768
  return @tags[tag].value
537
769
  end
@@ -545,12 +777,12 @@ module DICOM
545
777
  private
546
778
 
547
779
 
548
- # Re-encodes the value of a child DataElement (but only if the DataElement encoding is
780
+ # Re-encodes the value of a child Element (but only if the Element encoding is
549
781
  # influenced by a shift in endianness).
550
782
  #
551
783
  # === Parameters
552
784
  #
553
- # * <tt>element</tt> -- The DataElement who's value will be re-encoded.
785
+ # * <tt>element</tt> -- The Element who's value will be re-encoded.
554
786
  # * <tt>old_endian</tt> -- The previous endianness of the element binary (used for decoding the value).
555
787
  #
556
788
  #--
@@ -565,14 +797,13 @@ module DICOM
565
797
  else
566
798
  # Not all types of tags needs to be reencoded when switching endianness:
567
799
  case element.vr
568
- when "US", "SS", "UL", "SL", "FL", "FD", "OF", "OW" # Numbers
800
+ when "US", "SS", "UL", "SL", "FL", "FD", "OF", "OW", "AT" # Numbers or tag reference
569
801
  # Re-encode, as long as it is not a group 0002 element (which must always be little endian):
570
802
  unless element.tag.group == "0002"
571
803
  stream_old_endian = Stream.new(element.bin, old_endian)
572
- numbers = stream_old_endian.decode(element.length, element.vr)
573
- element.value = numbers
804
+ formatted_value = stream_old_endian.decode(element.length, element.vr)
805
+ element.value = formatted_value # (the value=() method also encodes a new binary for the element)
574
806
  end
575
- #when "AT" # Tag reference
576
807
  end
577
808
  end
578
809
  end