dicom 0.5 → 0.6
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.
- data/CHANGELOG +20 -4
- data/DOCUMENTATION +171 -1
- data/README +11 -3
- data/lib/DClient.rb +579 -0
- data/lib/DLibrary.rb +99 -75
- data/lib/DObject.rb +213 -262
- data/lib/DRead.rb +229 -300
- data/lib/DServer.rb +290 -0
- data/lib/DWrite.rb +218 -234
- data/lib/Dictionary.rb +2859 -2860
- data/lib/Link.rb +1079 -0
- data/lib/Stream.rb +351 -0
- data/lib/dicom.rb +7 -2
- data/lib/ruby_extensions.rb +11 -0
- metadata +10 -6
    
        data/lib/DServer.rb
    ADDED
    
    | @@ -0,0 +1,290 @@ | |
| 1 | 
            +
            #    Copyright 2009 Christoffer Lervag
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module DICOM
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              # This class contains code for setting up a Service Class Provider (SCP),
         | 
| 6 | 
            +
              # which will act as a simple storage node (a server that receives images).
         | 
| 7 | 
            +
              class DServer
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_accessor :host_ae, :max_package_size, :port, :timeout, :verbose
         | 
| 10 | 
            +
                attr_reader :errors, :notices
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                # Initialize the instance with a host adress and a port number.
         | 
| 13 | 
            +
                def initialize(port, options={})
         | 
| 14 | 
            +
                  require 'socket'
         | 
| 15 | 
            +
                  # Required parameters:
         | 
| 16 | 
            +
                  @port = port
         | 
| 17 | 
            +
                  # Optional parameters (and default values):
         | 
| 18 | 
            +
                  @lib =  options[:lib]  || DLibrary.new
         | 
| 19 | 
            +
                  @host_ae =  options[:host_ae]  || "RUBY_DICOM"
         | 
| 20 | 
            +
                  @max_package_size = options[:max_package_size] || 32768 # 16384
         | 
| 21 | 
            +
                  @timeout = options[:timeout] || 10 # seconds
         | 
| 22 | 
            +
                  @min_length = 12 # minimum number of bytes to expect in an incoming transmission
         | 
| 23 | 
            +
                  @verbose = options[:verbose]
         | 
| 24 | 
            +
                  @verbose = true if @verbose == nil # Default verbosity is 'on'.
         | 
| 25 | 
            +
                  # Other instance variables:
         | 
| 26 | 
            +
                  @errors = Array.new # errors and warnings are put in this array
         | 
| 27 | 
            +
                  @notices = Array.new # information on successful transmissions are put in this array
         | 
| 28 | 
            +
                  # Variables used for monitoring state of transmission:
         | 
| 29 | 
            +
                  @connection = nil # TCP connection status
         | 
| 30 | 
            +
                  @association = nil # DICOM Association status
         | 
| 31 | 
            +
                  @request_approved = nil # Status of our DICOM request
         | 
| 32 | 
            +
                  @release = nil # Status of received, valid release response
         | 
| 33 | 
            +
                  set_valid_abstract_syntaxes
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
             | 
| 37 | 
            +
                # Add a specified abstract syntax to the list of syntaxes that the server instance will accept.
         | 
| 38 | 
            +
                def add_abstract_syntax(value)
         | 
| 39 | 
            +
                  if value.is_a?(String)
         | 
| 40 | 
            +
                    @valid_abstract_syntaxes << value
         | 
| 41 | 
            +
                    @valid_abstract_syntaxes.sort!
         | 
| 42 | 
            +
                  else
         | 
| 43 | 
            +
                    add_error("Error: The specified abstract syntax is not a string!")
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
                # Print the list of valid abstract syntaxes to the screen.
         | 
| 49 | 
            +
                def print_syntaxes
         | 
| 50 | 
            +
                  puts "Abstract syntaxes accepted by this SCP:"
         | 
| 51 | 
            +
                  @valid_abstract_syntaxes.each do |syntax|
         | 
| 52 | 
            +
                    puts syntax
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
             | 
| 57 | 
            +
                # Remove a specific abstract syntax from the list of syntaxes that the server instance will accept.
         | 
| 58 | 
            +
                def remove_abstract_syntax(value)
         | 
| 59 | 
            +
                  if value.is_a?(String)
         | 
| 60 | 
            +
                    # Remove it:
         | 
| 61 | 
            +
                    @valid_abstract_syntaxes.delete(value)
         | 
| 62 | 
            +
                  else
         | 
| 63 | 
            +
                    add_error("Error: The specified abstract syntax is not a string!")
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
             | 
| 68 | 
            +
                # Completely clear the list of syntaxes that the server instance will accept.
         | 
| 69 | 
            +
                def remove_all_abstract_syntaxes
         | 
| 70 | 
            +
                  @valid_abstract_syntaxes = Array.new
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
             | 
| 74 | 
            +
                # Start a Storage Content Provider (SCP).
         | 
| 75 | 
            +
                # This service will receive and store DICOM files in a specified folder.
         | 
| 76 | 
            +
                def start_scp(path)
         | 
| 77 | 
            +
                  add_notice("Starting SCP server...")
         | 
| 78 | 
            +
                  add_notice("*********************************")
         | 
| 79 | 
            +
                  # Initiate server:
         | 
| 80 | 
            +
                  @scp = TCPServer.new(@port)
         | 
| 81 | 
            +
                  # Use a loop to listen for incoming messages:
         | 
| 82 | 
            +
                  loop do
         | 
| 83 | 
            +
                    Thread.start(@scp.accept) do |session|
         | 
| 84 | 
            +
                      # Initialize the network package handler for this session:
         | 
| 85 | 
            +
                      link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose)
         | 
| 86 | 
            +
                      add_notice("Connection established (name: #{session.peeraddr[2]}, ip: #{session.peeraddr[3]})")
         | 
| 87 | 
            +
                      # Receive an incoming message:
         | 
| 88 | 
            +
                      segments = link.receive_single_transmission(session)
         | 
| 89 | 
            +
                      info = segments.first
         | 
| 90 | 
            +
                      # Interpret the received message:
         | 
| 91 | 
            +
                      if info[:valid]
         | 
| 92 | 
            +
                        association_error = check_association_request(info)
         | 
| 93 | 
            +
                        unless association_error
         | 
| 94 | 
            +
                          syntax_result = check_syntax_requests(info)
         | 
| 95 | 
            +
                          link.handle_association_accept(session, info, syntax_result)
         | 
| 96 | 
            +
                          if syntax_result == "00" # Normal (no error)
         | 
| 97 | 
            +
                            add_notice("An incoming association request and its abstract syntax has been accepted.")
         | 
| 98 | 
            +
                            if info[:abstract_syntax] == "1.2.840.10008.1.1"
         | 
| 99 | 
            +
                              # Verification SOP Class (used for testing connections):
         | 
| 100 | 
            +
                              link.handle_release(session)
         | 
| 101 | 
            +
                            else
         | 
| 102 | 
            +
                              # Process the incoming data:
         | 
| 103 | 
            +
                              file_path = link.handle_incoming_data(session, path)
         | 
| 104 | 
            +
                              add_notice("DICOM file saved to: " + file_path)
         | 
| 105 | 
            +
                              # Send a receipt for received data:
         | 
| 106 | 
            +
                              link.handle_response(session)
         | 
| 107 | 
            +
                              # Release the connection:
         | 
| 108 | 
            +
                              link.handle_release(session)
         | 
