class DICOM::Stream

The Stream class handles string operations (encoding to and decoding from binary strings). It is used by the various classes of ruby-dicom for tasks such as reading and writing from/to files or network packets.

@note In practice, this class is for internal library use. It is typically not accessed

by the user, and can thus be considered a 'private' class.

Attributes

equal_endian[R]

A boolean which reports the relationship between the endianness of the system and the instance string.

errors[R]

An array of warning/error messages that (may) have been accumulated.

index[RW]

Our current position in the instance string (used only for decoding).

pad_byte[R]

A hash with vr as key and its corresponding pad byte as value.

str_endian[R]

The endianness of the instance string.

string[RW]

The instance string.

Public Class Methods

new(binary, string_endian, options={}) click to toggle source

Creates a Stream instance.

@param [String, NilClass] binary a binary string (or nil, if creating an empty instance) @param [Boolean] string_endian the endianness of the instance string (true for big endian, false for small endian) @param [Hash] options the options to use for creating the instance @option options [Integer] :index a position (offset) in the instance string where reading will start

# File lib/dicom/stream.rb, line 32
def initialize(binary, string_endian, options={})
  @string = binary || ''
  @index = options[:index] || 0
  @errors = Array.new
  self.endian = string_endian
end

Public Instance Methods

add_first(binary) click to toggle source

Prepends a pre-encoded string to the instance string (inserts at the beginning).

@param [String] binary a binary string

# File lib/dicom/stream.rb, line 43
def add_first(binary)
  @string = "#{binary}#{@string}" if binary
end
add_last(binary) click to toggle source

Appends a pre-encoded string to the instance string (inserts at the end).

@param [String] binary a binary string

# File lib/dicom/stream.rb, line 51
def add_last(binary)
  @string = "#{@string}#{binary}" if binary
end
decode(length, type) click to toggle source

Decodes a section of the instance string. The instance index is offset in accordance with the length read.

@note If multiple numbers are decoded, these are returned in an array. @param [Integer] length the string length to be decoded @param [String] type the type (vr) of data to decode @return [String, Integer, Float, Array] the formatted (decoded) data

# File lib/dicom/stream.rb, line 63
def decode(length, type)
  raise ArgumentError, "Invalid argument length. Expected Integer, got #{length.class}" unless length.is_a?(Integer)
  raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
  value = nil
  if (@index + length) <= @string.length
    # There are sufficient bytes remaining to extract the value:
    if type == 'AT'
      # We need to guard ourselves against the case where a string contains an invalid 'AT' value:
      if length == 4
        value = decode_tag
      else
        # Invalid. Just return nil.
        skip(length)
      end
    else
      # Decode the binary string and return value:
      value = @string.slice(@index, length).unpack(vr_to_str(type))
      # If the result is an array of one element, return the element instead of the array.
      # If result is contained in a multi-element array, the original array is returned.
      if value.length == 1
        value = value[0]
        # If value is a string, strip away possible trailing whitespace:
        value = value.rstrip if value.is_a?(String)
      end
      # Update our position in the string:
      skip(length)
    end
  end
  value
end
decode_all(type) click to toggle source

Decodes the entire instance string (typically used for decoding image data).

@note If multiple numbers are decoded, these are returned in an array. @param [String] type the type (vr) of data to decode @return [String, Integer, Float, Array] the formatted (decoded) data

# File lib/dicom/stream.rb, line 100
def decode_all(type)
  length = @string.length
  value = @string.slice(@index, length).unpack(vr_to_str(type))
  skip(length)
  return value
end
decode_tag() click to toggle source

Decodes 4 bytes of the instance string and formats it as a ruby-dicom tag string.

@return [String, NilClass] a formatted tag string ('GGGG,EEEE'), or nil (e.g. if at end of string)

# File lib/dicom/stream.rb, line 111
def decode_tag
  length = 4
  tag = nil
  if (@index + length) <= @string.length
    # There are sufficient bytes remaining to extract a full tag:
    str = @string.slice(@index, length).unpack(@hex)[0].upcase
    if @equal_endian
      tag = "#{str[2..3]}#{str[0..1]},#{str[6..7]}#{str[4..5]}"
    else
      tag = "#{str[0..3]},#{str[4..7]}"
    end
    # Update our position in the string:
    skip(length)
  end
  tag
end
encode(value, type) click to toggle source

Encodes a given value to a binary string.

@param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers @param [String] type the type (vr) of data to encode @return [String] an encoded binary string

# File lib/dicom/stream.rb, line 134
def encode(value, type)
  raise ArgumentError, "Invalid argument type. Expected string, got #{type.class}" unless type.is_a?(String)
  value = [value] unless value.is_a?(Array)
  return value.pack(vr_to_str(type))
end
encode_first(value, type) click to toggle source

Encodes a value to a binary string and prepends it to the instance string.

@param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers @param [String] type the type (vr) of data to encode

# File lib/dicom/stream.rb, line 145
def encode_first(value, type)
  value = [value] unless value.is_a?(Array)
  @string = "#{value.pack(vr_to_str(type))}#{@string}"
