dicom 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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, opts={})
20
+ def initialize(file_name=nil, options={})
20
21
  # Process option values, setting defaults for the ones that are not specified:
21
- @lib = opts[:lib] || DLibrary.new
22
- @sys_endian = opts[:sys_endian] || false
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 # of method initialize
39
+ end
38
40
 
39
41
 
40
42
  # Writes the DICOM information to file.
41
- def write()
42
- if @tags.size > 0
43
- # Check if we are able to create given file:
44
- open_file(@file_name)
45
- # Read the initial header of the file:
46
- if @file != nil
47
- # Initiate necessary variables:
48
- init_variables()
49
- # Write header:
50
- write_header()
51
- # Write meta information (if it is not present in the DICOM object):
52
- write_meta()
53
- # Write data elements:
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
- # We are finished writing the data elements, and as such, can close the file:
58
- @file.close()
59
- # Mark this write session as successful:
60
- @success = true
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
- # File is not writable, so we return:
63
- # (Error msg already registered in open_file method)
64
- return
65
- end # of if @file != nil
66
- else
67
- @msg += ["Error. No data elements to write."]
68
- end # of if @tags.size > 0
69
- end # of method write
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
- @file.write(["00"*128].pack('H*'))
80
- # Write the string "DICM" which is central to DICOM standards compliance:
81
- @file.write("DICM")
82
- end # of write_header
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 if it is missing, to ensure DICOM compliance.
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
- pos += [@tags.index("0002,0001")]
92
- meta += [["0002,0001", "OB", 2, ["0100"].pack("H*")]]
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
- pos += [@tags.index("0002,0010")]
95
- meta += [["0002,0010", "UI", 18, ["1.2.840.10008.1.2"].pack("a*")+["00"].pack("H*")]] # Implicit, little endian
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
- pos += [@tags.index("0002,0012")]
98
- meta += [["0002,0012", "UI", 26, ["1.2.826.0.1.3680043.8.641"].pack("a*")+["00"].pack("H*")]] # Ruby DICOM UID
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
- pos += [@tags.index("0002,0013")]
101
- meta += [["0002,0013", "SH", 10, ["RUBY_DICOM"].pack("a*")]]
102
- # Insert meta information:
103
- meta_added = false
104
- pos.each_index do |i|
105
- # Only insert element if it does not already exist (corresponding pos element shows no match):
106
- if pos[i] == nil
107
- meta_added = true
108
- # Find where to insert this data element.
109
- index = -1
110
- tag = meta[i][0]
111
- quit = false
112
- while quit != true do
113
- if tag < @tags[index+1]
114
- quit = true
115
- elsif @tags[index+1][0..3] != "0002"
116
- # Abort to avoid needlessly going through the whole array.
117
- quit = true
118
- else
119
- # Else increase index in anticipation of a 'hit'.
120
- index += 1
121
- end
122
- end # of while
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
- # Should have some kind of test that the last data was written succesfully?
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
- # Tag is originally of the form "0002,0010".
199
- # We need to reformat to get rid of the comma:
200
- tag = @tags[i][0..3] + @tags[i][5..8]
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 file:
219
- @file.write([tag_corr].pack('H*'))
220
- end # of method write_tag
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 (these two are the middle part of the data element):
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 = [0].pack(@ul)
230
- length2 = [0].pack(@us)
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 = [4294967295].pack(@ul)
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 = [@lengths[i]].pack(@ul)
238
- length2 = [@lengths[i]].pack(@us)
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
- @file.write([@types[i]].pack('a*'))
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
- @file.write(length4)
278
+ add(length4)
256
279
  else
257
280
  # 6 bytes total:
258
281
  # Two empty first:
259
- @file.write(["00"*2].pack('H*'))
282
+ empty = @stream.encode("00"*2, "HEX")
283
+ add(empty)
260
284
  # Value length (4 bytes):
261
- @file.write(length4)
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
- @file.write(length4)
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
- @file.write(length2)
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
- @file.write(length4)
277
- end # of if @explicit == true
278
- end # of method write_type_length
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
- @file.write(@raw[i])
285
- end # of method write_value
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 += ["Error! The program does not have permission or resources to create the file you specified. Returning. (#{file})"]
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 # of if arr != nil
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 # of if File.exist?(file)
320
- end # of method open_file
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 endian, explicitness and unpack variables:
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
- # Checks the Transfer Syntax UID element and updates class variables to prepare for correct writing of DICOM file.
342
- def process_transfer_syntax()
343
- ts_pos = @tags.index("0002,0010")
344
- if ts_pos != nil
345
- ts_value = @raw[ts_pos].unpack('a*').join.rstrip
346
- valid = @lib.check_ts_validity(ts_value)
347
- if not valid
348
- @msg+=["Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."]
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
- end # of method init_variables
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