| 109 | 
            +
                            end
         | 
| 110 | 
            +
                          else
         | 
| 111 | 
            +
                            # Abstract syntax in the incoming request was not accepted:
         | 
| 112 | 
            +
                            add_notice("An incoming association request was accepted, but it's abstract syntax was rejected. (#{abstract_syntax})")
         | 
| 113 | 
            +
                            # Since the requested abstract syntax was not accepted, the association must be released.
         | 
| 114 | 
            +
                            link.handle_release(session)
         | 
| 115 | 
            +
                          end
         | 
| 116 | 
            +
                        else
         | 
| 117 | 
            +
                          # The incoming association was not formally correct.
         | 
| 118 | 
            +
                          link.handle_rejection(session)
         | 
| 119 | 
            +
                        end
         | 
| 120 | 
            +
                      else
         | 
| 121 | 
            +
                        # The incoming message was not recognised as a valid DICOM message. Abort:
         | 
| 122 | 
            +
                        link.handle_abort(session)
         | 
| 123 | 
            +
                      end
         | 
| 124 | 
            +
                      # Terminate the connection:
         | 
| 125 | 
            +
                      session.close unless session.closed?
         | 
| 126 | 
            +
                      add_notice("Connection closed.")
         | 
| 127 | 
            +
                      add_notice("*********************************")
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
             | 
| 133 | 
            +
                # Following methods are private:
         | 
| 134 | 
            +
                private
         | 
| 135 | 
            +
             | 
| 136 | 
            +
             | 
| 137 | 
            +
                # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
         | 
| 138 | 
            +
                def add_error(error)
         | 
| 139 | 
            +
                  if @verbose
         | 
| 140 | 
            +
                    puts error
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                  @errors << error
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
             | 
| 146 | 
            +
                # Adds a notice (information regarding progress or successful communications) to the instance array,
         | 
| 147 | 
            +
                # and if verbosity is set for these kinds of messages, prints it to the screen as well.
         | 
| 148 | 
            +
                def add_notice(notice)
         | 
| 149 | 
            +
                  if @verbose
         | 
| 150 | 
            +
                    puts notice
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                  @notices << notice
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
             | 
| 156 | 
            +
                # Check if the association request is formally correct.
         | 
| 157 | 
            +
                # Things that can be checked here, are:
         | 
| 158 | 
            +
                # Application context name, calling AE title, called AE title
         | 
| 159 | 
            +
                # Error codes are given in the official dicom document, part 08_08, page 41
         | 
| 160 | 
            +
                def check_association_request(info)
         | 
| 161 | 
            +
                  error = nil
         | 
| 162 | 
            +
                  # For the moment there is no control on AE titles.
         | 
| 163 | 
            +
                  # Check that Application context name is as expected:
         | 
| 164 | 
            +
                  if info[:application_context] != "1.2.840.10008.3.1.1.1"
         | 
| 165 | 
            +
                    error = "02" # application context name not supported
         | 
| 166 | 
            +
                    add_error("Warning: Application context not recognised in the incoming association request. (#{info[:application_context]})")
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                  return error
         | 
| 169 | 
            +
                end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
             | 
| 172 | 
            +
                # Check if the requested abstract syntax & transfer syntax are supported:
         | 
| 173 | 
            +
                # Error codes are given in the official dicom document, part 08_08, page 39
         | 
| 174 | 
            +
                def check_syntax_requests(info)
         | 
| 175 | 
            +
                  result = "00" # (no error)
         | 
| 176 | 
            +
                  # We will accept any transfer syntax (as long as it is recognized in the library):
         | 
| 177 | 
            +
                  # (Weakness: Only checking the first occuring transfer syntax for now)
         | 
| 178 | 
            +
                  transfer_syntax = info[:ts].first[:transfer_syntax]
         | 
| 179 | 
            +
                  unless @lib.check_ts_validity(transfer_syntax)
         | 
| 180 | 
            +
                    result = "04" # transfer syntax not supported
         | 
| 181 | 
            +
                    add_error("Warning: Unsupported transfer syntax received in incoming association request. (#{transfer_syntax})")
         | 
| 182 | 
            +
                  end
         | 
| 183 | 
            +
                  # Check that abstract syntax is among the ones that have been set as valid for this server instance:
         | 
| 184 | 
            +
                  abstract_syntax = info[:abstract_syntax]
         | 
| 185 | 
            +
                  unless @valid_abstract_syntaxes.include?(abstract_syntax)
         | 
| 186 | 
            +
                    result = "03" # abstract syntax not supported
         | 
| 187 | 
            +
                  end
         | 
| 188 | 
            +
                  return result
         | 
| 189 | 
            +
                end
         | 
| 190 | 
            +
             | 
| 191 | 
            +
             | 
| 192 | 
            +
                # Set the default valid abstract syntaxes for our SCP.
         | 
| 193 | 
            +
                def set_valid_abstract_syntaxes
         | 
