dicom 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +55 -0
- data/README +51 -29
- data/init.rb +1 -0
- data/lib/dicom.rb +35 -21
- data/lib/dicom/{Anonymizer.rb → anonymizer.rb} +178 -80
- data/lib/dicom/constants.rb +121 -0
- data/lib/dicom/d_client.rb +888 -0
- data/lib/dicom/d_library.rb +208 -0
- data/lib/dicom/d_object.rb +424 -0
- data/lib/dicom/d_read.rb +433 -0
- data/lib/dicom/d_server.rb +397 -0
- data/lib/dicom/d_write.rb +420 -0
- data/lib/dicom/data_element.rb +175 -0
- data/lib/dicom/{Dictionary.rb → dictionary.rb} +390 -398
- data/lib/dicom/elements.rb +82 -0
- data/lib/dicom/file_handler.rb +116 -0
- data/lib/dicom/item.rb +87 -0
- data/lib/dicom/{Link.rb → link.rb} +749 -388
- data/lib/dicom/ruby_extensions.rb +44 -35
- data/lib/dicom/sequence.rb +62 -0
- data/lib/dicom/stream.rb +493 -0
- data/lib/dicom/super_item.rb +696 -0
- data/lib/dicom/super_parent.rb +615 -0
- metadata +25 -18
- data/DOCUMENTATION +0 -469
- data/lib/dicom/DClient.rb +0 -584
- data/lib/dicom/DLibrary.rb +0 -194
- data/lib/dicom/DObject.rb +0 -1579
- data/lib/dicom/DRead.rb +0 -532
- data/lib/dicom/DServer.rb +0 -304
- data/lib/dicom/DWrite.rb +0 -410
- data/lib/dicom/FileHandler.rb +0 -50
- data/lib/dicom/Stream.rb +0 -354
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
|