dicom 0.5 → 0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|