| 194 | 
            +
                  @valid_abstract_syntaxes = [
         | 
| 195 | 
            +
                    "1.2.840.10008.1.1", # "Verification SOP Class"
         | 
| 196 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1", # "Computed Radiography Image Storage"
         | 
| 197 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1.1", # "Digital X-Ray Image Storage - For Presentation"
         | 
| 198 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1.1.1", # "Digital X-Ray Image Storage - For Processing"
         | 
| 199 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1.2", # "Digital Mammography X-Ray Image Storage - For Presentation"
         | 
| 200 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1.2.1", # "Digital Mammography X-Ray Image Storage - For Processing"
         | 
| 201 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1.3", # "Digital Intra-oral X-Ray Image Storage - For Presentation"
         | 
| 202 | 
            +
                    "1.2.840.10008.5.1.4.1.1.1.3.1", # "Digital Intra-oral X-Ray Image Storage - For Processing"
         | 
| 203 | 
            +
                    "1.2.840.10008.5.1.4.1.1.2", # "CT Image Storage"
         | 
| 204 | 
            +
                    "1.2.840.10008.5.1.4.1.1.2.1", # "Enhanced CT Image Storage"
         | 
| 205 | 
            +
                    "1.2.840.10008.5.1.4.1.1.3", # "Ultrasound Multi-frame Image Storage" # RET
         | 
| 206 | 
            +
                    "1.2.840.10008.5.1.4.1.1.3.1", # "Ultrasound Multi-frame Image Storage"
         | 
| 207 | 
            +
                    "1.2.840.10008.5.1.4.1.1.4", # "MR Image Storage"
         | 
| 208 | 
            +
                    "1.2.840.10008.5.1.4.1.1.4.1", # "Enhanced MR Image Storage"
         | 
| 209 | 
            +
                    "1.2.840.10008.5.1.4.1.1.4.2", # "MR Spectroscopy Storage"
         | 
| 210 | 
            +
                    "1.2.840.10008.5.1.4.1.1.5", # "Nuclear Medicine Image Storage"
         | 
| 211 | 
            +
                    "1.2.840.10008.5.1.4.1.1.6", # "Ultrasound Image Storage"
         | 
| 212 | 
            +
                    "1.2.840.10008.5.1.4.1.1.6.1", # "Ultrasound Image Storage"
         | 
| 213 | 
            +
                    "1.2.840.10008.5.1.4.1.1.7", # "Secondary Capture Image Storage"
         | 
| 214 | 
            +
                    "1.2.840.10008.5.1.4.1.1.7.1", # "Multi-frame Single Bit Secondary Capture Image Storage"
         | 
| 215 | 
            +
                    "1.2.840.10008.5.1.4.1.1.7.2", # "Multi-frame Grayscale Byte Secondary Capture Image Storage"
         | 
| 216 | 
            +
                    "1.2.840.10008.5.1.4.1.1.7.3", # "Multi-frame Grayscale Word Secondary Capture Image Storage"
         | 
| 217 | 
            +
                    "1.2.840.10008.5.1.4.1.1.7.4", # "Multi-frame True Color Secondary Capture Image Storage"
         | 
| 218 | 
            +
                    "1.2.840.10008.5.1.4.1.1.8", # "Standalone Overlay Storage" # RET
         | 
| 219 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9", # "Standalone Curve Storage" # RET
         | 
| 220 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.1", # "Waveform Storage - Trial" # RET
         | 
| 221 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.1.1", # "12-lead ECG Waveform Storage"
         | 
| 222 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.1.2", # "General ECG Waveform Storage"
         | 
| 223 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.1.3", # "Ambulatory ECG Waveform Storage"
         | 
| 224 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.2.1", # "Hemodynamic Waveform Storage"
         | 
| 225 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.3.1", # "Cardiac Electrophysiology Waveform Storage"
         | 
| 226 | 
            +
                    "1.2.840.10008.5.1.4.1.1.9.4.1", # "Basic Voice Audio Waveform Storage"
         | 
| 227 | 
            +
                    "1.2.840.10008.5.1.4.1.1.10", # "Standalone Modality LUT Storage" # RET
         | 
| 228 | 
            +
                    "1.2.840.10008.5.1.4.1.1.11", # "Standalone VOI LUT Storage" # RET
         | 
| 229 | 
            +
                    "1.2.840.10008.5.1.4.1.1.11.1", # "Grayscale Softcopy Presentation State Storage SOP Class"
         | 
| 230 | 
            +
                    "1.2.840.10008.5.1.4.1.1.11.2", # "Color Softcopy Presentation State Storage SOP Class"
         | 
| 231 | 
            +
                    "1.2.840.10008.5.1.4.1.1.11.3", # "Pseudo-Color Softcopy Presentation State Storage SOP Class"
         | 
| 232 | 
            +
                    "1.2.840.10008.5.1.4.1.1.11.4", # "Blending Softcopy Presentation State Storage SOP Class"
         | 
| 233 | 
            +
                    "1.2.840.10008.5.1.4.1.1.12.1", # "X-Ray Angiographic Image Storage"
         | 
| 234 | 
            +
                    "1.2.840.10008.5.1.4.1.1.12.1.1", # "Enhanced XA Image Storage"
         | 
| 235 | 
            +
                    "1.2.840.10008.5.1.4.1.1.12.2", # "X-Ray Radiofluoroscopic Image Storage"
         | 
| 236 | 
            +
                    "1.2.840.10008.5.1.4.1.1.12.2.1", # "Enhanced XRF Image Storage"
         | 
| 237 | 
            +
                    "1.2.840.10008.5.1.4.1.1.13.1.1", # "X-Ray 3D Angiographic Image Storage"
         | 
| 238 | 
            +
                    "1.2.840.10008.5.1.4.1.1.13.1.2", # "X-Ray 3D Craniofacial Image Storage"
         | 
| 239 | 
            +
                    "1.2.840.10008.5.1.4.1.1.12.3", # "X-Ray Angiographic Bi-Plane Image Storage" # RET
         | 
| 240 | 
            +
                    "1.2.840.10008.5.1.4.1.1.20", # "Nuclear Medicine Image Storage"
         | 
| 241 | 
            +
                    "1.2.840.10008.5.1.4.1.1.66", # "Raw Data Storage"
         | 
| 242 | 
            +
                    "1.2.840.10008.5.1.4.1.1.66.1", # "Spatial Registration Storage"
         | 
| 243 | 
            +
                    "1.2.840.10008.5.1.4.1.1.66.2", # "Spatial Fiducials Storage"
         | 
| 244 | 
            +
                    "1.2.840.10008.5.1.4.1.1.66.3", # "Deformable Spatial Registration Storage"
         | 
| 245 | 
            +
                    "1.2.840.10008.5.1.4.1.1.66.4", # "Segmentation Storage"
         | 
| 246 | 
            +
                    "1.2.840.10008.5.1.4.1.1.67", # "Real World Value Mapping Storage"
         | 
| 247 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1", # "VL Image Storage - Trial" # RET
         | 
| 248 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.2", # "VL Multi-frame Image Storage - Trial" # RET
         | 
| 249 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.1", # "VL Endoscopic Image Storage"
         | 
| 250 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.1.1", # "Video Endoscopic Image Storage"
         | 
| 251 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.2", # "VL Microscopic Image Storage"
         | 
| 252 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.2.1", # "Video Microscopic Image Storage"
         | 
| 253 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.3", # "VL Slide-Coordinates Microscopic Image Storage"
         | 
| 254 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.4", # "VL Photographic Image Storage"
         | 
| 255 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.4.1", # "Video Photographic Image Storage"
         | 
| 256 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.5.1", # "Ophthalmic Photography 8 Bit Image Storage"
         | 
| 257 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.5.2", # "Ophthalmic Photography 16 Bit Image Storage"
         | 
| 258 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.5.3", # "Stereometric Relationship Storage"
         | 
| 259 | 
            +
                    "1.2.840.10008.5.1.4.1.1.77.1.5.4", # "Ophthalmic Tomography Image Storage"
         | 
| 260 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.1", # "Text SR Storage - Trial" # RET
         | 
| 261 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.2", # "Audio SR Storage - Trial" # RET
         | 
| 262 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.3", # "Detail SR Storage - Trial" # RET
         | 
| 263 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.4", # "Comprehensive SR Storage - Trial" # RET
         | 
| 264 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.11", # "Basic Text SR Storage"
         | 
| 265 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.22", # "Enhanced SR Storage"
         | 
| 266 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.33", # "Comprehensive SR Storage"
         | 
| 267 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.40", # "Procedure Log Storage"
         | 
| 268 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.50", # "Mammography CAD SR Storage"
         | 
| 269 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.59", # "Key Object Selection Document Storage"
         | 
| 270 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.65", # "Chest CAD SR Storage"
         | 
| 271 | 
            +
                    "1.2.840.10008.5.1.4.1.1.88.67", # "X-Ray Radiation Dose SR Storage"
         | 
| 272 | 
            +
                    "1.2.840.10008.5.1.4.1.1.104.1", # "Encapsulated PDF Storage"
         | 
| 273 | 
            +
                    "1.2.840.10008.5.1.4.1.1.104.2", # "Encapsulated CDA Storage"
         | 
| 274 | 
            +
                    "1.2.840.10008.5.1.4.1.1.128", # "Positron Emission Tomography Image Storage"
         | 
| 275 | 
            +
                    "1.2.840.10008.5.1.4.1.1.129", # "Standalone PET Curve Storage" # RET
         | 
| 276 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.1", # "RT Image Storage"
         | 
| 277 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.2", # "RT Dose Storage"
         | 
| 278 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.3", # "RT Structure Set Storage"
         | 
| 279 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.4", # "RT Beams Treatment Record Storage"
         | 
| 280 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.5", # "RT Plan Storage"
         | 
| 281 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.6", # "RT Brachy Treatment Record Storage"
         | 
| 282 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.7", # "RT Treatment Summary Record Storage"
         | 
| 283 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.8", # "RT Ion Plan Storage"
         | 
| 284 | 
            +
                    "1.2.840.10008.5.1.4.1.1.481.9" # "RT Ion Beams Treatment Record Storage"
         | 
| 285 | 
            +
                  ]
         | 
