class DICOM::Parent
Super class which contains common code for all parent elements.
Inheritance¶ ↑
Since all parents inherit from this class, these methods are available to instances of the following classes:
Public Instance Methods
Retrieves the child element matching the specified element tag or item index.
Only immediate children are searched. Grandchildren etc. are not included.
@param [String, Integer] tag_or_index a ruby-dicom tag string or item index @return [Element, Sequence
, Item
, NilClass] the matched element (or nil, if no match was made) @example Extract the “Pixel Data” data element from the DObject
instance
pixel_data_element = dcm["7FE0,0010"]
@example Extract the first Item
from a Sequence
first_item = dcm["3006,0020"][0]
# File lib/dicom/parent.rb, line 27 def [](tag_or_index) formatted = tag_or_index.is_a?(String) ? tag_or_index.upcase : tag_or_index return @tags[formatted] end
Adds an Element
or Sequence
instance to self (where self can be either a DObject
or an Item
).
@note Items can not be added with this method (use add_item instead).
@param [Element, Sequence] element a child element/sequence @param [Hash] options the options used for adding the element/sequence option options [Boolean] :no_follow when true, the method does not update the parent attribute of the child that is added @example Set a new patient's name to the DICOM
object
dcm.add(Element.new("0010,0010", "John_Doe"))
@example Add a previously defined element roi_name to the first item of a sequence
dcm["3006,0020"][0].add(roi_name)
# File lib/dicom/parent.rb, line 44 def add(element, options={}) unless element.is_a?(Item) unless self.is_a?(Sequence) # Does the element's binary value need to be reencoded? reencode = true if element.is_a?(Element) && element.endian != stream.str_endian # If we are replacing an existing Element, we need to make sure that this Element's parent value is erased before proceeding. self[element.tag].parent = nil if exists?(element.tag) # Add the element, and set its parent attribute: @tags[element.tag] = element element.parent = self unless options[:no_follow] # As the element has been moved in place, perform re-encode if indicated: element.value = element.value if reencode else 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." end else raise ArgumentError, "An Item is not allowed as a parameter to the add() method. Use add_item() instead." end end
Retrieves all (immediate) child elementals in an array (sorted by element tag).
@return [Array<Element, Item
, Sequence>] the parent's child elementals (an empty array if childless) @example Retrieve all top level elements in a DICOM
object
top_level_elements = dcm.children
# File lib/dicom/parent.rb, line 70 def children return @tags.sort.transpose[1] || Array.new end
Checks if an element actually has any child elementals (elements/items/sequences).
Notice the subtle difference between the children? and is_parent? methods. While they will give the same result in most real use cases, they differ when used on parent elements that do not have any children added yet.
For example, when called on an empty Sequence
, the children? method will return false, whereas the is_parent? method still returns true.
@return [Boolean] true if the element has children, and false if not
# File lib/dicom/parent.rb, line 85 def children? if @tags.length > 0 return true else return false end end
Gives the number of elements connected directly to this parent.
This count does NOT include the number of elements contained in any possible child elements.
@return [Integer] The number of child elements belonging to this parent
# File lib/dicom/parent.rb, line 99 def count return @tags.length end
Gives the total number of elements connected to this parent.
This count includes all the elements contained in any possible child elements.
@return [Integer] The total number of child elements connected to this parent
# File lib/dicom/parent.rb, line 109 def count_all # Iterate over all elements, and repeat recursively for all elements which themselves contain children. total_count = count @tags.each_value do |value| total_count += value.count_all if value.children? end return total_count end
Deletes the specified element from this parent.
@param [String, Integer] tag_or_index a ruby-dicom tag string or item index @param [Hash] options the options used for deleting the element option options [Boolean] :no_follow when true, the method does not update the parent attribute of the child that is deleted @example Delete an Element
from a DObject
instance
dcm.delete("0008,0090")
@example Delete Item
1 from a Sequence
dcm["3006,0020"].delete(1)
# File lib/dicom/parent.rb, line 128 def delete(tag_or_index, options={}) check_key(tag_or_index, :delete) # We need to delete the specified child element's parent reference in addition to removing it from the tag Hash. element = self[tag_or_index] if element element.parent = nil unless options[:no_follow] @tags.delete(tag_or_index) end end
Deletes all child elements from this parent.
# File lib/dicom/parent.rb, line 140 def delete_children @tags.each_key do |tag| delete(tag) end end
Deletes all elements of the specified group from this parent.
@param [String] group_string a group string (the first 4 characters of a tag string) @example Delete the File Meta Group of a DICOM
object
dcm.delete_group("0002")
# File lib/dicom/parent.rb, line 152 def delete_group(group_string) group_elements = group(group_string) group_elements.each do |element| delete(element.tag) end end
Deletes all private data/sequence elements from this parent.
@example Delete all private elements from a DObject
instance
dcm.delete_private
@example Delete only private elements belonging to a specific Sequence
dcm["3006,0020"].delete_private
# File lib/dicom/parent.rb, line 166 def delete_private # Iterate all children, and repeat recursively if a child itself has children, to delete all private data elements: children.each do |element| delete(element.tag) if element.tag.private? element.delete_private if element.children? end end
Deletes all retired data/sequence elements from this parent.
@example Delete all retired elements from a DObject
instance
dcm.delete_retired
# File lib/dicom/parent.rb, line 179 def delete_retired # Iterate all children, and repeat recursively if a child itself has children, to delete all retired elements: children.each do |element| dict_element = LIBRARY.element(element.tag) delete(element.tag) if dict_element && dict_element.retired? element.delete_retired if element.children? end end
Iterates all children of this parent, calling block
for each child.
# File lib/dicom/parent.rb, line 190 def each(&block) children.each_with_index(&block) end
Iterates the child elements of this parent, calling block
for each element.
# File lib/dicom/parent.rb, line 196 def each_element(&block) elements.each_with_index(&block) if children? end
Iterates the child items of this parent, calling block
for each item.
# File lib/dicom/parent.rb, line 202 def each_item(&block) items.each_with_index(&block) if children? end
Iterates the child sequences of this parent, calling block
for each sequence.
# File lib/dicom/parent.rb, line 208 def each_sequence(&block) sequences.each_with_index(&block) if children? end
Iterates the child tags of this parent, calling block
for each tag.
# File lib/dicom/parent.rb, line 214 def each_tag(&block) @tags.each_key(&block) end
Retrieves all child elements of this parent in an array.
@return [Array<Element>] child elements (or empty array, if childless)
# File lib/dicom/parent.rb, line 222 def elements children.select { |child| child.is_a?(Element)} end
A boolean which indicates whether the parent has any child elements.
@return [Boolean] true if any child elements exists, and false if not
# File lib/dicom/parent.rb, line 230 def elements? elements.any? end
Re-encodes the binary data strings of all child Element
instances. This also includes all the elements contained in any possible child elements.
@note This method is only intended for internal library use, but for technical reasons
(the fact that is called between instances of different classes), can't be made private.
@param [Boolean] old_endian the previous endianness of the elements/DObject instance (used for decoding values from binary)
# File lib/dicom/parent.rb, line 241 def encode_children(old_endian) # Cycle through all levels of children recursively: children.each do |element| if element.children? element.encode_children(old_endian) elsif element.is_a?(Element) encode_child(element, old_endian) end end end
Checks whether a specific data element tag is defined for this parent.
@param [String, Integer] tag_or_index a ruby-dicom tag string or item index @return [Boolean] true if the element is found, and false if not @example Do something with an element only if it exists
process_name(dcm["0010,0010"]) if dcm.exists?("0010,0010")
# File lib/dicom/parent.rb, line 259 def exists?(tag_or_index) if self[tag_or_index] return true else return false end end
Returns an array of all child elements that belongs to the specified group.
@param [String] group_string a group string (the first 4 characters of a tag string) @return [Array<Element, Item
, Sequence>] the matching child elements (an empty array if no children matches)
# File lib/dicom/parent.rb, line 272 def group(group_string) raise ArgumentError, "Expected String, got #{group_string.class}." unless group_string.is_a?(String) found = Array.new children.each do |child| found << child if child.tag.group == group_string.upcase end return found end
Gathers the desired information from the selected data elements and processes this information to make a text output which is nicely formatted.
@note This method is only intended for internal library use, but for technical reasons
(the fact that is called between instances of different classes), can't be made private. The method is used by the print() method to construct its text output.
@param [Integer] index the index which is given to the first child of this parent @param [Integer] max_digits the maximum number of digits in the index of an element (in reality the number of digits of the last element) @param [Integer] max_name the maximum number of characters in the name of any element to be printed @param [Integer] max_length the maximum number of digits in the length of an element @param [Integer] max_generations the maximum number of generations of children for this parent @param [Integer] visualization an array of string symbols which visualizes the tree structure that the children of this particular parent belongs to (for no visualization, an empty array is passed) @param [Hash] options the options to use when processing the print information @option options [Integer] :value_max if a value max length is specified, the element values which exceeds this are trimmed @return [Array] a text array and an index of the last element
# File lib/dicom/parent.rb, line 298 def handle_print(index, max_digits, max_name, max_length, max_generations, visualization, options={}) # FIXME: This method is somewhat complex, and some simplification, if possible, wouldn't hurt. elements = Array.new s = " " hook_symbol = "|_" last_item_symbol = " " nonlast_item_symbol = "| " children.each_with_index do |element, i| n_parents = element.parents.length # Formatting: Index i_s = s*(max_digits-(index).to_s.length) # Formatting: Name (and Tag) if element.tag == ITEM_TAG # Add index numbers to the Item names: name = "#{element.name} (\##{i})" else name = element.name end n_s = s*(max_name-name.length) # Formatting: Tag tag = "#{visualization.join}#{element.tag}" t_s = s*((max_generations-1)*2+9-tag.length) # Formatting: Length l_s = s*(max_length-element.length.to_s.length) # Formatting Value: if element.is_a?(Element) value = element.value.to_s else value = "" end if options[:value_max] value = "#{value[0..(options[:value_max]-3)]}.." if value.length > options[:value_max] end elements << "#{i_s}#{index} #{tag}#{t_s} #{name}#{n_s} #{element.vr} #{l_s}#{element.length} #{value}" index += 1 # If we have child elements, print those elements recursively: if element.children? if n_parents > 1 child_visualization = Array.new child_visualization.replace(visualization) if element == children.first if children.length == 1 # Last item: child_visualization.insert(n_parents-2, last_item_symbol) else # More items follows: child_visualization.insert(n_parents-2, nonlast_item_symbol) end elsif element == children.last # Last item: child_visualization[n_parents-2] = last_item_symbol child_visualization.insert(-1, hook_symbol) else # Neither first nor last (more items follows): child_visualization.insert(n_parents-2, nonlast_item_symbol) end elsif n_parents == 1 child_visualization = Array.new(1, hook_symbol) else child_visualization = Array.new end new_elements, index = element.handle_print(index, max_digits, max_name, max_length, max_generations, child_visualization, options) elements << new_elements end end return elements.flatten, index end
Gives a string containing a human-readable hash representation of the parent.
@return [String] a hash representation string of the parent
# File lib/dicom/parent.rb, line 370 def inspect to_hash.inspect end
Retrieves all child items of this parent in an array.
@return [Array<Item>] child items (or empty array, if childless)
# File lib/dicom/parent.rb, line 386 def items children.select { |child| child.is_a?(Item)} end
A boolean which indicates whether the parent has any child items.
@return [Boolean] true if any child items exists, and false if not
# File lib/dicom/parent.rb, line 394 def items? items.any? end
Sets the length of a Sequence
or Item
.
@note Currently, ruby-dicom does not use sequence/item lengths when writing DICOM
files (it sets the length to -1, meaning UNDEFINED). Therefore, in practice, it isn't necessary to use this method, at least as far as writing (valid) DICOM
files is concerned.
@param [Integer] new_length the new length to assign to the Sequence/Item
# File lib/dicom/parent.rb, line 406 def length=(new_length) unless self.is_a?(DObject) @length = new_length else raise "Length can not be set for a DObject instance." end end
Finds and returns the maximum character lengths of name and length which occurs for any child element, as well as the maximum number of generations of elements.
@note This method is only intended for internal library use, but for technical reasons
(the fact that is called between instances of different classes), can't be made private. The method is used by the print() method to achieve a proper format in its output.
# File lib/dicom/parent.rb, line 421 def max_lengths max_name = 0 max_length = 0 max_generations = 0 children.each do |element| if element.children? max_nc, max_lc, max_gc = element.max_lengths max_name = max_nc if max_nc > max_name max_length = max_lc if max_lc > max_length max_generations = max_gc if max_gc > max_generations end n_length = element.name.length l_length = element.length.to_s.length generations = element.parents.length max_name = n_length if n_length > max_name max_length = l_length if l_length > max_length max_generations = generations if generations > max_generations end return max_name, max_length, max_generations end
Handles missing methods, which in our case is intended to be dynamic method names matching DICOM
elements in the dictionary.
When a dynamic method name is matched against a DICOM
element, this method:
-
Returns the element if the method name suggests an element retrieval, and the element exists.
-
Returns nil if the method name suggests an element retrieval, but the element doesn't exist.
-
Returns a boolean, if the method name suggests a query (?), based on whether the matched element exists or not.
-
When the method name suggests assignment (=), an element is created with the supplied arguments, or if the argument is nil, the element is deleted.
-
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.
@param [Symbol] sym a method name
# File lib/dicom/parent.rb, line 455 def method_missing(sym, *args, &block) s = sym.to_s action = s[-1] # Try to match the method against a tag from the dictionary: tag = LIBRARY.as_tag(s) || LIBRARY.as_tag(s[0..-2]) if tag if action == '?' # Query: return self.exists?(tag) elsif action == '=' # Assignment: unless args.length==0 || args[0].nil? # What kind of element to create? if tag == 'FFFE,E000' return self.add_item elsif LIBRARY.element(tag).vr == 'SQ' return self.add(Sequence.new(tag)) else return self.add(Element.new(tag, *args)) end else return self.delete(tag) end else # Retrieval: return self[tag] end end # Forward to Object#method_missing: super end
Loads data from an encoded DICOM
string and creates items and elements which are linked to this instance.
@param [String] bin an encoded binary string containing DICOM
information @param [String] syntax the transfer syntax to use when decoding the DICOM
string @param [Boolean] switched indicating whether the transfer syntax 'switch' has occured in the data stream of this object
# File lib/dicom/d_read.rb, line 12 def parse(bin, syntax, switched=false, explicit=true) raise ArgumentError, "Invalid argument 'bin'. Expected String, got #{bin.class}." unless bin.is_a?(String) raise ArgumentError, "Invalid argument 'syntax'. Expected String, got #{syntax.class}." unless syntax.is_a?(String) read(bin, signature=false, :syntax => syntax, :switched => switched, :explicit => explicit) end
Prints all child elementals of this particular parent. Information such as tag, parent-child relationship, name, vr, length and value is gathered for each element and processed to produce a nicely formatted output.
@param [Hash] options the options to use for handling the printout option options [Integer] :value_max if a value max length is specified, the element values which exceeds this are trimmed option options [String] :file if a file path is specified, the output is printed to this file instead of being printed to the screen @return [Array<String>] an array of formatted element string lines @example Print a DObject
instance to screen
dcm.print
@example Print the DObject
to the screen, but specify a 25 character value cutoff to produce better-looking results
dcm.print(:value_max => 25)
@example Print to a text file the elements that belong to a specific Sequence
dcm["3006,0020"].print(:file => "dicom.txt")
# File lib/dicom/parent.rb, line 502 def print(options={}) # FIXME: Perhaps a :children => false option would be a good idea (to avoid lengthy printouts in cases where this would be desirable)? # 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? elements = Array.new # We first gather some properties that is necessary to produce a nicely formatted printout (max_lengths, count_all), # then the actual information is gathered (handle_print), # and lastly, we pass this information on to the methods which print the output (print_file or print_screen). if count > 0 max_name, max_length, max_generations = max_lengths max_digits = count_all.to_s.length visualization = Array.new elements, index = handle_print(start_index=1, max_digits, max_name, max_length, max_generations, visualization, options) if options[:file] print_file(elements, options[:file]) else print_screen(elements) end else puts "Notice: Object #{self} is empty (contains no data elements)!" end return elements end
Gives a string which represents this DICOM
parent. The DOBject is is represented by its class name, whereas elemental parents (Sequence
, Item
) is represented by their tags.
@return [String] a representation of the DICOM
parent
# File lib/dicom/parent.rb, line 531 def representation self.is_a?(DObject) ? 'DObject' : self.tag end
Checks if the parent responds to the given method (symbol) (whether the method is defined or not).
@param [Symbol] method a method name who's response is tested @param [Boolean] include_private if true, private methods are included in the search (not used by ruby-dicom) @return [Boolean] true if the parent responds to the given method (method is defined), and false if not
# File lib/dicom/parent.rb, line 552 def respond_to?(method, include_private=false) # Check the library for a tag corresponding to the given method name symbol: return true unless LIBRARY.as_tag(method.to_s).nil? # In case of a query (xxx?) or assign (xxx=), remove last character and try again: return true unless LIBRARY.as_tag(method.to_s[0..-2]).nil? # Forward to Object#respond_to?: super end
Retrieves all child sequences of this parent in an array.
@return [Array<Sequence>] child sequences (or empty array, if childless)
# File lib/dicom/parent.rb, line 565 def sequences children.select { |child| child.is_a?(Sequence) } end
A boolean which indicates whether the parent has any child sequences.
@return [Boolean] true if any child sequences exists, and false if not
# File lib/dicom/parent.rb, line 573 def sequences? sequences.any? end
Builds a nested hash containing all children of this parent.
Keys are determined by the key_representation attribute, and data element values are used as values.
-
For private elements, the tag is used for key instead of the key representation, as private tags lacks names.
-
For child-less parents, the key_representation attribute is used as value.
@return [Hash] a nested hash containing key & value pairs of all children
# File lib/dicom/parent.rb, line 585 def to_hash as_hash = Hash.new unless children? if self.is_a?(DObject) as_hash = {} else as_hash[(self.tag.private?) ? self.tag : self.send(DICOM.key_representation)] = nil end else children.each do |child| if child.tag.private? hash_key = child.tag elsif child.is_a?(Item) hash_key = "Item #{child.index}" else hash_key = child.send(DICOM.key_representation) end if child.is_a?(Element) as_hash[hash_key] = child.to_hash[hash_key] else as_hash[hash_key] = child.to_hash end end end return as_hash end
Builds a json string containing a human-readable representation of the parent.
@return [String] a human-readable representation of this parent
# File lib/dicom/parent.rb, line 616 def to_json to_hash.to_json end
Returns a yaml string containing a human-readable representation of the parent.
@return [String] a human-readable representation of this parent
# File lib/dicom/parent.rb, line 624 def to_yaml to_hash.to_yaml end
Gives the value of a specific Element
child of this parent.
-
Only
Element
instances have values.Parent
elements likeSequence
andItem
have no value themselves. -
If the specified tag is that of a parent element, an exception is raised.
@param [String] tag a tag string which identifies the child Element
@return [String, Integer, Float, NilClass] an element value (or nil, if no element is matched) @example Get the patient's name value
name = dcm.value("0010,0010")
@example Get the Frame of Reference UID
from the first item in the Referenced Frame of Reference Sequence
uid = dcm["3006,0010"][0].value("0020,0052")
# File lib/dicom/parent.rb, line 640 def value(tag) check_key(tag, :value) if exists?(tag) if self[tag].is_parent? raise ArgumentError, "Illegal parameter '#{tag}'. Parent elements, like the referenced '#{@tags[tag].class}', have no value. Only Element tags are valid." else return self[tag].value end else return nil end end
Private Instance Methods
Adds a binary string to (the end of) either the instance file or string.
@param [String] string a pre-encoded string
# File lib/dicom/d_write.rb, line 12 def add_encoded(string) if @file @stream.write(string) else # Are we writing to a single (big) string, or multiple (smaller) strings? unless @segments @stream.add_last(string) else add_with_segmentation(string) end end end
Adds an encoded string to the output stream, while keeping track of the accumulated size of the output stream, splitting it up as necessary, and transferring the encoded string fragments to an array.
@param [String] string a pre-encoded string
# File lib/dicom/d_write.rb, line 31 def add_with_segmentation(string) # As the encoded DICOM string will be cut in multiple, smaller pieces, we need to monitor the length of our encoded strings: if (string.length + @stream.length) > @max_size split_and_add(string) elsif (30 + @stream.length) > @max_size # End the current segment, and start on a new segment for this string. @segments << @stream.export @stream.add_last(string) else # We are nowhere near the limit, simply add the string: @stream.add_last(string) end end
Checks whether the given tag is a duplicate of an existing tag with this parent.
@param [String] tag the tag of the candidate duplicate elemental @param [String] elemental the duplicate elemental type (e.g. Sequence
, Element
)
# File lib/dicom/d_read.rb, line 27 def check_duplicate(tag, elemental) if @current_parent[tag] gp = @current_parent.parent ? "#{@current_parent.parent.representation} => " : '' p = @current_parent.representation logger.warn("Duplicate #{elemental} (#{tag}) detected at level: #{gp}#{p}") end end
Toggles the status for enclosed pixel data.
@param [Element, Item
, Sequence] element a data element
# File lib/dicom/d_write.rb, line 49 def check_encapsulated_image(element) # If DICOM object contains encapsulated pixel data, we need some special handling for its items: if element.tag == PIXEL_TAG and element.parent.is_a?(DObject) @enc_image = true if element.length <= 0 end end
Checks for the official DICOM
header signature.
@return [Boolean] true if the proper signature is present, false if not, and nil if the string was shorter then the length of the DICOM
signature
# File lib/dicom/d_read.rb, line 39 def check_header # According to the official DICOM standard, a DICOM file shall contain 128 consequtive (zero) bytes, # followed by 4 bytes that spell the string 'DICM'. Apparently, some providers seems to skip this in their DICOM files. # Check that the string is long enough to contain a valid header: if @str.length < 132 # This does not seem to be a valid DICOM string and so we return. return nil else @stream.skip(128) # Next 4 bytes should spell "DICM": identifier = @stream.decode(4, "STR") @header_length += 132 if identifier != "DICM" then # Header signature is not valid (we will still try to parse it is a DICOM string though): logger.warn("This string does not contain the expected DICOM header. Will try to parse the string anyway (assuming a missing header).") # As the string is not conforming to the DICOM standard, it is possible that it does not contain a # transfer syntax element, and as such, we attempt to choose the most probable encoding values here: @explicit = false return false else # Header signature is valid: @signature = true return true end end end
Checks the given key argument and logs a warning if an obviously incorrect key argument is used.
@param [String, Integer] tag_or_index the tag string or item index indentifying a given elemental @param [Symbol] method a representation of the method calling this method
# File lib/dicom/parent.rb, line 663 def check_key(tag_or_index, method) if tag_or_index.is_a?(String) logger.warn("Parent##{method} called with an invalid tag argument: #{tag_or_index}") unless tag_or_index.tag? elsif tag_or_index.is_a?(Integer) logger.warn("Parent##{method} called with a negative Integer argument: #{tag_or_index}") if tag_or_index < 0 else logger.warn("Parent##{method} called with an unexpected argument. Expected String or Integer, got: #{tag_or_index.class}") end end
Re-encodes the value of a child Element
(but only if the Element
encoding is influenced by a shift in endianness).
@param [Element] element the Element
who's value will be re-encoded @param [Boolean] old_endian the previous endianness of the element binary (used for decoding the value)
# File lib/dicom/parent.rb, line 679 def encode_child(element, old_endian) if element.tag == "7FE0,0010" # As encoding settings of the DObject has already been changed, we need to decode the old pixel values with the old encoding: stream_old_endian = Stream.new(nil, old_endian) pixels = decode_pixels(element.bin, stream_old_endian) encode_pixels(pixels, stream) else # Not all types of tags needs to be reencoded when switching endianness: case element.vr when "US", "SS", "UL", "SL", "FL", "FD", "OF", "OW", "AT" # Numbers or tag reference # Re-encode, as long as it is not a group 0002 element (which must always be little endian): unless element.tag.group == "0002" stream_old_endian = Stream.new(element.bin, old_endian) formatted_value = stream_old_endian.decode(element.length, element.vr) element.value = formatted_value # (the value=() method also encodes a new binary for the element) end end end end
Writes DICOM
content to a series of size-limited binary strings, which is returned in an array. This is typically used in preparation of transmitting DICOM
objects through network connections.
@param [Integer] max_size the maximum segment string length @param [Hash] options the options to use for encoding the DICOM
strings @option options [String] :syntax the transfer syntax used for the encoding settings of the post-meta part of the DICOM
string @return [Array<String>] the encoded DICOM
strings
# File lib/dicom/d_write.rb, line 64 def encode_in_segments(max_size, options={}) @max_size = max_size @transfer_syntax = options[:syntax] # Until a DICOM write has completed successfully the status is 'unsuccessful': @write_success = false # Default explicitness of start of DICOM file: @explicit = true # Default endianness of start of DICOM files (little endian): @str_endian = false # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that: @switched = false # Items contained under the Pixel Data element needs some special attention to write correctly: @enc_image = false # Create a Stream instance to handle the encoding of content to a binary string: @stream = Stream.new(nil, @str_endian) @segments = Array.new write_data_elements(children) # Extract the remaining string in our stream instance to our array of strings: @segments << @stream.export # Mark this write session as successful: @write_success = true return @segments end
Initializes common variables among the parent elements.
# File lib/dicom/parent.rb, line 701 def initialize_parent # All child data elements and sequences are stored in a hash where the tag string is used as key: @tags = Hash.new end
Tests if the path/file is writable, creates any folders if necessary, and opens the file for writing.
@param [String] file a path/file string
# File lib/dicom/d_write.rb, line 92 def open_file(file) # Check if file already exists: if File.exist?(file) # Is it writable? if File.writable?(file) @file = File.new(file, "wb") else # Existing file is not writable: logger.error("The program does not have permission or resources to create this file: #{file}") end else # File does not exist. # Check if this file's path contains a folder that does not exist, and therefore needs to be created: folders = file.split(File::SEPARATOR) if folders.length > 1 # Remove last element (which should be the file string): folders.pop path = folders.join(File::SEPARATOR) # Check if this path exists: unless File.directory?(path) # We need to create (parts of) this path: require 'fileutils' FileUtils.mkdir_p(path) end end # The path to this non-existing file is verified, and we can proceed to create the file: @file = File.new(file, "wb") end end
Prints an array of formatted element string lines gathered by the print() method to file.
@param [Array<String>] elements an array of formatted element string lines @param [String] file a path/file_name string
# File lib/dicom/parent.rb, line 711 def print_file(elements, file) File.open(file, 'w') do |output| elements.each do |line| output.print line + "\n" end end end
Prints an array of formatted element string lines gathered by the print() method to the screen.
@param [Array<String>] elements an array of formatted element string lines
# File lib/dicom/parent.rb, line 723 def print_screen(elements) elements.each do |line| puts line end end
Handles the process of reading a data element from the DICOM
string, and creating an element object from the parsed data.
@return [Boolean] nil if end of string has been reached (in an expected way), false if the element parse failed, and true if an element was parsed successfully
# File lib/dicom/d_read.rb, line 71 def process_data_element # FIXME: This method has grown a bit messy and isn't very pleasant to read. Cleanup possible? # After having been into a possible unknown sequence with undefined length, we may need to reset # explicitness from implicit to explicit: if !@original_explicit.nil? && @explicitness_reset_parent == @current_parent @explicit = @original_explicit end # STEP 1: # Attempt to read data element tag: tag = read_tag # Return nil if we have (naturally) reached the end of the data string. return nil unless tag # STEP 2: # Access library to retrieve the data element name and VR from the tag we just read: # (Note: VR will be overwritten in the next step if the DICOM string contains VR (explicit encoding)) name, vr = LIBRARY.name_and_vr(tag) # STEP 3: # Read VR (if it exists) and the length value: vr, length = read_vr_length(vr,tag) level_vr = vr # STEP 4: # Reading value of data element. # Special handling needed for items in encapsulated image data: if @enc_image and tag == ITEM_TAG # The first item appearing after the image element is a 'normal' item, the rest hold image data. # Note that the first item will contain data if there are multiple images, and so must be read. vr = "OW" # how about alternatives like OB? # Modify name of item if this is an item that holds pixel data: if @current_element.tag != PIXEL_TAG name = PIXEL_ITEM_NAME end end # Read the binary string of the element: bin = read_bin(length) if length > 0 # Read the value of the element (if it contains data, and it is not a sequence or ordinary item): if length > 0 and vr != "SQ" and tag != ITEM_TAG # Read the element's processed value: value = read_value(vr, length) else # Data element has no value (data). value = nil # Special case: Check if pixel data element is sequenced: if tag == PIXEL_TAG # Change name and vr of pixel data element if it does not contain data itself: name = ENCAPSULATED_PIXEL_NAME level_vr = "SQ" @enc_image = true end end # Create an Element from the gathered data: # if vr is UN ("unknown") and length is -1, treat as a sequence (sec. 6.2.2 of DICOM standard) if level_vr == "SQ" or tag == ITEM_TAG or (level_vr == "UN" and length == -1) if level_vr == "SQ" or (level_vr == "UN" and length == -1) check_duplicate(tag, 'Sequence') # If we get an unknown sequence with undefined length, we must switch to implicit for decoding its content: if level_vr == "UN" and length == -1 @original_explicit = @explicit @explicit = false @explicitness_reset_parent = @current_parent end unless @current_parent[tag] and !@overwrite @current_element = Sequence.new(tag, :length => length, :name => name, :parent => @current_parent, :vr => vr) else # We have skipped a sequence. This means that any following children # of this sequence must be skipped as well. We solve this by creating an 'orphaned' # sequence that has a parent defined, but does not add itself to this parent: @current_element = Sequence.new(tag, :length => length, :name => name, :vr => vr) @current_element.set_parent(@current_parent) end elsif tag == ITEM_TAG # Create an Item: if @enc_image @current_element = Item.new(:bin => bin, :length => length, :name => name, :parent => @current_parent, :vr => vr) else @current_element = Item.new(:length => length, :name => name, :parent => @current_parent, :vr => vr) end end # Common operations on the two types of parent elements: if length == 0 and @enc_image # Set as parent. Exceptions when parent will not be set: # Item/Sequence has zero length & Item is a pixel item (which contains pixels, not child elements). @current_parent = @current_element elsif length != 0 @current_parent = @current_element unless name == PIXEL_ITEM_NAME end # If length is specified (no delimitation items), load a new DRead instance to read these child elements # and load them into the current sequence. The exception is when we have a pixel data item. if length > 0 and not @enc_image @current_element.parse(bin, @transfer_syntax, switched=@switched, @explicit) @current_parent = @current_parent.parent return false unless @read_success end elsif DELIMITER_TAGS.include?(tag) # We do not create an element for the delimiter items. # The occurance of such a tag indicates that a sequence or item has ended, and the parent must be changed: @current_parent = @current_parent.parent else check_duplicate(tag, 'Element') unless @current_parent[tag] and !@overwrite @current_element = Element.new(tag, value, :bin => bin, :name => name, :parent => @current_parent, :vr => vr) # Check that the data stream didn't end abruptly: raise "The actual length of the value (#{@current_element.bin.length}) does not match its specified length (#{length}) for Data Element #{@current_element.tag}" if length != @current_element.bin.length end end # Return true to indicate success: return true end
Builds a DICOM
object by parsing an encoded DICOM
string.
@param [String] string a binary DICOM
string to be parsed @param [Boolean] signature if true (default), the parsing algorithm will look for the DICOM
header signature @param [Hash] options the options to use for parsing the DICOM
string @option options [Boolean] :overwrite for the rare case of a DICOM
file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates @option options [String] :syntax if a syntax string is specified, the parsing algorithm is forced to use this transfer syntax when decoding the string
# File lib/dicom/d_read.rb, line 187 def read(string, signature=true, options={}) # (Re)Set variables: @str = string @overwrite = options[:overwrite] # Presence of the official DICOM signature: @signature = false # Default explicitness of start of DICOM string (if undefined it defaults to true): @explicit = options[:explicit].nil? ? true : options[:explicit] # Default endianness of start of DICOM string is little endian: @str_endian = false # A switch of endianness may occur after the initial meta group, an this needs to be monitored: @switched_endian = false # Explicitness of the remaining groups after the initial 0002 group: @rest_explicit = false # Endianness of the remaining groups after the first group: @rest_endian = false # When the string switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that: @switched = options[:switched] ? options[:switched] : false # Keeping track of the data element parent status while parsing the DICOM string: @current_parent = self # Keeping track of what is the current data element: @current_element = self # Items contained under the pixel data element may contain data directly, so we need a variable to keep track of this: @enc_image = false # Assume header size is zero bytes until otherwise is determined: @header_length = 0 # Assume string will be read successfully and toggle it later if we experience otherwise: @read_success = true # Our encoding instance: @stream = Stream.new(@str, @str_endian) # If a transfer syntax has been specified as an option for a DICOM object, # make sure that it makes it into the object: if options[:syntax] @transfer_syntax = options[:syntax] Element.new("0002,0010", options[:syntax], :parent => self) if self.is_a?(DObject) end # Check for header information if indicated: if signature # Read and verify the DICOM header: header = check_header # If the string is without the expected header, we will attempt # to read data elements from the very start of the string: if header == false @stream.skip(-132) elsif header.nil? # Not a valid DICOM string, return: @read_success = false return end end # Run a loop which parses Data Elements, one by one, until the end of the data string is reached: data_element = true while data_element do # Using a rescue clause since processing Data Elements can cause errors when parsing an invalid DICOM string. begin # Extracting Data element information (nil is returned if end of the string is encountered in a normal way). data_element = process_data_element rescue Exception => msg # The parse algorithm crashed. Set data_element as false to break # the loop and toggle the success boolean to indicate failure. @read_success = false data_element = false # Output the raised message as a warning: logger.warn(msg.to_s) # Ouput the backtrace as debug information: logger.debug(msg.backtrace) # Explain the failure as an error: logger.error("Parsing a Data Element has failed. This is likely caused by an invalid DICOM encoding.") end end end
Reads the data element's binary value string (varying length).
@param [Integer] length the length of the binary string to be extracted @return [String] the element value
# File lib/dicom/d_read.rb, line 264 def read_bin(length) return @stream.extract(length) end
Reads the data element's tag (the 4 first bytes of a data element).
@return [String, NilClass] the element tag, or nil (if end of string reached)
# File lib/dicom/d_read.rb, line 272 def read_tag tag = @stream.decode_tag if tag # When we shift from group 0002 to another group we need to update our endian/explicitness variables: if tag.group != META_GROUP and @switched == false switch_syntax_on_read # We may need to read our tag again if endian has switched (in which case it has been misread): if @switched_endian @stream.skip(-4) tag = @stream.decode_tag end end end return tag end
Decodes the data element's value (varying length).
-
Data elements which have multiple numbers as value, will have these numbers joined to a string, separated by the \ character.
-
For some value representations (OW, OB, OF, UN), a value is not processed, and nil is returned.
This means that for data like pixel data, compressed data, unknown data, a value is not available in the data element, and must be processed from the data element's binary variable.
@param [String] vr the value representation of the data element which the value to be decoded belongs to @param [Integer] length the length of the binary string to be extracted @return [String, NilClass] the data element value
# File lib/dicom/d_read.rb, line 300 def read_value(vr, length) unless vr == "OW" or vr == "OB" or vr == "OF" or vr == "UN" # Since the binary string has already been extracted for this data element, we must first "rewind": @stream.skip(-length) # Decode data: value = @stream.decode(length, vr) # If the returned value is an array of multiple values, we will join these values to a string with the separator "\": value = value.join("\\") if value.is_a?(Array) else # No decoded value: value = nil end return value end
Reads the data element's value representation (2 bytes), as well as the data element's length (varying length: 2-6 bytes). The decoding scheme to be applied depends on explicitness, data element type and vr.
@param [String] vr the value representation that was retrieved from the dictionary for the tag of this data element @param [String] tag the tag of this data element @return [Array<String, Integer>] the value representation and length of the element
# File lib/dicom/d_read.rb, line 323 def read_vr_length(vr, tag) # Structure will differ, dependent on whether we have explicit or implicit encoding: reserved = 0 bytes = 0 # *****EXPLICIT*****: if @explicit == true # Step 1: Read VR, 2 bytes (if it exists - which are all cases except for the item related elements) vr = @stream.decode(2, "STR") unless ITEM_TAGS.include?(tag) # Step 2: Read length # Three possible structures for value length here, dependent on element vr: case vr when "OB","OW","OF","SQ","UN","UT" # 6 bytes total (2 reserved bytes preceeding the 4 byte value length) reserved = 2 bytes = 4 when ITEM_VR # For the item elements: "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD": bytes = 4 else # For all the other element vr, value length is 2 bytes: bytes = 2 end else # *****IMPLICIT*****: bytes = 4 end # Handle skips and read out length value: @stream.skip(reserved) if bytes == 2 length = @stream.decode(bytes, "US") # (2) else length = @stream.decode(bytes, "SL") # (4) end # Check that length is valid (according to the DICOM standard, it must be even): raise "Encountered a Data Element (#{tag}) with an invalid (odd) value length." if length.odd? and length > 0 return vr, length end
Splits a pre-encoded string in parts and adds it to the segments instance array.
@param [String] string a pre-encoded string
# File lib/dicom/d_write.rb, line 127 def split_and_add(string) # Duplicate the string as not to ruin the binary of the data element with our slicing: segment = string.dup append = segment.slice!(0, @max_size-@stream.length) # Clear out the stream along with a small part of the string: @segments << @stream.export + append if (30 + segment.length) > @max_size # The remaining part of the string is bigger than the max limit, fill up more segments: # How many full segments will this string fill? number = (segment.length/@max_size.to_f).floor start_index = 0 number.times { @segments << segment.slice(start_index, @max_size) start_index += @max_size } # The remaining part is added to the stream: @stream.add_last(segment.slice(start_index, segment.length - start_index)) else # The rest of the string is small enough that it can be added to the stream: @stream.add_last(segment) end end
Changes encoding variables as the parsing proceeds past the initial meta group part (0002,xxxx) of the DICOM
string.
# File lib/dicom/d_read.rb, line 364 def switch_syntax_on_read # Get the transfer syntax string, unless it has already been provided by keyword: @transfer_syntax = (self["0002,0010"] ? self["0002,0010"].value : IMPLICIT_LITTLE_ENDIAN) unless @transfer_syntax # Query the library with our particular transfer syntax string: ts = LIBRARY.uid(@transfer_syntax) logger.warn("Invalid/unknown transfer syntax: #{@transfer_syntax} Will try parsing the string, but errors may occur.") unless ts && ts.transfer_syntax? @rest_explicit = ts ? ts.explicit? : true @rest_endian = ts ? ts.big_endian? : false # Make sure we only run this method once: @switched = true # Update endian, explicitness and unpack variables: @switched_endian = true if @rest_endian != @str_endian @str_endian = @rest_endian @stream.endian = @rest_endian @explicit = @rest_explicit end
Changes encoding variables as the file writing proceeds past the initial meta group part (0002,xxxx) of the DICOM
object.
# File lib/dicom/d_write.rb, line 340 def switch_syntax_on_write # Process the transfer syntax string to establish encoding settings: ts = LIBRARY.uid(@transfer_syntax) logger.warn("Invalid/unknown transfer syntax: #{@transfer_syntax} Will complete encoding the file, but an investigation of the result is recommended.") unless ts && ts.transfer_syntax? @rest_explicit = ts ? ts.explicit? : true @rest_endian = ts ? ts.big_endian? : false # Make sure we only run this method once: @switched = true # Update explicitness and endianness (pack/unpack variables): @explicit = @rest_explicit @str_endian = @rest_endian @stream.endian = @rest_endian end
Encodes and writes a single data element.
@param [Element, Item
, Sequence] element a data element
# File lib/dicom/d_write.rb, line 154 def write_data_element(element) # Step 1: Write tag: write_tag(element.tag) # Step 2: Write [VR] and value length: write_vr_length(element.tag, element.vr, element.length) # Step 3: Write value (Insert the already encoded binary string): write_value(element.bin) check_encapsulated_image(element) end
Iterates through the data elements, encoding/writing one by one. If an element has children, this method is repeated recursively.
@note Group length data elements are NOT written (they are deprecated/retired in the DICOM
standard).
@param [Array<Element, Item
, Sequence>] elements an array of data elements (sorted by their tags)
# File lib/dicom/d_write.rb, line 171 def write_data_elements(elements) elements.each do |element| # If this particular element has children, write these (recursively) before proceeding with elements at the current level: if element.is_parent? if element.children? # Sequence/Item with child elements: element.reset_length unless @enc_image write_data_element(element) write_data_elements(element.children) if @enc_image # Write a delimiter for the pixel tag, but not for its items: write_delimiter(element) if element.tag == PIXEL_TAG else write_delimiter(element) end else # Parent is childless: if element.bin write_data_element(element) if element.bin.length > 0 elsif @include_empty_parents # Only write empty/childless parents if specifically indicated: write_data_element(element) write_delimiter(element) end end else # Ordinary Data Element: if element.tag.group_length? # Among group length elements, only write the meta group element (the others have been retired in the DICOM standard): write_data_element(element) if element.tag == "0002,0000" else write_data_element(element) end end end end
Encodes and writes an Item
or Sequence
delimiter.
@param [Item, Sequence] element a parent element
# File lib/dicom/d_write.rb, line 212 def write_delimiter(element) delimiter_tag = (element.tag == ITEM_TAG ? ITEM_DELIMITER : SEQUENCE_DELIMITER) write_tag(delimiter_tag) write_vr_length(delimiter_tag, ITEM_VR, 0) end
Handles the encoding of DICOM
information to string as well as writing it to file.
@param [Hash] options the options to use for encoding the DICOM
string @option options [String] :file_name the path & name of the DICOM
file which is to be written to disk @option options [Boolean] :signature if true, the 128 byte preamble and 'DICM' signature is prepended to the encoded string @option options [String] :syntax the transfer syntax used for the encoding settings of the post-meta part of the DICOM
string
# File lib/dicom/d_write.rb, line 225 def write_elements(options={}) # Check if we are able to create given file: open_file(options[:file_name]) # Go ahead and write if the file was opened successfully: if @file # Initiate necessary variables: @transfer_syntax = options[:syntax] # Until a DICOM write has completed successfully the status is 'unsuccessful': @write_success = false # Default explicitness of start of DICOM file: @explicit = true # Default endianness of start of DICOM files (little endian): @str_endian = false # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that: @switched = false # Items contained under the Pixel Data element needs some special attention to write correctly: @enc_image = false # Create a Stream instance to handle the encoding of content to a binary string: @stream = Stream.new(nil, @str_endian) # Tell the Stream instance which file to write to: @stream.set_file(@file) # Write the DICOM signature: write_signature if options[:signature] write_data_elements(children) # As file has been written successfully, it can be closed. @file.close # Mark this write session as successful: @write_success = true end end
Writes the DICOM
header signature (128 bytes + 'DICM').
# File lib/dicom/d_write.rb, line 258 def write_signature # Write the string "DICM" which along with the empty bytes that # will be put before it, identifies this as a valid DICOM file: identifier = @stream.encode("DICM", "STR") # Fill in 128 empty bytes: filler = @stream.encode("00"*128, "HEX") @stream.write(filler) @stream.write(identifier) end
Encodes and writes a tag (the first part of the data element).
@param [String] tag a data element tag
# File lib/dicom/d_write.rb, line 272 def write_tag(tag) # Group 0002 is always little endian, but the rest of the file may be little or big endian. # When we shift from group 0002 to another group we need to update our endian/explicitness variables: switch_syntax_on_write if tag.group != META_GROUP and @switched == false # Write to binary string: bin_tag = @stream.encode_tag(tag) add_encoded(bin_tag) end
Writes the data element's pre-encoded value.
@param [String] bin the binary string value of this data element
# File lib/dicom/d_write.rb, line 285 def write_value(bin) # This is pretty straightforward, just dump the binary data to the file/string: add_encoded(bin) if bin end
Encodes and writes the value representation (if it is to be written) and length value. The encoding scheme to be applied here depends on explicitness, data element type and vr.
@param [String] tag the tag of this data element @param [String] vr the value representation of this data element @param [Integer] length the data element's length
# File lib/dicom/d_write.rb, line 297 def write_vr_length(tag, vr, length) # Encode the length value (cover both scenarios of 2 and 4 bytes): length4 = @stream.encode(length, "SL") length2 = @stream.encode(length, "US") # Structure will differ, dependent on whether we have explicit or implicit encoding: # *****EXPLICIT*****: if @explicit == true # Step 1: Write VR (if it is to be written) unless ITEM_TAGS.include?(tag) # Write data element VR (2 bytes - since we are not dealing with an item related element): add_encoded(@stream.encode(vr, "STR")) end # Step 2: Write length # Three possible structures for value length here, dependent on data element vr: case vr when "OB","OW","OF","SQ","UN","UT" if @enc_image # (4 bytes) # Item under an encapsulated Pixel Data (7FE0,0010). add_encoded(length4) else # (6 bytes total) # Two reserved bytes first: add_encoded(@stream.encode("00"*2, "HEX")) # Value length (4 bytes): add_encoded(length4) end when ITEM_VR # (4 bytes) # For the item elements: "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD" add_encoded(length4) else # (2 bytes) # For all the other data element vr, value length is 2 bytes: add_encoded(length2) end else # *****IMPLICIT*****: # No VR written. # Writing value length (4 bytes): add_encoded(length4) end end