dicom 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dicom/DServer.rb DELETED
@@ -1,304 +0,0 @@
1
- # Copyright 2009-2010 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
-
10
- # Run the server and take a block for initializing.
11
- def self.run(port=104, path='./received/', &block)
12
- server = DServer.new(port)
13
- server.instance_eval(&block)
14
- server.start_scp(path)
15
- end
16
-
17
- # Accessible attributes:
18
- attr_accessor :host_ae, :max_package_size, :port, :timeout, :verbose, :file_handler
19
- attr_reader :errors, :notices
20
-
21
- # Initialize the instance with a host adress and a port number.
22
- def initialize(port=104, options={})
23
- require 'socket'
24
- # Required parameters:
25
- @port = port
26
- # Optional parameters (and default values):
27
- @file_handler = options[:file_handler] || FileHandler
28
- @host_ae = options[:host_ae] || "RUBY_DICOM"
29
- @max_package_size = options[:max_package_size] || 32768 # 16384
30
- @timeout = options[:timeout] || 10 # seconds
31
- @min_length = 12 # minimum number of bytes to expect in an incoming transmission
32
- @verbose = options[:verbose]
33
- @verbose = true if @verbose == nil # Default verbosity is 'on'.
34
- # Other instance variables:
35
- @errors = Array.new # errors and warnings are put in this array
36
- @notices = Array.new # information on successful transmissions are put in this array
37
- # Variables used for monitoring state of transmission:
38
- @connection = nil # TCP connection status
39
- @association = nil # DICOM Association status
40
- @request_approved = nil # Status of our DICOM request
41
- @release = nil # Status of received, valid release response
42
- set_valid_abstract_syntaxes
43
- end
44
-
45
-
46
- # Add a specified abstract syntax to the list of syntaxes that the server instance will accept.
47
- def add_abstract_syntax(value)
48
- if value.is_a?(String)
49
- @valid_abstract_syntaxes << value
50
- @valid_abstract_syntaxes.sort!
51
- else
52
- add_error("Error: The specified abstract syntax is not a string!")
53
- end
54
- end
55
-
56
-
57
- # Print the list of valid abstract syntaxes to the screen.
58
- def print_syntaxes
59
- puts "Abstract syntaxes accepted by this SCP:"
60
- @valid_abstract_syntaxes.each do |syntax|
61
- puts syntax
62
- end
63
- end
64
-
65
-
66
- # Remove a specific abstract syntax from the list of syntaxes that the server instance will accept.
67
- def remove_abstract_syntax(value)
68
- if value.is_a?(String)
69
- # Remove it:
70
- @valid_abstract_syntaxes.delete(value)
71
- else
72
- add_error("Error: The specified abstract syntax is not a string!")
73
- end
74
- end
75
-
76
-
77
- # Completely clear the list of syntaxes that the server instance will accept.
78
- def remove_all_abstract_syntaxes
79
- @valid_abstract_syntaxes = Array.new
80
- end
81
-
82
- # Start a Service Class Provider (SCP).
83
- # This service will receive and store DICOM files in a specified folder.
84
- def start_scp(path='./received/')
85
- add_notice("Starting SCP server...")
86
- add_notice("*********************************")
87
- # Initiate server:
88
- @scp = TCPServer.new(@port)
89
- # Use a loop to listen for incoming messages:
90
- loop do
91
- Thread.start(@scp.accept) do |session|
92
- # Initialize the network package handler for this session:
93
- link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose, :file_handler => @file_handler)
94
- add_notice("Connection established (name: #{session.peeraddr[2]}, ip: #{session.peeraddr[3]})")
95
- # Receive an incoming message:
96
- #segments = link.receive_single_transmission(session)
97
- segments = link.receive_multiple_transmissions(session)
98
- info = segments.first
99
- # Interpret the received message:
100
- if info[:valid]
101
- association_error = check_association_request(info)
102
- unless association_error
103
- syntax_result = check_syntax_requests(link, info)
104
- link.handle_association_accept(session, info, syntax_result)
105
- if syntax_result == "00" # Normal (no error)
106
- add_notice("An incoming association request and its abstract syntax has been accepted.")
107
- if info[:abstract_syntax] == "1.2.840.10008.1.1"
108
- # Verification SOP Class (used for testing connections):
109
- link.handle_release(session)
110
- else
111
- # Process the incoming data:
112
- success, message = link.handle_incoming_data(session, path)
113
- if success
114
- add_notice(message)
115
- # Send a receipt for received data:
116
- link.handle_response(session)
117
- else
118
- # Something has gone wrong:
119
- add_error(message)
120
- end
121
- # Release the connection:
122
- link.handle_release(session)
123
- end
124
- else
125
- # Abstract syntax in the incoming request was not accepted:
126
- add_notice("An incoming association request was accepted, but it's abstract syntax was rejected. (#{abstract_syntax})")
127
- # Since the requested abstract syntax was not accepted, the association must be released.
128
- link.handle_release(session)
129
- end
130
- else
131
- # The incoming association was not formally correct.
132
- link.handle_rejection(session)
133
- end
134
- else
135
- # The incoming message was not recognised as a valid DICOM message. Abort:
136
- link.handle_abort(session)
137
- end
138
- # Terminate the connection:
139
- session.close unless session.closed?
140
- add_notice("Connection closed.")
141
- add_notice("*********************************")
142
- end
143
- end
144
- end
145
-
146
-
147
- # Following methods are private:
148
- private
149
-
150
-
151
- # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
152
- def add_error(error)
153
- if @verbose
154
- puts error
155
- end
156
- @errors << error
157
- end
158
-
159
-
160
- # Adds a notice (information regarding progress or successful communications) to the instance array,
161
- # and if verbosity is set for these kinds of messages, prints it to the screen as well.
162
- def add_notice(notice)
163
- if @verbose
164
- puts notice
165
- end
166
- @notices << notice
167
- end
168
-
169
-
170
- # Check if the association request is formally correct.
171
- # Things that can be checked here, are:
172
- # Application context name, calling AE title, called AE title
173
- # Error codes are given in the official dicom document, part 08_08, page 41
174
- def check_association_request(info)
175
- error = nil
176
- # For the moment there is no control on AE titles.
177
- # Check that Application context name is as expected:
178
- if info[:application_context] != "1.2.840.10008.3.1.1.1"
179
- error = "02" # application context name not supported
180
- add_error("Warning: Application context not recognised in the incoming association request. (#{info[:application_context]})")
181
- end
182
- return error
183
- end
184
-
185
-
186
- # Check if the requested abstract syntax & transfer syntax are supported:
187
- # Error codes are given in the official dicom document, part 08_08, page 39
188
- def check_syntax_requests(link, info)
189
- result = "00" # (no error)
190
- # We will accept any transfer syntax (as long as it is recognized in the library):
191
- # (Weakness: Only checking the first occuring transfer syntax for now)
192
- transfer_syntax = link.extract_transfer_syntax(info)
193
- unless LIBRARY.check_ts_validity(transfer_syntax)
194
- result = "04" # transfer syntax not supported
195
- add_error("Warning: Unsupported transfer syntax received in incoming association request. (#{transfer_syntax})")
196
- end
197
- # Check that abstract syntax is among the ones that have been set as valid for this server instance:
198
- abstract_syntax = link.extract_abstract_syntax(info)
199
- unless @valid_abstract_syntaxes.include?(abstract_syntax)
200
- result = "03" # abstract syntax not supported
201
- end
202
- return result
203
- end
204
-
205
-
206
- # Set the default valid abstract syntaxes for our SCP.
207
- def set_valid_abstract_syntaxes
208
- @valid_abstract_syntaxes = [
209
- "1.2.840.10008.1.1", # "Verification SOP Class"
210
- "1.2.840.10008.5.1.4.1.1.1", # "Computed Radiography Image Storage"
211
- "1.2.840.10008.5.1.4.1.1.1.1", # "Digital X-Ray Image Storage - For Presentation"
212
- "1.2.840.10008.5.1.4.1.1.1.1.1", # "Digital X-Ray Image Storage - For Processing"
213
- "1.2.840.10008.5.1.4.1.1.1.2", # "Digital Mammography X-Ray Image Storage - For Presentation"
214
- "1.2.840.10008.5.1.4.1.1.1.2.1", # "Digital Mammography X-Ray Image Storage - For Processing"
215
- "1.2.840.10008.5.1.4.1.1.1.3", # "Digital Intra-oral X-Ray Image Storage - For Presentation"
216
- "1.2.840.10008.5.1.4.1.1.1.3.1", # "Digital Intra-oral X-Ray Image Storage - For Processing"
217
- "1.2.840.10008.5.1.4.1.1.2", # "CT Image Storage"
218
- "1.2.840.10008.5.1.4.1.1.2.1", # "Enhanced CT Image Storage"
219
- "1.2.840.10008.5.1.4.1.1.3", # "Ultrasound Multi-frame Image Storage" # RET
220
- "1.2.840.10008.5.1.4.1.1.3.1", # "Ultrasound Multi-frame Image Storage"
221
- "1.2.840.10008.5.1.4.1.1.4", # "MR Image Storage"
222
- "1.2.840.10008.5.1.4.1.1.4.1", # "Enhanced MR Image Storage"
223
- "1.2.840.10008.5.1.4.1.1.4.2", # "MR Spectroscopy Storage"
224
- "1.2.840.10008.5.1.4.1.1.5", # "Nuclear Medicine Image Storage"
225
- "1.2.840.10008.5.1.4.1.1.6", # "Ultrasound Image Storage"
226
- "1.2.840.10008.5.1.4.1.1.6.1", # "Ultrasound Image Storage"
227
- "1.2.840.10008.5.1.4.1.1.7", # "Secondary Capture Image Storage"
228
- "1.2.840.10008.5.1.4.1.1.7.1", # "Multi-frame Single Bit Secondary Capture Image Storage"
229
- "1.2.840.10008.5.1.4.1.1.7.2", # "Multi-frame Grayscale Byte Secondary Capture Image Storage"
230
- "1.2.840.10008.5.1.4.1.1.7.3", # "Multi-frame Grayscale Word Secondary Capture Image Storage"
231
- "1.2.840.10008.5.1.4.1.1.7.4", # "Multi-frame True Color Secondary Capture Image Storage"
232
- "1.2.840.10008.5.1.4.1.1.8", # "Standalone Overlay Storage" # RET
233
- "1.2.840.10008.5.1.4.1.1.9", # "Standalone Curve Storage" # RET
234
- "1.2.840.10008.5.1.4.1.1.9.1", # "Waveform Storage - Trial" # RET
235
- "1.2.840.10008.5.1.4.1.1.9.1.1", # "12-lead ECG Waveform Storage"
236
- "1.2.840.10008.5.1.4.1.1.9.1.2", # "General ECG Waveform Storage"
237
- "1.2.840.10008.5.1.4.1.1.9.1.3", # "Ambulatory ECG Waveform Storage"
238
- "1.2.840.10008.5.1.4.1.1.9.2.1", # "Hemodynamic Waveform Storage"
239
- "1.2.840.10008.5.1.4.1.1.9.3.1", # "Cardiac Electrophysiology Waveform Storage"
240
- "1.2.840.10008.5.1.4.1.1.9.4.1", # "Basic Voice Audio Waveform Storage"
241
- "1.2.840.10008.5.1.4.1.1.10", # "Standalone Modality LUT Storage" # RET
242
- "1.2.840.10008.5.1.4.1.1.11", # "Standalone VOI LUT Storage" # RET
243
- "1.2.840.10008.5.1.4.1.1.11.1", # "Grayscale Softcopy Presentation State Storage SOP Class"
244
- "1.2.840.10008.5.1.4.1.1.11.2", # "Color Softcopy Presentation State Storage SOP Class"
245
- "1.2.840.10008.5.1.4.1.1.11.3", # "Pseudo-Color Softcopy Presentation State Storage SOP Class"
246
- "1.2.840.10008.5.1.4.1.1.11.4", # "Blending Softcopy Presentation State Storage SOP Class"
247
- "1.2.840.10008.5.1.4.1.1.12.1", # "X-Ray Angiographic Image Storage"
248
- "1.2.840.10008.5.1.4.1.1.12.1.1", # "Enhanced XA Image Storage"
249
- "1.2.840.10008.5.1.4.1.1.12.2", # "X-Ray Radiofluoroscopic Image Storage"
250
- "1.2.840.10008.5.1.4.1.1.12.2.1", # "Enhanced XRF Image Storage"
251
- "1.2.840.10008.5.1.4.1.1.13.1.1", # "X-Ray 3D Angiographic Image Storage"
252
- "1.2.840.10008.5.1.4.1.1.13.1.2", # "X-Ray 3D Craniofacial Image Storage"
253
- "1.2.840.10008.5.1.4.1.1.12.3", # "X-Ray Angiographic Bi-Plane Image Storage" # RET
254
- "1.2.840.10008.5.1.4.1.1.20", # "Nuclear Medicine Image Storage"
255
- "1.2.840.10008.5.1.4.1.1.66", # "Raw Data Storage"
256
- "1.2.840.10008.5.1.4.1.1.66.1", # "Spatial Registration Storage"
257
- "1.2.840.10008.5.1.4.1.1.66.2", # "Spatial Fiducials Storage"
258
- "1.2.840.10008.5.1.4.1.1.66.3", # "Deformable Spatial Registration Storage"
259
- "1.2.840.10008.5.1.4.1.1.66.4", # "Segmentation Storage"
260
- "1.2.840.10008.5.1.4.1.1.67", # "Real World Value Mapping Storage"
261
- "1.2.840.10008.5.1.4.1.1.77.1", # "VL Image Storage - Trial" # RET
262
- "1.2.840.10008.5.1.4.1.1.77.2", # "VL Multi-frame Image Storage - Trial" # RET
263
- "1.2.840.10008.5.1.4.1.1.77.1.1", # "VL Endoscopic Image Storage"
264
- "1.2.840.10008.5.1.4.1.1.77.1.1.1", # "Video Endoscopic Image Storage"
265
- "1.2.840.10008.5.1.4.1.1.77.1.2", # "VL Microscopic Image Storage"
266
- "1.2.840.10008.5.1.4.1.1.77.1.2.1", # "Video Microscopic Image Storage"
267
- "1.2.840.10008.5.1.4.1.1.77.1.3", # "VL Slide-Coordinates Microscopic Image Storage"
268
- "1.2.840.10008.5.1.4.1.1.77.1.4", # "VL Photographic Image Storage"
269
- "1.2.840.10008.5.1.4.1.1.77.1.4.1", # "Video Photographic Image Storage"
270
- "1.2.840.10008.5.1.4.1.1.77.1.5.1", # "Ophthalmic Photography 8 Bit Image Storage"
271
- "1.2.840.10008.5.1.4.1.1.77.1.5.2", # "Ophthalmic Photography 16 Bit Image Storage"
272
- "1.2.840.10008.5.1.4.1.1.77.1.5.3", # "Stereometric Relationship Storage"
273
- "1.2.840.10008.5.1.4.1.1.77.1.5.4", # "Ophthalmic Tomography Image Storage"
274
- "1.2.840.10008.5.1.4.1.1.88.1", # "Text SR Storage - Trial" # RET
275
- "1.2.840.10008.5.1.4.1.1.88.2", # "Audio SR Storage - Trial" # RET
276
- "1.2.840.10008.5.1.4.1.1.88.3", # "Detail SR Storage - Trial" # RET
277
- "1.2.840.10008.5.1.4.1.1.88.4", # "Comprehensive SR Storage - Trial" # RET
278
- "1.2.840.10008.5.1.4.1.1.88.11", # "Basic Text SR Storage"
279
- "1.2.840.10008.5.1.4.1.1.88.22", # "Enhanced SR Storage"
280
- "1.2.840.10008.5.1.4.1.1.88.33", # "Comprehensive SR Storage"
281
- "1.2.840.10008.5.1.4.1.1.88.40", # "Procedure Log Storage"
282
- "1.2.840.10008.5.1.4.1.1.88.50", # "Mammography CAD SR Storage"
283
- "1.2.840.10008.5.1.4.1.1.88.59", # "Key Object Selection Document Storage"
284
- "1.2.840.10008.5.1.4.1.1.88.65", # "Chest CAD SR Storage"
285
- "1.2.840.10008.5.1.4.1.1.88.67", # "X-Ray Radiation Dose SR Storage"
286
- "1.2.840.10008.5.1.4.1.1.104.1", # "Encapsulated PDF Storage"
287
- "1.2.840.10008.5.1.4.1.1.104.2", # "Encapsulated CDA Storage"
288
- "1.2.840.10008.5.1.4.1.1.128", # "Positron Emission Tomography Image Storage"
289
- "1.2.840.10008.5.1.4.1.1.129", # "Standalone PET Curve Storage" # RET
290
- "1.2.840.10008.5.1.4.1.1.481.1", # "RT Image Storage"
291
- "1.2.840.10008.5.1.4.1.1.481.2", # "RT Dose Storage"
292
- "1.2.840.10008.5.1.4.1.1.481.3", # "RT Structure Set Storage"
293
- "1.2.840.10008.5.1.4.1.1.481.4", # "RT Beams Treatment Record Storage"
294
- "1.2.840.10008.5.1.4.1.1.481.5", # "RT Plan Storage"
295
- "1.2.840.10008.5.1.4.1.1.481.6", # "RT Brachy Treatment Record Storage"
296
- "1.2.840.10008.5.1.4.1.1.481.7", # "RT Treatment Summary Record Storage"
297
- "1.2.840.10008.5.1.4.1.1.481.8", # "RT Ion Plan Storage"
298
- "1.2.840.10008.5.1.4.1.1.481.9" # "RT Ion Beams Treatment Record Storage"
299
- ]
300
- end
301
-
302
-
303
- end # of class
304
- end # of module
data/lib/dicom/DWrite.rb DELETED
@@ -1,410 +0,0 @@
1
- # Copyright 2008-2010 Christoffer Lervag
2
-
3
- # Some notes about this DICOM file writing class:
4
- # In its current state, this class will always try to write the file such that it is compliant to the
5
- # official standard (DICOM 3 Part 10), containing header and meta information (group 0002).
6
- # If this is unwanted behaviour, it is easy to modify the source code here to avoid this.
7
- #
8
- # It is important to note, that while the goal is to be fully DICOM compliant, no guarantees are given
9
- # that this is actually achieved. You are encouraged to thouroughly test your files for compatibility after creation.
10
- # Please contact the author if you discover any issues with file creation.
11
-
12
- module DICOM
13
-
14
- # Class for writing the data from a DObject to a valid DICOM file.
15
- class DWrite
16
-
17
- attr_writer :tags, :vr, :lengths, :bin, :rest_endian, :rest_explicit
18
- attr_reader :success, :msg
19
-
20
- # Initialize the DWrite instance.
21
- def initialize(file_name=nil, options={})
22
- # Process option values, setting defaults for the ones that are not specified:
23
- @sys_endian = options[:sys_endian] || false
24
- @file_name = file_name
25
- @transfer_syntax = options[:transfer_syntax] || "1.2.840.10008.1.2" # Implicit, little endian
26
-
27
- # Create arrays used for storing data element information:
28
- @tags = Array.new
29
- @vr = Array.new
30
- @lengths = Array.new
31
- @bin = Array.new
32
- # Array for storing error/warning messages:
33
- @msg = Array.new
34
- # Default values that may be overwritten by the user:
35
- # Explicitness of the remaining groups after the initial 0002 group:
36
- @rest_explicit = false
37
- # Endianness of the remaining groups after the first group:
38
- @rest_endian = false
39
- end
40
-
41
-
42
- # Writes the DICOM information to file.
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
68
- @tags.each_index do |i|
69
- write_data_element(i)
70
- end
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 & vr/length:
102
- write_tag(i)
103
- write_vr_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 = @bin[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 = @bin[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)
128
- else
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
137
-
138
-
139
- # Following methods are private:
140
- private
141
-
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
-
153
- # Writes the official DICOM 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")
158
- # Fill in 128 empty bytes:
159
- filler = @stream.encode("00"*128, "HEX")
160
- @stream.write(filler)
161
- @stream.write(identifier)
162
- end
163
-
164
-
165
- # Inserts group 0002 data elements.
166
- def write_meta
167
- # File Meta Information Version:
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)
174
- # Transfer Syntax UID:
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)
181
- # Implementation Class UID:
182
- tag = @stream.encode_tag("0002,0012")
183
- @stream.add_last(tag)
184
- @stream.encode_last("UI", "STR")
185
- value = @stream.encode_value(UID, "STR")
186
- @stream.encode_last(value.length, "US")
187
- @stream.add_last(value)
188
- # Implementation Version Name:
189
- tag = @stream.encode_tag("0002,0013")
190
- @stream.add_last(tag)
191
- @stream.encode_last("SH", "STR")
192
- value = @stream.encode_value(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
- # VR:
204
- vr = @stream.encode("UL", "STR")
205
- @stream.add_first(vr)
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
212
-
213
-
214
- # Writes each data element to file:
215
- def write_data_element(i)
216
- # Step 1: Write tag:
217
- write_tag(i)
218
- # Step 2: Write [vr] and value length:
219
- write_vr_length(i)
220
- # Step 3: Write value:
221
- write_value(i)
222
- # If DICOM object contains encapsulated pixel data, we need some special handling for its items:
223
- if @tags[i] == "7FE0,0010"
224
- @enc_image = true if @lengths[i].to_i == 0
225
- end
226
- end
227
-
228
-
229
- # Writes the tag (first part of the data element):
230
- def write_tag(i)
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.
234
- # When we shift from group 0002 to another group we need to update our endian/explicitness variables:
235
- if tag[0..3] != "0002" and @switched == false
236
- switch_syntax
237
- end
238
- # Write to binary string:
239
- bin_tag = @stream.encode_tag(tag)
240
- add(bin_tag)
241
- end
242
-
243
-
244
- # Writes the VR (if it is to be written) and length value
245
- # (these two are the middle part of the data element):
246
- def write_vr_length(i)
247
- # First some preprocessing:
248
- # Set length value:
249
- if @lengths[i] == nil
250
- # Set length value to 0:
251
- length4 = @stream.encode(0, "UL")
252
- length2 = @stream.encode(0, "US")
253
- elsif @lengths[i] == "UNDEFINED"
254
- # Set length to 'ff ff ff ff':
255
- length4 = @stream.encode(4294967295, "UL")
256
- # No length2 necessary for this case.
257
- else
258
- # Pick length value from array:
259
- length4 = @stream.encode(@lengths[i], "UL")
260
- length2 = @stream.encode(@lengths[i], "US")
261
- end
262
- # Structure will differ, dependent on whether we have explicit or implicit encoding:
263
- # *****EXPLICIT*****:
264
- if @explicit == true
265
- # Step 1: Write VR (if it is to be written)
266
- unless @tags[i] == "FFFE,E000" or @tags[i] == "FFFE,E00D" or @tags[i] == "FFFE,E0DD"
267
- # Write data element VR (2 bytes - since we are not dealing with an item related element):
268
- vr = @stream.encode(@vr[i], "STR")
269
- add(vr)
270
- end
271
- # Step 2: Write length
272
- # Three possible structures for value length here, dependent on data element vr:
273
- case @vr[i]
274
- when "OB","OW","SQ","UN","UT"
275
- if @enc_image
276
- # Item under an encapsulated Pixel Data (7FE0,0010):
277
- # 4 bytes:
278
- add(length4)
279
- else
280
- # 6 bytes total:
281
- # Two empty first:
282
- empty = @stream.encode("00"*2, "HEX")
283
- add(empty)
284
- # Value length (4 bytes):
285
- add(length4)
286
- end
287
- when "()"
288
- # 4 bytes:
289
- # For tags "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
290
- add(length4)
291
- else
292
- # 2 bytes:
293
- # For all the other data element vr, value length is 2 bytes:
294
- add(length2)
295
- end
296
- else
297
- # *****IMPLICIT*****:
298
- # No VR written.
299
- # Writing value length (4 bytes):
300
- add(length4)
301
- end
302
- end # of write_vr_length
303
-
304
-
305
- # Writes the value (last part of the data element):
306
- def write_value(i)
307
- # This is pretty straightforward, just dump the binary data to the file/string:
308
- add(@bin[i])
309
- end
310
-
311
-
312
- # Tests if the file/path is writable, creates any folders
313
- # if necessary, and opens the file for writing.
314
- def open_file(file)
315
- # Two cases: File already exists or it does not.
316
- # Check if file exists:
317
- if File.exist?(file)
318
- # Is it writable?
319
- if File.writable?(file)
320
- @file = File.new(file, "wb")
321
- else
322
- # Existing file is not writable:
323
- @msg << "Error! The program does not have permission or resources to create the file you specified. Returning. (#{file})"
324
- end
325
- else
326
- # File does not exist.
327
- # Check if this file's path contains a folder that does not exist, and therefore needs to be created:
328
- folders = file.split(File::SEPARATOR)
329
- if folders.length > 1
330
- # Remove last element (which should be the file string):
331
- folders.pop
332
- path = folders.join(File::SEPARATOR)
333
- # Check if this path exists:
334
- unless File.directory?(path)
335
- # We need to create (parts of) this path:
336
- require 'fileutils'
337
- FileUtils.mkdir_p path
338
- end
339
- end
340
- # The path to this non-existing file should now be prepared, and we proceed to creating the file:
341
- @file = File.new(file, "wb")
342
- end
343
- end
344
-
345
-
346
- # Changes encoding variables as the file writing proceeds past the initial 0002 group of the DICOM file.
347
- def switch_syntax
348
- # The information from the Transfer syntax element (if present), needs to be processed:
349
- result = LIBRARY.process_transfer_syntax(@transfer_syntax.rstrip)
350
- # Result is a 3-element array: [Validity of ts, explicitness, endianness]
351
- unless result[0]
352
- @msg << "Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."
353
- end
354
- @rest_explicit = result[1]
355
- @rest_endian = result[2]
356
- # We only plan to run this method once:
357
- @switched = true
358
- # Update explicitness and endianness (pack/unpack variables):
359
- @file_endian = @rest_endian
360
- @stream.set_endian(@rest_endian)
361
- @explicit = @rest_explicit
362
- @stream.explicit = @rest_explicit
363
- if @sys_endian == @file_endian
364
- @endian = true
365
- else
366
- @endian = false
367
- end
368
- end
369
-
370
-
371
- # Find the position of the first tag which is not a group "0002" tag:
372
- def first_non_meta
373
- i = 0
374
- go = true
375
- while go == true and i < @tags.length do
376
- tag = @tags[i]
377
- if tag[0..3] == "0002"
378
- i += 1
379
- else
380
- go = false
381
- end
382
- end
383
- return i
384
- end
385
-
386
-
387
- # Initializes the variables used when executing this program.
388
- def init_variables
389
- # Variables that are accesible from outside:
390
- # Until a DICOM write has completed successfully the status is 'unsuccessful':
391
- @success = false
392
- # Variables used internally:
393
- # Default explicitness of start of DICOM file:
394
- @explicit = true
395
- # Default endianness of start of DICOM files is little endian:
396
- @file_endian = false
397
- # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
398
- @switched = false
399
- # Use a "relationship endian" variable to guide writing of file:
400
- if @sys_endian == @file_endian
401
- @endian = true
402
- else
403
- @endian = false
404
- end
405
- # Items contained under the Pixel Data element needs some special attention to write correctly:
406
- @enc_image = false
407
- end
408
-
409
- end # of class
410
- end # of module