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