| 286 | 
            +
                end
         | 
| 287 | 
            +
             | 
| 288 | 
            +
             | 
| 289 | 
            +
              end
         | 
| 290 | 
            +
            end
         | 
    
        data/lib/DWrite.rb
    CHANGED
    
    | @@ -12,15 +12,17 @@ | |
| 12 12 | 
             
            module DICOM
         | 
| 13 13 | 
             
              # Class for writing the data from DObject to a valid DICOM file:
         | 
| 14 14 | 
             
              class DWrite
         | 
| 15 | 
            +
             | 
| 15 16 | 
             
                attr_writer :tags, :types, :lengths, :raw, :rest_endian, :rest_explicit
         | 
| 16 17 | 
             
                attr_reader :success, :msg
         | 
| 17 18 |  | 
| 18 19 | 
             
                # Initialize the DWrite instance.
         | 
| 19 | 
            -
                def initialize(file_name=nil,  | 
| 20 | 
            +
                def initialize(file_name=nil, options={})
         | 
| 20 21 | 
             
                  # Process option values, setting defaults for the ones that are not specified:
         | 
| 21 | 
            -
                  @lib =   | 
| 22 | 
            -
                  @sys_endian =  | 
| 22 | 
            +
                  @lib =  options[:lib] || DLibrary.new
         | 
| 23 | 
            +
                  @sys_endian = options[:sys_endian] || false
         | 
| 23 24 | 
             
                  @file_name = file_name
         | 
| 25 | 
            +
                  @transfer_syntax = options[:transfer_syntax] || "1.2.840.10008.1.2" # Implicit, little endian
         | 
| 24 26 |  | 
| 25 27 | 
             
                  # Create arrays used for storing data element information:
         | 
| 26 28 | 
             
                  @tags = Array.new
         | 
| @@ -34,147 +36,179 @@ module DICOM | |
| 34 36 | 
             
                  @rest_explicit = false
         | 
| 35 37 | 
             
                  # Endianness of the remaining groups after the first group:
         | 
| 36 38 | 
             
                  @rest_endian = false
         | 
| 37 | 
            -
                end | 
| 39 | 
            +
                end
         | 
| 38 40 |  | 
| 39 41 |  | 
| 40 42 | 
             
                # Writes the DICOM information to file.
         | 
| 41 | 
            -
                def write()
         | 
| 42 | 
            -
                  if  | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                     | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 43 | 
            +
                def write(body = nil)
         | 
| 44 | 
            +
                  # Check if we are able to create given file:
         | 
| 45 | 
            +
                  open_file(@file_name)
         | 
| 46 | 
            +
                  # Go ahead and write if the file was opened successfully:
         | 
| 47 | 
            +
                  if @file != nil
         | 
| 48 | 
            +
                    # Initiate necessary variables:
         | 
| 49 | 
            +
                    init_variables
         | 
| 50 | 
            +
                    # Create a Stream instance to handle the encoding of content to
         | 
| 51 | 
            +
                    # the binary string that will eventually be saved to file:
         | 
| 52 | 
            +
                    @stream = Stream.new(nil, @file_endian, @explicit)
         | 
| 53 | 
            +
                    # Tell the Stream instance which file to write to:
         | 
| 54 | 
            +
                    @stream.set_file(@file)
         | 
| 55 | 
            +
                    # Write header:
         | 
| 56 | 
            +
                    write_header
         | 
| 57 | 
            +
                    # Meta information:
         | 
| 58 | 
            +
                    # A simple check to determine whether we need to write meta information to the DICOM object:
         | 
| 59 | 
            +
                    if @tags.length > 0
         | 
| 60 | 
            +
                      write_meta unless @tags[0].include?("0002")
         | 
| 61 | 
            +
                    else
         | 
| 62 | 
            +
                      write_meta
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                    # Write either body or data elements:
         | 
| 65 | 
            +
                    if body
         | 
| 66 | 
            +
                      @stream.add_last(body)
         | 
| 67 | 
            +
                    else
         | 
| 54 68 | 
             
                      @tags.each_index do |i|
         | 
| 55 69 | 
             
                        write_data_element(i)
         | 
| 56 70 | 
             
                      end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                    # As file has been written successfully, it can be closed.
         | 
| 73 | 
            +
                    @file.close
         | 
| 74 | 
            +
                    # Mark this write session as successful:
         | 
| 75 | 
            +
                    @success = true
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
             | 
| 80 | 
            +
                # Write DICOM content to a series of size-limited binary strings
         | 
| 81 | 
            +
                # (typically used when transmitting DICOM objects through network connections)
         | 
| 82 | 
            +
                # The method returns an array of binary strings.
         | 
| 83 | 
            +
                def encode_segments(size)
         | 
| 84 | 
            +
                  # Initiate necessary variables:
         | 
| 85 | 
            +
                  init_variables
         | 
| 86 | 
            +
                  # When sending a DICOM file across the network, no header or meta information is needed.
         | 
| 87 | 
            +
                  # We must therefore find the position of the first tag which is not a meta information tag.
         | 
| 88 | 
            +
                  first_pos = first_non_meta
         | 
| 89 | 
            +
                  last_pos = @tags.length - 1
         | 
| 90 | 
            +
                  # Create a Stream instance to handle the encoding of content to
         | 
| 91 | 
            +
                  # the binary string that will eventually be saved to file:
         | 
| 92 | 
            +
                  @stream = Stream.new(nil, @file_endian, @explicit)
         | 
| 93 | 
            +
                  # Start encoding data elements, and start on a new string when the size limit is reached:
         | 
| 94 | 
            +
                  segments = Array.new
         | 
| 95 | 
            +
                  (first_pos..last_pos).each do |i|
         | 
| 96 | 
            +
                    value_length = @lengths[i].to_i
         | 
| 97 | 
            +
                    # Test the length of the upcoming data element against our size limitation:
         | 
| 98 | 
            +
                    if value_length > size
         | 
| 99 | 
            +
                      # Start writing content from this data element,
         | 
| 100 | 
            +
                      # then continue writing its content in the next segments.
         | 
| 101 | 
            +
                      # Write tag & type/length:
         | 
| 102 | 
            +
                      write_tag(i)
         | 
| 103 | 
            +
                      write_type_length(i)
         | 
| 104 | 
            +
                      # Find out how much of this element's value we can write, then add it:
         | 
| 105 | 
            +
                      available = size - @stream.length
         | 
| 106 | 
            +
                      value_first_part = @raw[i].slice(0, available)
         | 
| 107 | 
            +
                      @stream.add_last(value_first_part)
         | 
| 108 | 
            +
                      # Add segment and reset:
         | 
| 109 | 
            +
                      segments << @stream.string
         | 
| 110 | 
            +
                      @stream.reset
         | 
| 111 | 
            +
                      # Find out how many more segments our data element value will fill:
         | 
| 112 | 
            +
                      remaining_segments = ((value_length - available).to_f / size.to_f).ceil
         | 
| 113 | 
            +
                      index = available
         | 
| 114 | 
            +
                      # Iterate through the data element's value until we have added it entirely:
         | 
| 115 | 
            +
                      remaining_segments.times do
         | 
| 116 | 
            +
                        value = @raw[i].slice(index, size)
         | 
| 117 | 
            +
                        index = index + size
         | 
| 118 | 
            +
                        @stream.add_last(value)
         | 
| 119 | 
            +
                        # Add segment and reset:
         | 
| 120 | 
            +
                        segments << @stream.string
         | 
| 121 | 
            +
                        @stream.reset
         | 
| 122 | 
            +
                      end
         | 
| 123 | 
            +
                    elsif (10 + value_length + @stream.length) >= size
         | 
| 124 | 
            +
                      # End the current segment, and start on a new segment for the next data element.
         | 
| 125 | 
            +
                      segments << @stream.string
         | 
| 126 | 
            +
                      @stream.reset
         | 
| 127 | 
            +
                      write_data_element(i)
         | 
| 61 128 | 
             
                    else
         | 
| 62 | 
            -
                      #  | 
| 63 | 
            -
                       | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
                   | 
| 67 | 
            -
             | 
| 68 | 
            -
                   | 
| 69 | 
            -
                end | 
| 129 | 
            +
                      # Write the next data element to the current segment:
         | 
| 130 | 
            +
                      write_data_element(i)
         | 
| 131 | 
            +
                    end
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
                  # Mark this write session as successful:
         | 
| 134 | 
            +
                  @success = true
         | 
| 135 | 
            +
                  return segments
         | 
| 136 | 
            +
                end
         | 
| 70 137 |  | 
| 71 138 |  | 
| 72 139 | 
             
                # Following methods are private:
         | 
| 73 140 | 
             
                private
         | 
| 74 141 |  | 
| 75 142 |  | 
| 143 | 
            +
                # Add a binary string to (the end of) either file or string.
         | 
| 144 | 
            +
                def add(string)
         | 
| 145 | 
            +
                  if @file
         | 
| 146 | 
            +
                    @stream.write(string)
         | 
| 147 | 
            +
                  else
         | 
| 148 | 
            +
                    @stream.add_last(string)
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
             | 
| 76 153 | 
             
                # Writes the official DICOM header:
         | 
| 77 | 
            -
                def write_header | 
| 154 | 
            +
                def write_header
         | 
| 155 | 
            +
                  # Write the string "DICM" which along with the empty bytes that
         | 
| 156 | 
            +
                  # will be put before it, identifies this as a valid DICOM file:
         | 
| 157 | 
            +
                  identifier = @stream.encode("DICM", "STR")
         | 
| 78 158 | 
             
                  # Fill in 128 empty bytes:
         | 
| 79 | 
            -
                  @ | 
| 80 | 
            -
                   | 
| 81 | 
            -
                  @ | 
| 82 | 
            -
                end | 
| 159 | 
            +
                  filler = @stream.encode("00"*128, "HEX")
         | 
| 160 | 
            +
                  @stream.write(filler)
         | 
| 161 | 
            +
                  @stream.write(identifier)
         | 
| 162 | 
            +
                end
         | 
| 83 163 |  | 
| 84 164 |  | 
| 85 | 
            -
                # Inserts group 0002  | 
| 86 | 
            -
                def write_meta | 
| 87 | 
            -
                  # We will check for the existance of 5 group 0002 elements, and if they are not present, we will insert them:
         | 
| 88 | 
            -
                  pos = Array.new()
         | 
| 89 | 
            -
                  meta = Array.new()
         | 
| 165 | 
            +
                # Inserts group 0002 data elements.
         | 
| 166 | 
            +
                def write_meta
         | 
| 90 167 | 
             
                  # File Meta Information Version:
         | 
| 91 | 
            -
                   | 
| 92 | 
            -
                   | 
| 168 | 
            +
                  tag = @stream.encode_tag("0002,0001")
         | 
| 169 | 
            +
                  @stream.add_last(tag)
         | 
| 170 | 
            +
                  @stream.encode_last("OB", "STR")
         | 
| 171 | 
            +
                  @stream.encode_last("0000", "HEX") # (2 reserved bytes)
         | 
| 172 | 
            +
                  @stream.encode_last(2, "UL")
         | 
| 173 | 
            +
                  @stream.encode_last("0001", "HEX") # (Value)
         | 
| 93 174 | 
             
                  # Transfer Syntax UID:
         | 
| 94 | 
            -
                   | 
| 95 | 
            -
                   | 
| 175 | 
            +
                  tag = @stream.encode_tag("0002,0010")
         | 
| 176 | 
            +
                  @stream.add_last(tag)
         | 
| 177 | 
            +
                  @stream.encode_last("UI", "STR")
         | 
| 178 | 
            +
                  value = @stream.encode_value(@transfer_syntax, "STR")
         | 
| 179 | 
            +
                  @stream.encode_last(value.length, "US")
         | 
| 180 | 
            +
                  @stream.add_last(value)
         | 
| 96 181 | 
             
                  # Implementation Class UID:
         | 
| 97 | 
            -
                   | 
| 98 | 
            -
                   | 
| 182 | 
            +
                  tag = @stream.encode_tag("0002,0012")
         | 
| 183 | 
            +
                  @stream.add_last(tag)
         | 
| 184 | 
            +
                  @stream.encode_last("UI", "STR")
         | 
| 185 | 
            +
                  value = @stream.encode_value(@implementation_uid, "STR")
         | 
| 186 | 
            +
                  @stream.encode_last(value.length, "US")
         | 
| 187 | 
            +
                  @stream.add_last(value)
         | 
| 99 188 | 
             
                  # Implementation Version Name:
         | 
| 100 | 
            -
                   | 
| 101 | 
            -
                   | 
| 102 | 
            -
                   | 
| 103 | 
            -
                   | 
| 104 | 
            -
                   | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
                      # Insert data element in the correct array position:
         | 
| 124 | 
            -
                      if index == -1
         | 
| 125 | 
            -
                        # Insert at the beginning of array:
         | 
| 126 | 
            -
                        @tags = [meta[i][0]] + @tags
         | 
| 127 | 
            -
                        @types = [meta[i][1]] + @types
         | 
| 128 | 
            -
                        @lengths = [meta[i][2]] + @lengths
         | 
| 129 | 
            -
                        @raw = [meta[i][3]] + @raw
         | 
| 130 | 
            -
                      else
         | 
| 131 | 
            -
                        # One or more elements comes before this element:
         | 
| 132 | 
            -
                        @tags = @tags[0..index] + [meta[i][0]] + @tags[(index+1)..(@tags.length-1)]
         | 
| 133 | 
            -
                        @types = @types[0..index] + [meta[i][1]] + @types[(index+1)..(@types.length-1)]
         | 
| 134 | 
            -
                        @lengths = @lengths[0..index] + [meta[i][2]] + @lengths[(index+1)..(@lengths.length-1)]
         | 
| 135 | 
            -
                        @raw = @raw[0..index] + [meta[i][3]] + @raw[(index+1)..(@raw.length-1)]
         | 
| 136 | 
            -
                      end # if index == -1
         | 
| 137 | 
            -
                    end # of if pos[i] != nil
         | 
| 138 | 
            -
                  end # of pos.each_index
         | 
| 139 | 
            -
                  # Calculate the length of group 0002:
         | 
| 140 | 
            -
                  length = 0
         | 
| 141 | 
            -
                  quit = false
         | 
| 142 | 
            -
                  j = 0
         | 
| 143 | 
            -
                  while quit == false do
         | 
| 144 | 
            -
                    if @tags[j][0..3] != "0002"
         | 
| 145 | 
            -
                      quit = true
         | 
| 146 | 
            -
                    else
         | 
| 147 | 
            -
                      # Add to length if group 0002:
         | 
| 148 | 
            -
                      if @tags[j] != "0002,0000"
         | 
| 149 | 
            -
                        if @types[j] == "OB"
         | 
| 150 | 
            -
                          length += 12 + @lengths[j]
         | 
| 151 | 
            -
                        else
         | 
| 152 | 
            -
                          length += 8 + @lengths[j]
         | 
| 153 | 
            -
                        end
         | 
| 154 | 
            -
                      end
         | 
| 155 | 
            -
                      j += 1
         | 
| 156 | 
            -
                    end # of if @tags[j][0..3]..
         | 
| 157 | 
            -
                  end # of while
         | 
| 158 | 
            -
                  # Set group length:
         | 
| 159 | 
            -
                  gl_pos = @tags.index("0002,0000")
         | 
| 160 | 
            -
                  gl_info = ["0002,0000", "UL", 4, [length].pack("I*")]
         | 
| 161 | 
            -
                  # Update group length, but only if there have been some modifications or GL is nonexistant:
         | 
| 162 | 
            -
                  if meta_added == true or gl_pos != nil
         | 
| 163 | 
            -
                    if gl_pos == nil
         | 
| 164 | 
            -
                      # Add group length (to beginning of arrays):
         | 
| 165 | 
            -
                      @tags = [gl_info[0]] + @tags
         | 
| 166 | 
            -
                      @types = [gl_info[1]] + @types
         | 
| 167 | 
            -
                      @lengths = [gl_info[2]] + @lengths
         | 
| 168 | 
            -
                      @raw = [gl_info[3]] + @raw
         | 
| 169 | 
            -
                    else
         | 
| 170 | 
            -
                      # Edit existing group length:
         | 
| 171 | 
            -
                      @tags[gl_pos] = gl_info[0]
         | 
| 172 | 
            -
                      @types[gl_pos] = gl_info[1]
         | 
| 173 | 
            -
                      @lengths[gl_pos] = gl_info[2]
         | 
| 174 | 
            -
                      @raw[gl_pos] = gl_info[3]
         | 
| 175 | 
            -
                    end
         | 
| 176 | 
            -
                  end
         | 
| 177 | 
            -
                end # of method write_meta
         | 
| 189 | 
            +
                  tag = @stream.encode_tag("0002,0013")
         | 
| 190 | 
            +
                  @stream.add_last(tag)
         | 
| 191 | 
            +
                  @stream.encode_last("SH", "STR")
         | 
| 192 | 
            +
                  value = @stream.encode_value(@implementation_name, "STR")
         | 
| 193 | 
            +
                  @stream.encode_last(value.length, "US")
         | 
| 194 | 
            +
                  @stream.add_last(value)
         | 
| 195 | 
            +
                  # Group length:
         | 
| 196 | 
            +
                  # This data element will be put first in the binary string, and built 'backwards'.
         | 
| 197 | 
            +
                  # Value:
         | 
| 198 | 
            +
                  value = @stream.encode(@stream.length, "UL")
         | 
| 199 | 
            +
                  @stream.add_first(value)
         | 
| 200 | 
            +
                  # Length:
         | 
| 201 | 
            +
                  length = @stream.encode(4, "US")
         | 
| 202 | 
            +
                  @stream.add_first(length)
         | 
| 203 | 
            +
                  # Type:
         | 
| 204 | 
            +
                  type = @stream.encode("UL", "STR")
         | 
| 205 | 
            +
                  @stream.add_first(type)
         | 
| 206 | 
            +
                  # Tag:
         | 
| 207 | 
            +
                  tag = @stream.encode_tag("0002,0000")
         | 
| 208 | 
            +
                  @stream.add_first(tag)
         | 
| 209 | 
            +
                  # Write the meta information to file:
         | 
| 210 | 
            +
                  @stream.write(@stream.string)
         | 
| 211 | 
            +
                end
         | 
| 178 212 |  | 
| 179 213 |  | 
| 180 214 | 
             
                # Writes each data element to file:
         | 
| @@ -189,53 +223,41 @@ module DICOM | |
| 189 223 | 
             
                  if @tags[i] == "7FE0,0010"
         | 
| 190 224 | 
             
                    @enc_image = true if @lengths[i].to_i == 0
         | 
| 191 225 | 
             
                  end
         | 
| 192 | 
            -
             | 
| 193 | 
            -
                end # of method write_data_element
         | 
| 226 | 
            +
                end
         | 
| 194 227 |  | 
| 195 228 |  | 
| 196 229 | 
             
                # Writes the tag (first part of the data element):
         | 
| 197 230 | 
             
                def write_tag(i)
         | 
| 198 | 
            -
                  #  | 
| 199 | 
            -
                   | 
| 200 | 
            -
                   | 
| 201 | 
            -
                  # Whether DICOM file is big or little endian, the first 0002 group is always little endian encoded.
         | 
| 202 | 
            -
                  # On a big endian system, I believe the order of the numbers need not be changed,
         | 
| 203 | 
            -
                  # but this has not been tested yet.
         | 
| 204 | 
            -
                  if @sys_endian == false
         | 
| 205 | 
            -
                    # System is little endian:
         | 
| 206 | 
            -
                    # Change the order of the numbers so that it becomes correct when packed as hex:
         | 
| 207 | 
            -
                    tag_corr = tag[2..3] + tag[0..1] + tag[6..7] + tag[4..5]
         | 
| 208 | 
            -
                  end
         | 
| 231 | 
            +
                  # Extract current tag:
         | 
| 232 | 
            +
                  tag = @tags[i]
         | 
| 233 | 
            +
                  # Group 0002 is always little endian, but the rest of the file may be little or big endian.
         | 
| 209 234 | 
             
                  # When we shift from group 0002 to another group we need to update our endian/explicitness variables:
         | 
| 210 235 | 
             
                  if tag[0..3] != "0002" and @switched == false
         | 
| 211 | 
            -
                    switch_syntax | 
| 212 | 
            -
                  end
         | 
| 213 | 
            -
                  # Perhaps we need to rearrange the tag if the file encoding is now big endian:
         | 
| 214 | 
            -
                  if not @endian
         | 
| 215 | 
            -
                    # Need to rearrange the first and second part of each string:
         | 
| 216 | 
            -
                    tag_corr = tag
         | 
| 236 | 
            +
                    switch_syntax
         | 
| 217 237 | 
             
                  end
         | 
| 218 | 
            -
                  # Write to  | 
| 219 | 
            -
                  @ | 
| 220 | 
            -
             | 
| 238 | 
            +
                  # Write to binary string:
         | 
| 239 | 
            +
                  bin_tag = @stream.encode_tag(tag)
         | 
| 240 | 
            +
                  add(bin_tag)
         | 
| 241 | 
            +
                end
         | 
| 221 242 |  | 
| 222 243 |  | 
| 223 | 
            -
                # Writes the type (VR) (if it is to be written) and length value | 
| 244 | 
            +
                # Writes the type (VR) (if it is to be written) and length value
         | 
| 245 | 
            +
                # (these two are the middle part of the data element):
         | 
| 224 246 | 
             
                def write_type_length(i)
         | 
| 225 247 | 
             
                  # First some preprocessing:
         | 
| 226 248 | 
             
                  # Set length value:
         | 
| 227 249 | 
             
                  if @lengths[i] == nil
         | 
| 228 250 | 
             
                    # Set length value to 0:
         | 
| 229 | 
            -
                    length4 =  | 
| 230 | 
            -
                    length2 =  | 
| 251 | 
            +
                    length4 = @stream.encode(0, "UL")
         | 
| 252 | 
            +
                    length2 = @stream.encode(0, "US")
         | 
| 231 253 | 
             
                  elsif @lengths[i] == "UNDEFINED"
         | 
| 232 254 | 
             
                    # Set length to 'ff ff ff ff':
         | 
| 233 | 
            -
                    length4 =  | 
| 255 | 
            +
                    length4 = @stream.encode(4294967295, "UL")
         | 
| 234 256 | 
             
                    # No length2 necessary for this case.
         | 
| 235 257 | 
             
                  else
         | 
| 236 258 | 
             
                    # Pick length value from array:
         | 
| 237 | 
            -
                    length4 =  | 
| 238 | 
            -
                    length2 =  | 
| 259 | 
            +
                    length4 = @stream.encode(@lengths[i], "UL")
         | 
| 260 | 
            +
                    length2 = @stream.encode(@lengths[i], "US")
         | 
| 239 261 | 
             
                  end
         | 
| 240 262 | 
             
                  # Structure will differ, dependent on whether we have explicit or implicit encoding:
         | 
| 241 263 | 
             
                  # *****EXPLICIT*****:
         | 
| @@ -243,7 +265,8 @@ module DICOM | |
| 243 265 | 
             
                    # Step 1: Write VR (if it is to be written)
         | 
| 244 266 | 
             
                    unless @tags[i] == "FFFE,E000" or @tags[i] == "FFFE,E00D" or @tags[i] == "FFFE,E0DD"
         | 
| 245 267 | 
             
                      # Write data element type (VR) (2 bytes - since we are not dealing with an item related element):
         | 
| 246 | 
            -
                      @ | 
| 268 | 
            +
                      vr = @stream.encode(@types[i], "STR")
         | 
| 269 | 
            +
                      add(vr)
         | 
| 247 270 | 
             
                    end
         | 
| 248 271 | 
             
                    # Step 2: Write length
         | 
| 249 272 | 
             
                    # Three possible structures for value length here, dependent on data element type:
         | 
| @@ -252,37 +275,38 @@ module DICOM | |
| 252 275 | 
             
                        if @enc_image
         | 
| 253 276 | 
             
                          # Item under an encapsulated Pixel Data (7FE0,0010):
         | 
| 254 277 | 
             
                          # 4 bytes:
         | 
| 255 | 
            -
                           | 
| 278 | 
            +
                          add(length4)
         | 
| 256 279 | 
             
                        else
         | 
| 257 280 | 
             
                          # 6 bytes total:
         | 
| 258 281 | 
             
                          # Two empty first:
         | 
| 259 | 
            -
                          @ | 
| 282 | 
            +
                          empty = @stream.encode("00"*2, "HEX")
         | 
| 283 | 
            +
                          add(empty)
         | 
| 260 284 | 
             
                          # Value length (4 bytes):
         | 
| 261 | 
            -
                           | 
| 285 | 
            +
                          add(length4)
         | 
| 262 286 | 
             
                        end
         | 
| 263 287 | 
             
                      when "()"
         | 
| 264 288 | 
             
                        # 4 bytes:
         | 
| 265 289 | 
             
                        # For tags "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
         | 
| 266 | 
            -
                         | 
| 290 | 
            +
                        add(length4)
         | 
| 267 291 | 
             
                      else
         | 
| 268 292 | 
             
                        # 2 bytes:
         | 
| 269 293 | 
             
                        # For all the other data element types, value length is 2 bytes:
         | 
| 270 | 
            -
                         | 
| 294 | 
            +
                        add(length2)
         | 
| 271 295 | 
             
                    end # of case type
         | 
| 272 296 | 
             
                  else
         | 
| 273 297 | 
             
                    # *****IMPLICIT*****:
         | 
| 274 298 | 
             
                    # No VR written.
         | 
| 275 299 | 
             
                    # Writing value length (4 bytes):
         | 
| 276 | 
            -
                     | 
| 277 | 
            -
                  end | 
| 278 | 
            -
                end # of  | 
| 300 | 
            +
                    add(length4)
         | 
| 301 | 
            +
                  end
         | 
| 302 | 
            +
                end # of write_type_length
         | 
| 279 303 |  | 
| 280 304 |  | 
| 281 305 | 
             
                # Writes the value (last part of the data element):
         | 
| 282 306 | 
             
                def write_value(i)
         | 
| 283 | 
            -
                  # This is pretty straightforward, just dump the binary data to the file:
         | 
| 284 | 
            -
                   | 
| 285 | 
            -
                end | 
| 307 | 
            +
                  # This is pretty straightforward, just dump the binary data to the file/string:
         | 
| 308 | 
            +
                  add(@raw[i])
         | 
| 309 | 
            +
                end
         | 
| 286 310 |  | 
| 287 311 |  | 
| 288 312 | 
             
                # Tests if the file is writable and opens it.
         | 
| @@ -295,7 +319,7 @@ module DICOM | |
| 295 319 | 
             
                      @file = File.new(file, "wb")
         | 
| 296 320 | 
             
                    else
         | 
| 297 321 | 
             
                      # Existing file is not writable:
         | 
| 298 | 
            -
                      @msg  | 
| 322 | 
            +
                      @msg << "Error! The program does not have permission or resources to create the file you specified. Returning. (#{file})"
         | 
| 299 323 | 
             
                    end
         | 
| 300 324 | 
             
                  else
         | 
| 301 325 | 
             
                    # File does not exist.
         | 
| @@ -313,100 +337,59 @@ module DICOM | |
| 313 337 | 
             
                          FileUtils.mkdir_p path
         | 
| 314 338 | 
             
                        end
         | 
| 315 339 | 
             
                      end
         | 
| 316 | 
            -
                    end | 
| 340 | 
            +
                    end
         | 
| 317 341 | 
             
                    # The path to this non-existing file should now be prepared, and we will thus create the file:
         | 
| 318 342 | 
             
                    @file = File.new(file, "wb")
         | 
| 319 | 
            -
                  end | 
| 320 | 
            -
                end | 
| 343 | 
            +
                  end
         | 
| 344 | 
            +
                end
         | 
| 321 345 |  | 
| 322 346 |  | 
| 323 347 | 
             
                # Changes encoding variables as the file writing proceeds past the initial 0002 group of the DICOM file.
         | 
| 324 | 
            -
                def switch_syntax | 
| 348 | 
            +
                def switch_syntax
         | 
| 325 349 | 
             
                  # The information from the Transfer syntax element (if present), needs to be processed:
         | 
| 326 | 
            -
                  process_transfer_syntax()
         | 
| 350 | 
            +
                  result = @lib.process_transfer_syntax(@transfer_syntax.rstrip)
         | 
| 351 | 
            +
                  # Result is a 3-element array: [Validity of ts, explicitness, endianness]
         | 
| 352 | 
            +
                  unless result[0]
         | 
| 353 | 
            +
                    @msg << "Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."
         | 
| 354 | 
            +
                  end
         | 
| 355 | 
            +
                  @rest_explicit = result[1]
         | 
| 356 | 
            +
                  @rest_endian = result[2]
         | 
| 327 357 | 
             
                  # We only plan to run this method once:
         | 
| 328 358 | 
             
                  @switched = true
         | 
| 329 | 
            -
                  # Update  | 
| 359 | 
            +
                  # Update explicitness and endianness (pack/unpack variables):
         | 
| 330 360 | 
             
                  @file_endian = @rest_endian
         | 
| 361 | 
            +
                  @stream.set_endian(@rest_endian)
         | 
| 331 362 | 
             
                  @explicit = @rest_explicit
         | 
| 363 | 
            +
                  @stream.explicit = @rest_explicit
         | 
| 332 364 | 
             
                  if @sys_endian == @file_endian
         | 
| 333 365 | 
             
                    @endian = true
         | 
| 334 366 | 
             
                  else
         | 
| 335 367 | 
             
                    @endian = false
         | 
| 336 368 | 
             
                  end
         | 
| 337 | 
            -
                  set_pack_strings()
         | 
| 338 369 | 
             
                end
         | 
| 339 370 |  | 
| 340 371 |  | 
| 341 | 
            -
                #  | 
| 342 | 
            -
                def  | 
| 343 | 
            -
                   | 
| 344 | 
            -
                   | 
| 345 | 
            -
             | 
| 346 | 
            -
                     | 
| 347 | 
            -
                    if  | 
| 348 | 
            -
                       | 
| 372 | 
            +
                # Find the position of the first tag which is not a group "0002" tag:
         | 
| 373 | 
            +
                def first_non_meta
         | 
| 374 | 
            +
                  i = 0
         | 
| 375 | 
            +
                  go = true
         | 
| 376 | 
            +
                  while go == true and i < @tags.length do
         | 
| 377 | 
            +
                    tag = @tags[i]
         | 
| 378 | 
            +
                    if tag[0..3] == "0002"
         | 
| 379 | 
            +
                      i += 1
         | 
| 380 | 
            +
                    else
         | 
| 381 | 
            +
                      go = false
         | 
| 349 382 | 
             
                    end
         | 
| 350 | 
            -
                    case ts_value
         | 
| 351 | 
            -
                      # Some variations with uncompressed pixel data:
         | 
| 352 | 
            -
                      when "1.2.840.10008.1.2"
         | 
| 353 | 
            -
                        # Implicit VR, Little Endian
         | 
| 354 | 
            -
                        @rest_explicit = false
         | 
| 355 | 
            -
                        @rest_endian = false
         | 
| 356 | 
            -
                      when "1.2.840.10008.1.2.1"
         | 
| 357 | 
            -
                        # Explicit VR, Little Endian
         | 
| 358 | 
            -
                        @rest_explicit = true
         | 
| 359 | 
            -
                        @rest_endian = false
         | 
| 360 | 
            -
                      when "1.2.840.10008.1.2.1.99"
         | 
| 361 | 
            -
                        # Deflated Explicit VR, Little Endian
         | 
| 362 | 
            -
                        @msg += ["Warning: Transfer syntax 'Deflated Explicit VR, Little Endian' is untested. Unknown if this is handled correctly!"]
         | 
| 363 | 
            -
                        @rest_explicit = true
         | 
| 364 | 
            -
                        @rest_endian = false
         | 
| 365 | 
            -
                      when "1.2.840.10008.1.2.2"
         | 
| 366 | 
            -
                        # Explicit VR, Big Endian
         | 
| 367 | 
            -
                        @rest_explicit = true
         | 
| 368 | 
            -
                        @rest_endian = true
         | 
| 369 | 
            -
                      else
         | 
| 370 | 
            -
                        # For everything else, assume compressed pixel data, with Explicit VR, Little Endian:
         | 
| 371 | 
            -
                        @rest_explicit = true
         | 
| 372 | 
            -
                        @rest_endian = false
         | 
| 373 | 
            -
                    end # of case ts_value
         | 
| 374 | 
            -
                  end # of if ts_pos != nil
         | 
| 375 | 
            -
                end # of method process_syntax
         | 
| 376 | 
            -
             | 
| 377 | 
            -
             | 
| 378 | 
            -
                # Sets the pack format strings that will be used for numbers depending on endianness of file/system.
         | 
| 379 | 
            -
                def set_pack_strings
         | 
| 380 | 
            -
                  if @endian
         | 
| 381 | 
            -
                    # System endian equals file endian:
         | 
| 382 | 
            -
                    # Native byte order.
         | 
| 383 | 
            -
                    @by = "C*" # Byte (1 byte)
         | 
| 384 | 
            -
                    @us = "S*" # Unsigned short (2 bytes)
         | 
| 385 | 
            -
                    @ss = "s*" # Signed short (2 bytes)
         | 
| 386 | 
            -
                    @ul = "I*" # Unsigned long (4 bytes)
         | 
| 387 | 
            -
                    @sl = "l*" # Signed long (4 bytes)
         | 
| 388 | 
            -
                    @fs = "e*" # Floating point single (4 bytes)
         | 
| 389 | 
            -
                    @fd = "E*" # Floating point double ( 8 bytes)
         | 
| 390 | 
            -
                  else
         | 
| 391 | 
            -
                    # System endian not equal to file endian:
         | 
| 392 | 
            -
                    # Network byte order.
         | 
| 393 | 
            -
                    @by = "C*"
         | 
| 394 | 
            -
                    @us = "n*"
         | 
| 395 | 
            -
                    @ss = "n*" # Not correct (gives US)
         | 
| 396 | 
            -
                    @ul = "N*"
         | 
| 397 | 
            -
                    @sl = "N*" # Not correct (gives UL)
         | 
| 398 | 
            -
                    @fs = "g*"
         | 
| 399 | 
            -
                    @fd = "G*"
         | 
| 400 383 | 
             
                  end
         | 
| 384 | 
            +
                  return i
         | 
| 401 385 | 
             
                end
         | 
| 402 386 |  | 
| 403 387 |  | 
| 404 388 | 
             
                # Initializes the variables used when executing this program.
         | 
| 405 | 
            -
                def init_variables | 
| 389 | 
            +
                def init_variables
         | 
| 406 390 | 
             
                  # Variables that are accesible from outside:
         | 
| 407 391 | 
             
                  # Until a DICOM write has completed successfully the status is 'unsuccessful':
         | 
| 408 392 | 
             
                  @success = false
         | 
| 409 | 
            -
             | 
| 410 393 | 
             
                  # Variables used internally:
         | 
| 411 394 | 
             
                  # Default explicitness of start of DICOM file:
         | 
| 412 395 | 
             
                  @explicit = true
         | 
| @@ -420,11 +403,12 @@ module DICOM | |
| 420 403 | 
             
                  else
         | 
| 421 404 | 
             
                    @endian = false
         | 
| 422 405 | 
             
                  end
         | 
| 423 | 
            -
                  # Set which format strings to use when unpacking numbers:
         | 
| 424 | 
            -
                  set_pack_strings
         | 
| 425 406 | 
             
                  # Items contained under the Pixel Data element needs some special attention to write correctly:
         | 
| 426 407 | 
             
                  @enc_image = false
         | 
| 427 | 
            -
             | 
| 408 | 
            +
                  # Version information:
         | 
| 409 | 
            +
                  @implementation_uid = "1.2.826.0.1.3680043.8.641"
         | 
| 410 | 
            +
                  @implementation_name = "RUBY_DICOM_0.6"
         | 
| 411 | 
            +
                end
         | 
| 428 412 |  | 
| 429 413 | 
             
              end # of class
         | 
| 430 414 | 
             
            end # of module
         |