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/DRead.rb
CHANGED
@@ -11,31 +11,45 @@ module DICOM
|
|
11
11
|
# Class for reading the data from a DICOM file:
|
12
12
|
class DRead
|
13
13
|
|
14
|
-
attr_reader :success
|
14
|
+
attr_reader :success, :names, :tags, :types, :lengths, :values, :raw, :levels, :explicit, :file_endian, :msg
|
15
15
|
|
16
16
|
# Initialize the DRead instance.
|
17
|
-
def initialize(
|
17
|
+
def initialize(string=nil, options={})
|
18
18
|
# Process option values, setting defaults for the ones that are not specified:
|
19
|
-
@lib =
|
20
|
-
@sys_endian =
|
19
|
+
@lib = options[:lib] || DLibrary.new
|
20
|
+
@sys_endian = options[:sys_endian] || false
|
21
|
+
@bin = options[:bin]
|
22
|
+
@transfer_syntax = options[:syntax]
|
21
23
|
# Initiate the variables that are used during file reading:
|
22
|
-
init_variables
|
24
|
+
init_variables
|
23
25
|
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if @file == nil
|
29
|
-
# File is not readable, so we return:
|
30
|
-
return
|
26
|
+
# Are we going to read from a file, or read from a binary string:
|
27
|
+
if @bin
|
28
|
+
# Read from the provided binary string:
|
29
|
+
@str = string
|
31
30
|
else
|
31
|
+
# Read from file:
|
32
|
+
open_file(string)
|
33
|
+
# Read the initial header of the file:
|
34
|
+
if @file == nil
|
35
|
+
# File is not readable, so we return:
|
36
|
+
return
|
37
|
+
else
|
38
|
+
# Extract the content of the file to a binary string:
|
39
|
+
@str = @file.read
|
40
|
+
@file.close
|
41
|
+
end
|
42
|
+
end
|
43
|
+
# Create a Stream instance to handle the decoding of content from this binary string:
|
44
|
+
@stream = Stream.new(@str, @file_endian, @explicit)
|
45
|
+
# Do not check for header information when supplied a (network) binary string:
|
46
|
+
unless @bin
|
32
47
|
# Read and verify the DICOM header:
|
33
|
-
header = check_header
|
34
|
-
# If the file didnt have the expected header, we will attempt to read
|
48
|
+
header = check_header
|
49
|
+
# If the file didnt have the expected header, we will attempt to read
|
50
|
+
# data elements from the very start file:
|
35
51
|
if header == false
|
36
|
-
@
|
37
|
-
@file = File.new(file_name, "rb")
|
38
|
-
@header_length = 0
|
52
|
+
@stream.skip(-132)
|
39
53
|
elsif header == nil
|
40
54
|
# Not a valid DICOM file, return:
|
41
55
|
return
|
@@ -46,12 +60,10 @@ module DICOM
|
|
46
60
|
# (Data element information is stored in arrays by the method process_data_element)
|
47
61
|
data_element = true
|
48
62
|
while data_element != false do
|
49
|
-
data_element = process_data_element
|
63
|
+
data_element = process_data_element
|
50
64
|
end
|
51
65
|
|
52
66
|
# Post processing:
|
53
|
-
# Close the file as we are finished reading it:
|
54
|
-
@file.close()
|
55
67
|
# Assume file has been read successfully:
|
56
68
|
@success = true
|
57
69
|
# Check if the last element was read out correctly (that the length of its data (@raw.last.length)
|
@@ -59,12 +71,65 @@ module DICOM
|
|
59
71
|
# We only run this test if the last element has a positive expectation value, obviously.
|
60
72
|
if @lengths.last.to_i > 0
|
61
73
|
if @raw.last.length != @lengths.last
|
62
|
-
@msg
|
74
|
+
@msg << "Error! The data content read from file does not match the length specified for the tag #{@tags.last}. It seems this is either an invalid or corrupt DICOM file. Returning."
|
63
75
|
@success = false
|
64
76
|
return
|
65
77
|
end
|
66
78
|
end
|
67
|
-
end # of
|
79
|
+
end # of initialize
|
80
|
+
|
81
|
+
|
82
|
+
# Extract an array of binary strings
|
83
|
+
# (this is typically used if one intends to transmit the DICOM file through a network connection)
|
84
|
+
def extract_segments(size)
|
85
|
+
# For this purpose we are not interested to include header or meta information.
|
86
|
+
# We must therefore find the position of the first tag which is not a meta information tag.
|
87
|
+
pos = first_non_meta
|
88
|
+
# Start position:
|
89
|
+
if pos == 0
|
90
|
+
start = 0
|
91
|
+
else
|
92
|
+
# First byte after the integrated length of the previous tag is our start:
|
93
|
+
start = @integrated_lengths[pos-1]
|
94
|
+
end
|
95
|
+
# Iterate through the tags and monitor the integrated_lengths values to determine
|
96
|
+
# when we need to start a new segment.
|
97
|
+
segments = Array.new
|
98
|
+
last_pos = pos
|
99
|
+
@tags.each_index do |i|
|
100
|
+
# Have we passed the size limit?
|
101
|
+
if (@integrated_lengths[i] - start) > size
|
102
|
+
# We either need to stop the current segment at the previous tag, or if
|
103
|
+
# this is a long tag (typically image data), we need to split its data
|
104
|
+
# and put it in several segments.
|
105
|
+
if (@integrated_lengths[i] - @integrated_lengths[i-1]) > size
|
106
|
+
# This element's value needs to be split up into several segments.
|
107
|
+
# How many segments are needed to fit this element?
|
108
|
+
number = ((@integrated_lengths[i] - start).to_f / size.to_f).ceil
|
109
|
+
number.times do
|
110
|
+
# Extract data and add to segments:
|
111
|
+
last_pos = (start+size-1)
|
112
|
+
segments << @stream.string[start..last_pos]
|
113
|
+
# Update start position for next segment:
|
114
|
+
start = last_pos + 1
|
115
|
+
end
|
116
|
+
else
|
117
|
+
# End the current segment at the last data element, then start the new segment with this element.
|
118
|
+
last_pos = @integrated_lengths[i-1]
|
119
|
+
segments << @stream.string[start..last_pos]
|
120
|
+
# Update start position for next segment:
|
121
|
+
start = last_pos + 1
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
# After running the above iteration, it is possible that we have some data elements remaining
|
126
|
+
# at the end of the file who's length are beneath the size limit, and thus has not been put into a segment.
|
127
|
+
if (last_pos + 1) < @stream.string.length
|
128
|
+
# Add the remaining data elements to a segment:
|
129
|
+
segments << @stream.string[start..@stream.string.length]
|
130
|
+
end
|
131
|
+
return segments
|
132
|
+
end
|
68
133
|
|
69
134
|
|
70
135
|
# Following methods are private:
|
@@ -72,40 +137,40 @@ module DICOM
|
|
72
137
|
|
73
138
|
|
74
139
|
# Checks the initial header of the DICOM file.
|
75
|
-
def check_header
|
140
|
+
def check_header
|
76
141
|
# According to the official DICOM standard, a DICOM file shall contain 128
|
77
142
|
# consequtive (zero) bytes followed by 4 bytes that spell the string 'DICM'.
|
78
143
|
# Apparently, some providers seems to skip this in their DICOM files.
|
79
|
-
|
80
|
-
@
|
81
|
-
|
82
|
-
bin2 = @file.read(4)
|
83
|
-
@header_length += 4
|
84
|
-
# Check if this binary was successfully read (if not, this short file is not a valid DICOM file and we will return):
|
85
|
-
if bin2
|
86
|
-
dicm = bin2.unpack('a' * 4).join
|
87
|
-
else
|
144
|
+
# Check that the file is long enough to contain a valid header:
|
145
|
+
if @str.length < 132
|
146
|
+
# This does not seem to be a valid DICOM file and so we return.
|
88
147
|
return nil
|
89
|
-
end
|
90
|
-
if dicm != 'DICM' then
|
91
|
-
# Header is not valid (we will still try to read it is a DICOM file though):
|
92
|
-
@msg += ["Warning: The specified file does not contain the official DICOM header. Will try to read the file anyway, as some sources are known to skip this header."]
|
93
|
-
# As the file is not conforming to the DICOM standard, it is possible that it does not contain a
|
94
|
-
# transfer syntax element, and as such, we attempt to choose the most probable encoding values here:
|
95
|
-
@explicit = false
|
96
|
-
return false
|
97
148
|
else
|
98
|
-
|
99
|
-
|
149
|
+
@stream.skip(128)
|
150
|
+
# Next 4 bytes should spell "DICM":
|
151
|
+
identifier = @stream.decode(4, "STR")
|
152
|
+
@header_length += 132
|
153
|
+
if identifier != "DICM" then
|
154
|
+
# Header is not valid (we will still try to read it is a DICOM file though):
|
155
|
+
@msg << "Warning: The specified file does not contain the official DICOM header. Will try to read the file anyway, as some sources are known to skip this header."
|
156
|
+
# As the file is not conforming to the DICOM standard, it is possible that it does not contain a
|
157
|
+
# transfer syntax element, and as such, we attempt to choose the most probable encoding values here:
|
158
|
+
@explicit = false
|
159
|
+
@stream.explicit = false
|
160
|
+
return false
|
161
|
+
else
|
162
|
+
# Header is valid:
|
163
|
+
return true
|
164
|
+
end
|
100
165
|
end
|
101
|
-
end
|
166
|
+
end
|
102
167
|
|
103
168
|
|
104
169
|
# Governs the process of reading data elements from the DICOM file.
|
105
|
-
def process_data_element
|
170
|
+
def process_data_element
|
106
171
|
#STEP 1: ------------------------------------------------------
|
107
172
|
# Attempt to read data element tag, but abort if we have reached end of file:
|
108
|
-
tag = read_tag
|
173
|
+
tag = read_tag
|
109
174
|
if tag == false
|
110
175
|
# End of file, no more elements.
|
111
176
|
return false
|
@@ -155,102 +220,83 @@ module DICOM
|
|
155
220
|
# Set the hiearchy level of this data element:
|
156
221
|
set_level(level_type, length, tag, name)
|
157
222
|
# Transfer the gathered data to arrays and return true:
|
158
|
-
@names
|
159
|
-
@tags
|
160
|
-
@types
|
161
|
-
@lengths
|
162
|
-
@values
|
163
|
-
@raw
|
223
|
+
@names << name
|
224
|
+
@tags << tag
|
225
|
+
@types << type
|
226
|
+
@lengths << length
|
227
|
+
@values << value
|
228
|
+
@raw << raw
|
164
229
|
return true
|
165
|
-
end # of
|
230
|
+
end # of process_data_element
|
166
231
|
|
167
232
|
|
168
233
|
# Reads and returns the data element's TAG (4 first bytes of element).
|
169
|
-
def read_tag
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end
|
176
|
-
# Add the length of the data element tag. If this was the first element read from file, we need to add the header length too:
|
234
|
+
def read_tag
|
235
|
+
tag = @stream.decode_tag
|
236
|
+
# Do not proceed if we have reached end of file (tag is nil):
|
237
|
+
return false unless tag
|
238
|
+
# Tag was valid, so we add the length of the data element tag.
|
239
|
+
# If this was the first element read from file, we need to add the header length too:
|
177
240
|
if @integrated_lengths.length == 0
|
178
241
|
# Increase the array with the length of the header + the 4 bytes:
|
179
|
-
@integrated_lengths
|
242
|
+
@integrated_lengths << (@header_length + 4)
|
180
243
|
else
|
181
244
|
# For the remaining elements, increase the array with the integrated length of the previous elements + the 4 bytes:
|
182
|
-
@integrated_lengths
|
183
|
-
end
|
184
|
-
# Unpack the blobs:
|
185
|
-
tag1 = bin1.unpack('h*')[0].reverse.upcase
|
186
|
-
tag2 = bin2.unpack('h*')[0].reverse.upcase
|
187
|
-
# Whether DICOM file is big or little endian, the first 0002 group is always little endian encoded.
|
188
|
-
# In case of big endian system:
|
189
|
-
if @sys_endian
|
190
|
-
# Rearrange the numbers (# This has never been tested btw.):
|
191
|
-
tag1 = tag1[2..3]+tag1[0..1]
|
192
|
-
tag2 = tag2[2..3]+tag2[0..1]
|
245
|
+
@integrated_lengths << (@integrated_lengths[@integrated_lengths.length-1] + 4)
|
193
246
|
end
|
194
247
|
# When we shift from group 0002 to another group we need to update our endian/explicitness variables:
|
195
|
-
if
|
196
|
-
switch_syntax
|
197
|
-
end
|
198
|
-
# Perhaps we need to rearrange the tag strings?
|
199
|
-
if not @endian
|
200
|
-
# Need to rearrange the first and second part of each string:
|
201
|
-
tag1 = tag1[2..3]+tag1[0..1]
|
202
|
-
tag2 = tag2[2..3]+tag2[0..1]
|
248
|
+
if tag[0..3] != "0002" and @switched == false
|
249
|
+
switch_syntax
|
203
250
|
end
|
204
|
-
|
205
|
-
|
206
|
-
end # of method read_tag
|
251
|
+
return tag
|
252
|
+
end
|
207
253
|
|
208
254
|
|
209
|
-
# Reads and returns data element TYPE (VR) (2 bytes) and data element LENGTH (Varying length).
|
255
|
+
# Reads and returns data element TYPE (VR) (2 bytes) and data element LENGTH (Varying length; 2-6 bytes).
|
210
256
|
def read_type_length(type,tag)
|
211
257
|
# Structure will differ, dependent on whether we have explicit or implicit encoding:
|
258
|
+
pre_skip = 0
|
259
|
+
bytes = 0
|
212
260
|
# *****EXPLICIT*****:
|
213
261
|
if @explicit == true
|
214
262
|
# Step 1: Read VR (if it exists)
|
215
263
|
unless tag == "FFFE,E000" or tag == "FFFE,E00D" or tag == "FFFE,E0DD"
|
216
264
|
# Read the element's type (2 bytes - since we are not dealing with an item related element):
|
217
|
-
|
265
|
+
type = @stream.decode(2, "STR")
|
218
266
|
@integrated_lengths[@integrated_lengths.length-1] += 2
|
219
|
-
type = bin.unpack('a*').join
|
220
267
|
end
|
221
268
|
# Step 2: Read length
|
222
269
|
# Three possible structures for value length here, dependent on element type:
|
223
270
|
case type
|
224
271
|
when "OB","OW","SQ","UN"
|
225
272
|
# 6 bytes total:
|
226
|
-
# Two empty first:
|
227
|
-
|
228
|
-
@integrated_lengths[@integrated_lengths.length-1] += 2
|
273
|
+
# Two empty bytes first:
|
274
|
+
pre_skip = 2
|
229
275
|
# Value length (4 bytes):
|
230
|
-
|
231
|
-
@integrated_lengths[@integrated_lengths.length-1] += 4
|
232
|
-
length = bin.unpack(@ul)[0]
|
276
|
+
bytes = 4
|
233
277
|
when "()"
|
234
278
|
# 4 bytes:
|
235
279
|
# For elements "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
|
236
|
-
|
237
|
-
@integrated_lengths[@integrated_lengths.length-1] += 4
|
238
|
-
length = bin.unpack(@ul)[0]
|
280
|
+
bytes = 4
|
239
281
|
else
|
240
282
|
# 2 bytes:
|
241
283
|
# For all the other element types, value length is 2 bytes:
|
242
|
-
|
243
|
-
@integrated_lengths[@integrated_lengths.length-1] += 2
|
244
|
-
length = bin.unpack(@us)[0]
|
284
|
+
bytes = 2
|
245
285
|
end
|
246
286
|
else
|
247
287
|
# *****IMPLICIT*****:
|
248
|
-
#
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
288
|
+
# Value length (4 bytes):
|
289
|
+
bytes = 4
|
290
|
+
end
|
291
|
+
# Handle skips and read out length value:
|
292
|
+
@stream.skip(pre_skip)
|
293
|
+
if bytes == 2
|
294
|
+
length = @stream.decode(bytes, "US") # (2)
|
295
|
+
else
|
296
|
+
length = @stream.decode(bytes, "UL") # (4)
|
253
297
|
end
|
298
|
+
# Update integrated lengths array:
|
299
|
+
@integrated_lengths[@integrated_lengths.length-1] += (pre_skip + bytes)
|
254
300
|
# For encapsulated data, the element length will not be defined. To convey this,
|
255
301
|
# the hex sequence 'ff ff ff ff' is used (-1 converted to signed long, 4294967295 converted to unsigned long).
|
256
302
|
if length == 4294967295
|
@@ -261,110 +307,36 @@ module DICOM
|
|
261
307
|
@msg += ["Warning: Odd number of bytes in data element's length occured. This is a violation of the DICOM standard, but program will attempt to read the rest of the file anyway."]
|
262
308
|
end
|
263
309
|
return [type, length]
|
264
|
-
end # of
|
310
|
+
end # of read_type_length
|
265
311
|
|
266
312
|
|
267
313
|
# Reads and returns data element VALUE (Of varying length - which is determined at an earlier stage).
|
268
314
|
def read_value(type, length)
|
269
|
-
#
|
270
|
-
bin = @
|
315
|
+
# Extract the binary data:
|
316
|
+
bin = @stream.extract(length)
|
271
317
|
@integrated_lengths[@integrated_lengths.size-1] += length
|
272
|
-
#
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
#
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
data = bin.unpack(@ul).join("/")
|
283
|
-
end
|
284
|
-
|
285
|
-
# Signed long: (4 bytes)
|
286
|
-
when "SL"
|
287
|
-
if length <= 4
|
288
|
-
data = bin.unpack(@sl)[0]
|
289
|
-
else
|
290
|
-
data = bin.unpack(@sl).join("/")
|
291
|
-
end
|
292
|
-
|
293
|
-
# Unsigned short: (2 bytes)
|
294
|
-
when "US"
|
295
|
-
if length <= 2
|
296
|
-
data = bin.unpack(@us)[0]
|
297
|
-
else
|
298
|
-
data = bin.unpack(@us).join("/")
|
299
|
-
end
|
300
|
-
|
301
|
-
# Signed short: (2 bytes)
|
302
|
-
when "SS"
|
303
|
-
if length <= 2
|
304
|
-
data = bin.unpack(@ss)[0]
|
305
|
-
else
|
306
|
-
data = bin.unpack(@ss).join("/")
|
307
|
-
end
|
308
|
-
|
309
|
-
# Floating point single: (4 bytes)
|
310
|
-
when "FL"
|
311
|
-
if length <= 4
|
312
|
-
data = bin.unpack(@fs)[0]
|
313
|
-
else
|
314
|
-
data = bin.unpack(@fs).join("/")
|
315
|
-
end
|
316
|
-
|
317
|
-
# Floating point double: (8 bytes)
|
318
|
-
when "FD"
|
319
|
-
if length <= 8
|
320
|
-
data = bin.unpack(@fd)[0]
|
321
|
-
else
|
322
|
-
data = bin.unpack(@fd).join("/")
|
323
|
-
end
|
324
|
-
|
325
|
-
# The data element contains a tag as its value (4 bytes):
|
326
|
-
when "AT"
|
327
|
-
# Bytes read in following order: 1 0 , 3 2 (And Hex nibbles read in this order: Hh)
|
328
|
-
# NB! This probably needs to be modified when dealing with something other than little endian.
|
329
|
-
# Value is unpacked to a string in the format GGGGEEEE.
|
330
|
-
data = (bin.unpack("xHXhX2HXh").join + bin.unpack("x3HXhX2HXh").join).upcase
|
331
|
-
#data = (bin.unpack("xHXhX2HXh").join + "," + bin.unpack("x3HXhX2HXh").join).upcase
|
332
|
-
|
333
|
-
# We have a number of VRs that are decoded as string:
|
334
|
-
when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT' #,'VR'
|
335
|
-
data = bin.unpack('a*').join
|
336
|
-
|
337
|
-
# NB!
|
338
|
-
# FOLLOWING ELEMENT TYPES WILL NOT BE DECODED.
|
339
|
-
# DECODING OF PIXEL DATA IS MOVED TO DOBJECT FOR PERFORMANCE REASONS.
|
340
|
-
|
341
|
-
# Unknown information, header element is not recognized from local database:
|
342
|
-
when "UN"
|
343
|
-
#data=bin.unpack('H*')[0]
|
344
|
-
|
345
|
-
# Other byte string, 1-byte integers
|
346
|
-
when "OB"
|
347
|
-
#data = bin.unpack('H*')[0]
|
348
|
-
|
349
|
-
# Other float string, 4-byte floating point numbers
|
350
|
-
when "OF"
|
351
|
-
# NB! This element type has not been tested yet with an actual DICOM file.
|
352
|
-
#data = bin.unpack(@fs)
|
353
|
-
|
354
|
-
# Image data:
|
355
|
-
# Other word string, 2-byte integers
|
356
|
-
when "OW"
|
357
|
-
# empty
|
358
|
-
|
359
|
-
# Unknown VR:
|
318
|
+
# Decode data?
|
319
|
+
# Some data elements (like those containing image data, compressed data or unknown data),
|
320
|
+
# will not be decoded here.
|
321
|
+
unless type == "OW" or type == "OB" or type == "OF" or type == "UN"
|
322
|
+
# "Rewind" and extract the value from this binary data:
|
323
|
+
@stream.skip(-length)
|
324
|
+
# Decode data:
|
325
|
+
value = @stream.decode(length, type)
|
326
|
+
if not value.is_a?(Array)
|
327
|
+
data = value
|
360
328
|
else
|
361
|
-
|
362
|
-
#
|
363
|
-
|
364
|
-
|
329
|
+
# If the returned value is not a string, it is an array of multiple elements,
|
330
|
+
# which need to be joined to a string with the separator "\":
|
331
|
+
data = value.join("\\")
|
332
|
+
end
|
333
|
+
else
|
334
|
+
# No decoded data:
|
335
|
+
data = nil
|
336
|
+
end
|
365
337
|
# Return the data:
|
366
338
|
return [data, bin]
|
367
|
-
end # of
|
339
|
+
end # of read_value
|
368
340
|
|
369
341
|
|
370
342
|
# Sets the level of the current element in the hiearchy.
|
@@ -389,9 +361,9 @@ module DICOM
|
|
389
361
|
@current_level = @current_level + 1
|
390
362
|
# If length of sequence/item is specified, we must note this length + the current element position in the arrays:
|
391
363
|
if length.to_i != 0
|
392
|
-
@hierarchy
|
364
|
+
@hierarchy << [length, @integrated_lengths.last]
|
393
365
|
else
|
394
|
-
@hierarchy
|
366
|
+
@hierarchy << type
|
395
367
|
end
|
396
368
|
end
|
397
369
|
# Need to check whether a previous sequence or item has ended, if so the level must be decreased by one:
|
@@ -406,14 +378,14 @@ module DICOM
|
|
406
378
|
if @hierarchy.size > 0
|
407
379
|
# Do not perform this check for Pixel Data Items or Sequence Delimitation Items:
|
408
380
|
# (If performed, it will give false errors for the case when we have Encapsulated Pixel Data)
|
409
|
-
check_level_end
|
381
|
+
check_level_end unless name == "Pixel Data Item" or tag == "FFFE,E0DD"
|
410
382
|
end
|
411
|
-
end # of
|
383
|
+
end # of set_level
|
412
384
|
|
413
385
|
|
414
386
|
# Checks how far we've read in the DICOM file to determine if we have reached a point
|
415
387
|
# where sub-levels are ending. This method is recursive, as multiple sequences/items might end at the same point.
|
416
|
-
def check_level_end
|
388
|
+
def check_level_end
|
417
389
|
# The test is only meaningful to perform if we are not expecting an 'end of sequence/item' element to signal the level-change.
|
418
390
|
if (@hierarchy.last).is_a?(Array)
|
419
391
|
described_length = (@hierarchy.last)[0]
|
@@ -427,7 +399,7 @@ module DICOM
|
|
427
399
|
if (@hierarchy.size > 1)
|
428
400
|
@hierarchy = @hierarchy[0..(@hierarchy.size-2)]
|
429
401
|
# There might be numerous levels that ends at this particular point, so we need to do a recursive repeat to check.
|
430
|
-
check_level_end
|
402
|
+
check_level_end
|
431
403
|
else
|
432
404
|
@hierarchy = Array.new()
|
433
405
|
end
|
@@ -439,7 +411,7 @@ module DICOM
|
|
439
411
|
end
|
440
412
|
end
|
441
413
|
end
|
442
|
-
end
|
414
|
+
end
|
443
415
|
|
444
416
|
|
445
417
|
# Tests if the file is readable and opens it.
|
@@ -450,115 +422,78 @@ module DICOM
|
|
450
422
|
if File.size(file) > 8
|
451
423
|
@file = File.new(file, "rb")
|
452
424
|
else
|
453
|
-
@msg
|
425
|
+
@msg << "Error! File is too small to contain DICOM information. Returning. (#{file})"
|
454
426
|
end
|
455
427
|
else
|
456
|
-
@msg
|
428
|
+
@msg << "Error! File is a directory. Returning. (#{file})"
|
457
429
|
end
|
458
430
|
else
|
459
|
-
@msg
|
431
|
+
@msg << "Error! File exists but I don't have permission to read it. Returning. (#{file})"
|
460
432
|
end
|
461
433
|
else
|
462
|
-
@msg
|
434
|
+
@msg << "Error! The file you have supplied does not exist. Returning. (#{file})"
|
463
435
|
end
|
464
|
-
end
|
436
|
+
end
|
465
437
|
|
466
438
|
|
467
439
|
# Changes encoding variables as the file reading proceeds past the initial 0002 group of the DICOM file.
|
468
|
-
def switch_syntax
|
469
|
-
#
|
470
|
-
|
440
|
+
def switch_syntax
|
441
|
+
# Get the transfer syntax string, unless it has already been provided by keyword:
|
442
|
+
unless @transfer_syntax
|
443
|
+
ts_pos = @tags.index("0002,0010")
|
444
|
+
if ts_pos
|
445
|
+
@transfer_syntax = @values[ts_pos].rstrip
|
446
|
+
else
|
447
|
+
@transfer_syntax = "1.2.840.10008.1.2" # Default is implicit, little endian
|
448
|
+
end
|
449
|
+
end
|
450
|
+
# Query the library with our particular transfer syntax string:
|
451
|
+
result = @lib.process_transfer_syntax(@transfer_syntax)
|
452
|
+
# Result is a 3-element array: [Validity of ts, explicitness, endianness]
|
453
|
+
unless result[0]
|
454
|
+
@msg+=["Warning: Invalid/unknown transfer syntax! Will try reading the file, but errors may occur."]
|
455
|
+
end
|
456
|
+
@rest_explicit = result[1]
|
457
|
+
@rest_endian = result[2]
|
471
458
|
# We only plan to run this method once:
|
472
459
|
@switched = true
|
473
460
|
# Update endian, explicitness and unpack variables:
|
474
461
|
@file_endian = @rest_endian
|
462
|
+
@stream.set_endian(@rest_endian)
|
475
463
|
@explicit = @rest_explicit
|
476
|
-
|
477
|
-
@endian = true
|
478
|
-
else
|
479
|
-
@endian = false
|
480
|
-
end
|
481
|
-
set_unpack_strings()
|
464
|
+
@stream.explicit = @rest_explicit
|
482
465
|
end
|
483
466
|
|
484
467
|
|
485
|
-
#
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
468
|
+
# Find the position of the first tag which is not a group "0002" tag:
|
469
|
+
def first_non_meta
|
470
|
+
i = 0
|
471
|
+
go = true
|
472
|
+
while go == true and i < @tags.length do
|
473
|
+
tag = @tags[i]
|
474
|
+
if tag[0..3] == "0002"
|
475
|
+
i += 1
|
476
|
+
else
|
477
|
+
go = false
|
494
478
|
end
|
495
|
-
case ts_value
|
496
|
-
# Some variations with uncompressed pixel data:
|
497
|
-
when "1.2.840.10008.1.2"
|
498
|
-
# Implicit VR, Little Endian
|
499
|
-
@rest_explicit = false
|
500
|
-
@rest_endian = false
|
501
|
-
when "1.2.840.10008.1.2.1"
|
502
|
-
# Explicit VR, Little Endian
|
503
|
-
@rest_explicit = true
|
504
|
-
@rest_endian = false
|
505
|
-
when "1.2.840.10008.1.2.1.99"
|
506
|
-
# Deflated Explicit VR, Little Endian
|
507
|
-
@msg += ["Warning: Transfer syntax 'Deflated Explicit VR, Little Endian' is untested. Unknown if this is handled correctly!"]
|
508
|
-
@rest_explicit = true
|
509
|
-
@rest_endian = false
|
510
|
-
when "1.2.840.10008.1.2.2"
|
511
|
-
# Explicit VR, Big Endian
|
512
|
-
@rest_explicit = true
|
513
|
-
@rest_endian = true
|
514
|
-
else
|
515
|
-
# For everything else, assume compressed pixel data, with Explicit VR, Little Endian:
|
516
|
-
@rest_explicit = true
|
517
|
-
@rest_endian = false
|
518
|
-
end # of case ts_value
|
519
|
-
end # of if ts_pos != nil
|
520
|
-
end # of method process_syntax
|
521
|
-
|
522
|
-
|
523
|
-
# Sets the unpack format strings that will be used for numbers depending on endianness of file/system.
|
524
|
-
def set_unpack_strings
|
525
|
-
if @endian
|
526
|
-
# System endian equals file endian:
|
527
|
-
# Native byte order.
|
528
|
-
@by = "C*" # Byte (1 byte)
|
529
|
-
@us = "S*" # Unsigned short (2 bytes)
|
530
|
-
@ss = "s*" # Signed short (2 bytes)
|
531
|
-
@ul = "I*" # Unsigned long (4 bytes)
|
532
|
-
@sl = "l*" # Signed long (4 bytes)
|
533
|
-
@fs = "e*" # Floating point single (4 bytes)
|
534
|
-
@fd = "E*" # Floating point double ( 8 bytes)
|
535
|
-
else
|
536
|
-
# System endian not equal to file endian:
|
537
|
-
# Network byte order.
|
538
|
-
@by = "C*"
|
539
|
-
@us = "n*"
|
540
|
-
@ss = "n*" # Not correct (gives US)
|
541
|
-
@ul = "N*"
|
542
|
-
@sl = "N*" # Not correct (gives UL)
|
543
|
-
@fs = "g*"
|
544
|
-
@fd = "G*"
|
545
479
|
end
|
480
|
+
return i
|
546
481
|
end
|
547
482
|
|
548
483
|
|
549
484
|
# Initiates the variables that are used during file reading.
|
550
|
-
def init_variables
|
485
|
+
def init_variables
|
551
486
|
# Variables that hold data that will be available to the DObject class.
|
552
487
|
# Arrays that will hold information from the elements of the DICOM file:
|
553
|
-
@names = Array.new
|
554
|
-
@tags = Array.new
|
555
|
-
@types = Array.new
|
556
|
-
@lengths = Array.new
|
557
|
-
@values = Array.new
|
558
|
-
@raw = Array.new
|
559
|
-
@levels = Array.new
|
488
|
+
@names = Array.new
|
489
|
+
@tags = Array.new
|
490
|
+
@types = Array.new
|
491
|
+
@lengths = Array.new
|
492
|
+
@values = Array.new
|
493
|
+
@raw = Array.new
|
494
|
+
@levels = Array.new
|
560
495
|
# Array that will holde any messages generated while reading the DICOM file:
|
561
|
-
@msg = Array.new
|
496
|
+
@msg = Array.new
|
562
497
|
# Variables that contain properties of the DICOM file:
|
563
498
|
# Variable to keep track of whether the image pixel data in this file are compressed or not, and if it exists at all:
|
564
499
|
# Default explicitness of start of DICOM file::
|
@@ -571,10 +506,10 @@ module DICOM
|
|
571
506
|
# Variables used internally when reading through the DICOM file:
|
572
507
|
# Array for keeping track of how many bytes have been read from the file up to and including each data element:
|
573
508
|
# (This is necessary for tracking the hiearchy in some DICOM files)
|
574
|
-
@integrated_lengths = Array.new
|
509
|
+
@integrated_lengths = Array.new
|
575
510
|
@header_length = 0
|
576
511
|
# Array to keep track of the hierarchy of elements (this will be used to determine when a sequence or item is finished):
|
577
|
-
@hierarchy = Array.new
|
512
|
+
@hierarchy = Array.new
|
578
513
|
@hierarchy_error = false
|
579
514
|
# Explicitness of the remaining groups after the initial 0002 group:
|
580
515
|
@rest_explicit = false
|
@@ -582,14 +517,6 @@ module DICOM
|
|
582
517
|
@rest_endian = false
|
583
518
|
# When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
|
584
519
|
@switched = false
|
585
|
-
# Use a "relationship endian" variable to guide reading of file:
|
586
|
-
if @sys_endian == @file_endian
|
587
|
-
@endian = true
|
588
|
-
else
|
589
|
-
@endian = false
|
590
|
-
end
|
591
|
-
# Set which format strings to use when unpacking numbers:
|
592
|
-
set_unpack_strings
|
593
520
|
# A length variable will be used at the end to check whether the last element was read correctly, or whether the file endend unexpectedly:
|
594
521
|
@data_length = 0
|
595
522
|
# Keeping track of the data element's level while reading through the file:
|
@@ -598,7 +525,9 @@ module DICOM
|
|
598
525
|
@undef = "UNDEFINED"
|
599
526
|
# Items contained under the pixel data element may contain data directly, so we need a variable to keep track of this:
|
600
527
|
@enc_image = false
|
528
|
+
# Assume header size is zero bytes until otherwise is determined:
|
529
|
+
@header_length = 0
|
601
530
|
end
|
602
531
|
|
603
|
-
end #
|
604
|
-
end #
|
532
|
+
end # of class
|
533
|
+
end # of module
|