end
encode_last(value, type) click to toggle source

Encodes a value to a binary string and appends it to the instance string.

@param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers @param [String] type the type (vr) of data to encode

# File lib/dicom/stream.rb, line 155
def encode_last(value, type)
  value = [value] unless value.is_a?(Array)
  @string = "#{@string}#{value.pack(vr_to_str(type))}"
end
encode_string_with_trailing_spaces(string, target_length) click to toggle source

Appends a string with trailling spaces to achieve a target length, and encodes it to a binary string.

@param [String] string a string to be padded @param [Integer] target_length the target length of the string @return [String] an encoded binary string

# File lib/dicom/stream.rb, line 166
def encode_string_with_trailing_spaces(string, target_length)
  length = string.length
  if length < target_length
    return "#{[string].pack(@str)}#{['20'*(target_length-length)].pack(@hex)}"
  elsif length == target_length
    return [string].pack(@str)
  else
    raise "The specified string is longer than the allowed maximum length (String: #{string}, Target length: #{target_length})."
  end
end
encode_tag(tag) click to toggle source

Encodes a tag from the ruby-dicom format ('GGGG,EEEE') to a proper binary string.

@param [String] tag a ruby-dicom type tag string @return [String] an encoded binary string

# File lib/dicom/stream.rb, line 182
def encode_tag(tag)
  [
    @equal_endian ? "#{tag[2..3]}#{tag[0..1]}#{tag[7..8]}#{tag[5..6]}" : "#{tag[0..3]}#{tag[5..8]}"
  ].pack(@hex)
end
encode_value(value, vr) click to toggle source

Encodes a value, and if the the resulting binary string has an odd length, appends a proper padding byte to make it even length.

@param [String, Integer, Float, Array] value a formatted value (String, Integer, etc..) or an array of numbers @param [String] vr the value representation of data to encode @return [String] the encoded binary string

# File lib/dicom/stream.rb, line 195
def encode_value(value, vr)
  if vr == 'AT'
    bin = encode_tag(value)
  else
    # Make sure the value is in an array:
    value = [value] unless value.is_a?(Array)
    # Get the proper pack string:
    type = vr_to_str(vr)
    # Encode:
    bin = value.pack(type)
    # Add an empty byte if the resulting binary has an odd length:
    bin = "#{bin}#{@pad_byte[vr]}" if bin.length.odd?
  end
  return bin
end
endian=(string_endian) click to toggle source

Sets the endianness of the instance string. The relationship between the string endianness and the system endianness determines which encoding/decoding flags to use.

@param [Boolean] string_endian the endianness of the instance string (true for big endian, false for small endian)

# File lib/dicom/stream.rb, line 216
def endian=(string_endian)
  @str_endian = string_endian
  configure_endian
  set_pad_byte
  set_string_formats
  set_format_hash
end
export(length=nil) click to toggle source

Extracts the entire instance string, or optionally, just the first part of it if a length is specified.

@note The exported string is removed from the instance string. @param [Integer] length the length of the string to cut out (if nil, the entire string is exported) @return [String] the instance string (or part of it)

# File lib/dicom/stream.rb, line 231
def export(length=nil)
  if length
    string = @string.slice!(0, length)
  else
    string = @string
    reset
  end
  return string
end
extract(length) click to toggle source

Extracts and returns a binary string of the given length, starting at the index position. The instance index is then offset in accordance with the length read.

@param [Integer] length the length of the string to be extracted @return [String] a part of the instance string

# File lib/dicom/stream.rb, line 247
def extract(length)
  str = @string.slice(@index, length)
  skip(length)
  return str
end
length() click to toggle source

Gives the length of the instance string.

@return [Integer] the instance string's length

# File lib/dicom/stream.rb, line 257
def length
  return @string.length
end
reset() click to toggle source

Resets the instance string and index.

# File lib/dicom/stream.rb, line 281
def reset
  @string = ''
  @index = 0
end
reset_index() click to toggle source

Resets the instance index.

# File lib/dicom/stream.rb, line 288
def reset_index
  @index = 0
end
rest_length() click to toggle source

Calculates the remaining length of the instance string (from the index position).

@return [Integer] the remaining length of the instance string

# File lib/dicom/stream.rb, line 265
def rest_length
  length = @string.length - @index
  return length
end
rest_string() click to toggle source

Extracts the remaining part of the instance string (from the index position to the end of the string).

@return [String] the remaining part of the instance string

# File lib/dicom/stream.rb, line 274
def rest_string
  str = @string[@index..(@string.length-1)]
  return str
end
set_file(file) click to toggle source

Sets the instance file variable.

@note For performance reasons, we enable the Stream instance to write directly to file,

to avoid expensive string operations which will otherwise slow down the write performance.

@param [File] file a File object

# File lib/dicom/stream.rb, line 299
def set_file(file)
  @file = file
end
set_string(binary) click to toggle source

Sets a new instance string, and resets the index variable.

@param [String] binary an encoded string

# File lib/dicom/stream.rb, line 307
def set_string(binary)
  binary = binary[0] if binary.is_a?(Array)
  @string = binary
  @index = 0
