dicom 0.8 → 0.9

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