end
skip(offset) click to toggle source

Applies an offset (positive or negative) to the instance index.

@param [Integer] offset the length to skip (positive) or rewind (negative)

# File lib/dicom/stream.rb, line 317
def skip(offset)
  @index += offset
end
write(binary) click to toggle source

Writes a binary string to the File object of this instance.

@param [String] binary a binary string

# File lib/dicom/stream.rb, line 325
def write(binary)
  @file.write(binary)
end

Private Instance Methods

configure_endian() click to toggle source

Determines the relationship between system and string endianness, and sets the instance endian variable.

# File lib/dicom/stream.rb, line 335
def configure_endian
  if CPU_ENDIAN == @str_endian
    @equal_endian = true
  else
    @equal_endian = false
  end
end
set_format_hash() click to toggle source

Sets the hash which is used to convert data element types (VR) to encode/decode format strings accepted by Ruby's pack/unpack methods.

# File lib/dicom/stream.rb, line 360
def set_format_hash
  @format = {
    'BY' => @by, # Byte/Character (1-byte integers)
    'US' => @us, # Unsigned short (2 bytes)
    'SS' => @ss, # Signed short (2 bytes)
    'UL' => @ul, # Unsigned long (4 bytes)
    'SL' => @sl, # Signed long (4 bytes)
    'FL' => @fs, # Floating point single (4 bytes)
    'FD' => @fd, # Floating point double (8 bytes)
    'OB' => @by, # Other byte string (1-byte integers)
    'OF' => @fs, # Other float string (4-byte floating point numbers)
    'OW' => @us, # Other word string (2-byte integers)
    'AT' => @hex, # Tag reference (4 bytes) NB: For tags the spesialized encode_tag/decode_tag methods are used instead of this lookup table.
    'UN' => @hex, # Unknown information (header element is not recognized from local database)
    'HEX' => @hex, # HEX
    # We have a number of VRs that are decoded as string:
    'AE' => @str,
    'AS' => @str,
    'CS' => @str,
    'DA' => @str,
    'DS' => @str,
    'DT' => @str,
    'IS' => @str,
    'LO' => @str,
    'LT' => @str,
    'PN' => @str,
    'SH' => @str,
    'ST' => @str,
    'TM' => @str,
    'UI' => @str,
    'UT' => @str,
    'STR' => @str
  }
end
set_pad_byte() click to toggle source

Sets the hash which is used to keep track of which bytes to use for padding data elements of various vr which have an odd value length.

# File lib/dicom/stream.rb, line 398
def set_pad_byte
  @pad_byte = {
    # Space character:
    'AE' => "\x20",
    'AS' => "\x20",
    'CS' => "\x20",
    'DA' => "\x20",
    'DS' => "\x20",
    'DT' => "\x20",
    'IS' => "\x20",
    'LO' => "\x20",
    'LT' => "\x20",
    'PN' => "\x20",
    'SH' => "\x20",
    'ST' => "\x20",
    'TM' => "\x20",
    'UT' => "\x20",
    # Zero byte:
    'AT' => "\x00",
    'BY' => "\x00",
    'FL' => "\x00",
    'FD' => "\x00",
    'OB' => "\x00",
    'OF' => "\x00",
    'OW' => "\x00",
    'SL' => "\x00",
    'SQ' => "\x00",
    'SS' => "\x00",
    'UI' => "\x00",
    'UL' => "\x00",
    'UN' => "\x00",
    'US' => "\x00"
  }
end
set_string_formats() click to toggle source

Sets the pack/unpack format strings that are used for encoding/decoding. Some of these depends on the endianness of the system and the encoded string.

# File lib/dicom/stream.rb, line 436
def set_string_formats
  if @equal_endian
    # Little endian byte order:
    @us = 'S<*' # Unsigned short (2 bytes)
    @ss = 's<*' # Signed short (2 bytes)
    @ul = 'L<*' # Unsigned long (4 bytes)
    @sl = 'l<*' # Signed long (4 bytes)
    @fs = 'e*' # Floating point single (4 bytes)
    @fd = 'E*' # Floating point double ( 8 bytes)
  else
    # Network (big endian) byte order:
    @us = 'S>*'
    @ss = 's>*'
    @ul = 'L>*'
    @sl = 'l>'
    @fs = 'g*'
    @fd = 'G*'
  end
  # Format strings that are not dependent on endianness:
  @by = 'C*' # Unsigned char (1 byte)
  @str = 'a*'
  @hex = 'H*' # (this may be dependent on endianness(?))
end
vr_to_str(vr) click to toggle source

Converts a data type/vr to an encode/decode string used by Ruby's pack/unpack methods.

@param [String] vr a value representation (data type) @return [String] an encode/decode format string

# File lib/dicom/stream.rb, line 348
def vr_to_str(vr)
  unless @format[vr]
    errors << "Warning: Element type #{vr} does not have a reading method assigned to it. Something is not implemented correctly or the DICOM data analyzed is invalid."
    return @hex
  else
    return @format[vr]
  end
end