dicom 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +30 -4
- data/DOCUMENTATION +135 -14
- data/README +14 -5
- data/lib/Anonymizer.rb +433 -0
- data/lib/DLibrary.rb +29 -24
- data/lib/DObject.rb +779 -212
- data/lib/DRead.rb +410 -679
- data/lib/DWrite.rb +432 -0
- data/lib/Dictionary.rb +2795 -2805
- data/lib/dicom.rb +5 -1
- metadata +11 -9
data/lib/DRead.rb
CHANGED
@@ -1,197 +1,100 @@
|
|
1
|
+
# Copyright 2008-2009 Christoffer Lerv�g
|
2
|
+
|
3
|
+
# Some notes about this DICOM file reading class:
|
4
|
+
# In addition to reading files that are compliant to DICOM 3 Part 10,
|
5
|
+
# the philosophy of this library is to have maximum compatibility,
|
6
|
+
# and thus it will read most 'DICOM' files that deviate from the standard.
|
7
|
+
# While reading files, this class will also analyse the hierarchy of elements
|
8
|
+
# for those DICOM files that feature sequences and items, enabling the user
|
9
|
+
# to take advantage of this information for advanced querying of the
|
10
|
+
# DICOM object afterwards.
|
11
|
+
|
1
12
|
module DICOM
|
2
13
|
# Class for reading the data from a DICOM file:
|
3
14
|
class DRead
|
4
15
|
|
5
|
-
attr_reader :success
|
16
|
+
attr_reader :success,:names,:labels,:types,:lengths,:values,:raw,:levels,:explicit,:file_endian,:msg
|
6
17
|
|
7
18
|
# Initialize the DRead instance.
|
8
|
-
def initialize(file_name=nil,
|
9
|
-
|
10
|
-
@
|
11
|
-
|
12
|
-
# Arrays that will hold information from the DICOM file:
|
13
|
-
@names = Array.new()
|
14
|
-
@labels = Array.new()
|
15
|
-
@types = Array.new()
|
16
|
-
@lengths = Array.new()
|
17
|
-
@values = Array.new()
|
18
|
-
@raw = Array.new()
|
19
|
-
@levels = Array.new()
|
20
|
-
# Keeping track of how many bytes have been read from the file up to and including each tag:
|
21
|
-
# This is necessary for tracking the hiearchy in some DICOM files.
|
22
|
-
@integrated_lengths = Array.new()
|
23
|
-
@header_length = 0
|
24
|
-
# Keep track of the hierarchy of tags (this will be used to determine when a sequence or item is finished):
|
25
|
-
@hierarchy = Array.new()
|
26
|
-
@hierarchy_error = false
|
27
|
-
# Array that will holde any messages generated while reading the DICOM file:
|
28
|
-
@msg = Array.new()
|
29
|
-
# Explicitness (explicit (true) by default):
|
30
|
-
@explicit = true
|
31
|
-
# Explicitness of the remaining groups after the first group:
|
32
|
-
@rest_explicit = true
|
33
|
-
# Variable to keep track of whether the current sequence being read have length specified or not:
|
34
|
-
@sq_length = false
|
35
|
-
# Variable to keep track of whether the image pixel data in this file are compressed or not, and if it exists at all:
|
36
|
-
@compression = false
|
37
|
-
# Pixel data is color or greyscale?
|
38
|
-
@color = false
|
39
|
-
# Default endianness of start of DICOM files is little endian:
|
40
|
-
@file_endian=false
|
19
|
+
def initialize(file_name=nil, opts={})
|
20
|
+
# Process option values, setting defaults for the ones that are not specified:
|
21
|
+
@lib = opts[:lib] || DLibrary.new
|
22
|
+
@sys_endian = opts[:sys_endian] || false
|
41
23
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
# Endianness of the remaining groups after the first group:
|
48
|
-
@rest_endian=false
|
49
|
-
# Use a "relationship endian" variable to guide reading of file (true if they are equal):
|
50
|
-
if @sys_endian == @file_endian
|
51
|
-
@endian = true
|
52
|
-
else
|
53
|
-
@endian = false
|
54
|
-
end
|
55
|
-
# A length variable will be used at the end to check whether the last tag was read correctly, or whether the file endend unexpectedly:
|
56
|
-
@data_length = 0
|
57
|
-
# Variable used to tell whether file was read succesfully or not:
|
58
|
-
@success = false
|
59
|
-
# Keeping track of the tag level while reading through the file:
|
60
|
-
@current_level = 0
|
61
|
-
|
62
|
-
# Open file for binary reading:
|
63
|
-
begin
|
64
|
-
@file = File.new(file_name, "rb")
|
65
|
-
rescue
|
66
|
-
@msg += ["Error! Could not open file: "+ file_name + " Returning."]
|
67
|
-
return
|
68
|
-
end
|
69
|
-
|
70
|
-
# Load the library class (DICOM dictionary):
|
71
|
-
if lib != nil
|
72
|
-
# Library already specified by user:
|
73
|
-
@lib = lib
|
74
|
-
else
|
75
|
-
@lib = DLibrary.new()
|
76
|
-
end
|
24
|
+
# Initiate the variables that are used during file reading:
|
25
|
+
init_variables()
|
26
|
+
|
27
|
+
# Test if file is readable and open it to the @file variable:
|
28
|
+
open_file(file_name)
|
77
29
|
|
78
30
|
# Read the initial header of the file:
|
79
|
-
|
80
|
-
|
81
|
-
@file.close()
|
82
|
-
@file = File.new(file_name, "rb")
|
83
|
-
@header_length = 0
|
84
|
-
elsif header == nil
|
85
|
-
# Reading the file did not succeed, and we need to abort.
|
86
|
-
@msg += ["Error! Could not read: "+ file_name + " It might be a directory. Returning."]
|
31
|
+
if @file == nil
|
32
|
+
# File is not readable, so we return:
|
87
33
|
return
|
34
|
+
else
|
35
|
+
# Read and verify the DICOM header:
|
36
|
+
header = check_header()
|
37
|
+
# If there was no header, we will attempt to read tags from the very start of the file:
|
38
|
+
if header == false
|
39
|
+
@file.close()
|
40
|
+
@file = File.new(file_name, "rb")
|
41
|
+
@header_length = 0
|
42
|
+
end
|
88
43
|
end
|
89
|
-
|
90
|
-
#
|
44
|
+
|
45
|
+
# Run a loop to read the tags:
|
46
|
+
# (Tag information is stored in arrays by the method process_tag)
|
91
47
|
tag = true
|
92
|
-
|
93
|
-
|
94
|
-
tag=process_tag()
|
95
|
-
# Store the tag information in arrays:
|
96
|
-
if tag != false
|
97
|
-
@names+=[tag[0]]
|
98
|
-
@labels+=[tag[1]]
|
99
|
-
@types+=[tag[2]]
|
100
|
-
@lengths+=[tag[3]]
|
101
|
-
@values+=[tag[4]]
|
102
|
-
@raw+=[tag[5]]
|
103
|
-
end
|
48
|
+
while tag != false do
|
49
|
+
tag = process_tag()
|
104
50
|
end
|
105
|
-
|
106
|
-
#
|
107
|
-
check_pixel_status()
|
108
|
-
# Index of last element in tag arrays:
|
109
|
-
@lastIndex=@names.length-1
|
51
|
+
|
52
|
+
# Post processing:
|
110
53
|
# Close the file as we are finished reading it:
|
111
54
|
@file.close()
|
112
|
-
#
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
55
|
+
# Assume file has been read successfully:
|
56
|
+
@success = true
|
57
|
+
# Check if the last tag was read out correctly (that the length of its data (@raw.last.length)
|
58
|
+
# corresponds to that expected by the length specified in the DICOM file (@lengths.last)).
|
59
|
+
# We only run this test if the last tag has a positive expectation value, obviously.
|
60
|
+
if @lengths.last.to_i > 0
|
61
|
+
if @raw.last.length != @lengths.last
|
62
|
+
@msg += ["Error! The data content read from file does not match the length specified for the tag #{@labels.last}. It seems this is either an invalid or corrupt DICOM file. Returning."]
|
63
|
+
@success = false
|
64
|
+
return
|
65
|
+
end
|
117
66
|
end
|
118
|
-
end
|
67
|
+
end # of method initialize
|
119
68
|
|
120
69
|
|
121
|
-
#
|
122
|
-
|
123
|
-
return [@names,@labels,@types,@lengths,@values,@raw,@levels,@compression,@color,@explicit, @file_endian, @msg]
|
124
|
-
end
|
70
|
+
# Following methods are private:
|
71
|
+
private
|
125
72
|
|
126
73
|
|
127
74
|
# Checks the initial header of the DICOM file.
|
128
75
|
def check_header()
|
129
76
|
# According to the official DICOM standard, a DICOM file shall contain 128
|
130
|
-
# consequtive zero bytes followed by 4 bytes that spell the string 'DICM'.
|
77
|
+
# consequtive (zero) bytes followed by 4 bytes that spell the string 'DICM'.
|
131
78
|
# Apparently, some providers seems to skip this in their DICOM files.
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@header_length += 128
|
136
|
-
rescue
|
137
|
-
# The file could not be read. Most likely because the file name variable supplied to this instance was in fact a directory.
|
138
|
-
return nil
|
139
|
-
end
|
140
|
-
str_header1=bin1.unpack('a' * 128).to_s
|
79
|
+
bin1 = @file.read(128)
|
80
|
+
@header_length += 128
|
81
|
+
#filler = bin1.unpack('a' * 128).to_s
|
141
82
|
# Next 4 bytes should spell 'DICM':
|
142
|
-
bin2
|
83
|
+
bin2 = @file.read(4)
|
143
84
|
@header_length += 4
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
@msg+=["Warning: The specified file does not contain the official DICOM header."]
|
148
|
-
|
149
|
-
#
|
150
|
-
|
151
|
-
# Check for skipped group 0002:
|
152
|
-
group_label=bin1.unpack('h4').to_s.reverse.upcase
|
153
|
-
if (group_label.include? "2")
|
154
|
-
#Assume the file starts with a group 0002 tag, as "normal".
|
155
|
-
# Assume a default transfer syntax: Implicit, Little Endian.
|
156
|
-
@explicit = false
|
157
|
-
@rest_explicit = false
|
158
|
-
@file_endian = false
|
159
|
-
@rest_endian = false
|
160
|
-
@compression = false
|
161
|
-
else
|
162
|
-
# Assume a default transfer syntax: Implicit, Little Endian.
|
163
|
-
# (Turns out I use the same settings as above, which makes this somewhat silly, but I'll leave it like this for now in case of any changes later)
|
164
|
-
@explicit = false
|
165
|
-
@rest_explicit = false
|
166
|
-
@file_endian = false
|
167
|
-
@rest_endian = false
|
168
|
-
@compression = false
|
169
|
-
@msg+=["Warning: Group '0002' Transfer Syntax does not exist. Assuming Implicit VR, Little Endian."]
|
170
|
-
end
|
85
|
+
dicm = bin2.unpack('a' * 4).to_s
|
86
|
+
if dicm != 'DICM' then
|
87
|
+
# Header is not valid (we will still try to read it is a DICOM file though):
|
88
|
+
@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."]
|
89
|
+
# As the file is not conforming to the DICOM standard, it is possible that it does not contain a
|
90
|
+
# transfer syntax tag, and as such, we attempt to choose the most probable encoding values here:
|
91
|
+
@explicit = false
|
171
92
|
return false
|
172
93
|
else
|
94
|
+
# Header is valid:
|
173
95
|
return true
|
174
96
|
end
|
175
|
-
end
|
176
|
-
|
177
|
-
|
178
|
-
# Checks the status of the pixel data that has been read from the DICOM file: whether it exists at all and if its greyscale or color.
|
179
|
-
# Modifies instance variable @color if color image is detected and instance variable @compression if no pixel data is detected.
|
180
|
-
def check_pixel_status()
|
181
|
-
# Check if pixel data is present:
|
182
|
-
pixel_pos = @labels.index("7FE0,0010")
|
183
|
-
if pixel_pos == nil
|
184
|
-
@compression = nil
|
185
|
-
return
|
186
|
-
end
|
187
|
-
# Check for color image:
|
188
|
-
col_string = get_value("0028,0004")
|
189
|
-
if col_string != false
|
190
|
-
if (col_string.include? "RGB") or (col_string.include? "COLOR") or (col_string.include? "COLOUR")
|
191
|
-
@color = true
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
97
|
+
end # of method check_header
|
195
98
|
|
196
99
|
|
197
100
|
# Governs the process of reading tags in the DICOM file.
|
@@ -199,106 +102,73 @@ module DICOM
|
|
199
102
|
#going on here in all cases. Perhaps some day I will get the courage to have a go at it again.)
|
200
103
|
def process_tag()
|
201
104
|
#STEP 1: ------------------------------------------------------
|
202
|
-
#
|
203
|
-
label=read_label()
|
105
|
+
# Attempt to read tag label, but abort if we have reached end of file:
|
106
|
+
label = read_label()
|
204
107
|
if label == false
|
108
|
+
# End of file, no more tags.
|
205
109
|
return false
|
206
|
-
end
|
207
|
-
#
|
110
|
+
end
|
111
|
+
# STEP 2: ------------------------------------------------------
|
112
|
+
# Access library to retrieve the tag name and VR from the label we have read:
|
208
113
|
lib_data = @lib.get_name_vr(label)
|
209
114
|
name = lib_data[0]
|
210
115
|
vr = lib_data[1]
|
211
|
-
if
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
end
|
216
|
-
# STEP 2: ----------------------------------------------------
|
217
|
-
# Continue reading the tag information: Byte type and length.
|
116
|
+
# (Note: VR will be overwritten if the DICOM file contains VR)
|
117
|
+
|
118
|
+
# STEP 3: ----------------------------------------------------
|
119
|
+
# Read tag VR (if it exists) and the length value:
|
218
120
|
tag_info = read_type_length(vr,label)
|
219
121
|
type = tag_info[0]
|
122
|
+
level_type = type
|
220
123
|
length = tag_info[1]
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
name = "Encapsulated information"
|
241
|
-
end
|
242
|
-
# Set hiearchy level:
|
243
|
-
set_level(type, length, label)
|
244
|
-
return [name,label,type,length,data]
|
245
|
-
end
|
246
|
-
# Add the length of the content of the tag to the last element in the integrated_lengths array:
|
247
|
-
# (but not if it is a sequence or item, as in this case the length of the tag is its sub-tags)
|
248
|
-
if length.to_i != 0 and type != "SQ" and type != "()"
|
249
|
-
@integrated_lengths[@integrated_lengths.size-1] += length
|
250
|
-
end
|
251
|
-
# Set hiearchy level:
|
252
|
-
set_level(type, length, label)
|
253
|
-
# Some special handling for item related tags, which may result in returning without reading data:
|
254
|
-
if type == "()"
|
255
|
-
# If length is zero, just return:
|
256
|
-
if length == 0
|
257
|
-
type = ""
|
258
|
-
data = nil
|
259
|
-
@data_length = 0
|
260
|
-
return [name,label,type,length,data]
|
261
|
-
else
|
262
|
-
# If there is content, this may, in the case of an image, be the image data.
|
263
|
-
# Must insert the image's type here.
|
264
|
-
# Some times when this tag has a length, it does not have content in itself, but instead
|
265
|
-
# have content in a number of subtags.
|
266
|
-
if @sq_length != true
|
267
|
-
# Treat the item as containing image data:
|
268
|
-
type = "OW" # A more general approach should be implemented here.
|
269
|
-
# For this special case, where item contains the data itself, instead of in sub-tags,
|
270
|
-
# we declare that there is to be no sub-level after all.
|
271
|
-
# This handling is not particularly obvious or elegant, and perhaps in the future I will
|
272
|
-
# be able to rewrite this whole process_tag method to something more sane.
|
273
|
-
@current_level = @current_level - 1
|
274
|
-
end
|
275
|
-
end
|
276
|
-
end
|
277
|
-
# STEP 3: ----------------------------------------
|
278
|
-
# Finally read the tag data.
|
279
|
-
tag_data = read_data(type,length)
|
280
|
-
value = tag_data[0]
|
281
|
-
raw = tag_data[1]
|
282
|
-
# Check for the Transfer Syntax UID tag, and process it:
|
283
|
-
if label == "0002,0010"
|
284
|
-
process_syntax(value)
|
285
|
-
end
|
286
|
-
if type == "SQ" or type == "()"
|
287
|
-
@data_length = length # To avoid false errors. In time perhaps a better way of handling this will be found.
|
124
|
+
|
125
|
+
# STEP 4: ----------------------------------------
|
126
|
+
# Reading value of tag.
|
127
|
+
# Special handling needed for items in encapsulated image data:
|
128
|
+
if @enc_image and label == "FFFE,E000"
|
129
|
+
# The first item appearing after the image tag is a 'normal' item, the rest hold image data.
|
130
|
+
# Note that the first item will contain data if there are multiple images, and so must be read.
|
131
|
+
type = "OW" # how about alternatives like OB?
|
132
|
+
# Modify name of item if this is an item that holds pixel data:
|
133
|
+
if @labels.last != "7FE0,0010"
|
134
|
+
name = "Pixel Data Item"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
# Read the value of the tag (if it contains data, and it is not a sequence or ordinary item):
|
138
|
+
if length.to_i > 0 and type != "SQ" and type != "()"
|
139
|
+
# Read the tag data:
|
140
|
+
tag_data = read_data(type,length)
|
141
|
+
value = tag_data[0]
|
142
|
+
raw = tag_data[1]
|
288
143
|
else
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
144
|
+
# No tag data.
|
145
|
+
# Special case: Check if pixel data tag is sequenced:
|
146
|
+
if label == "7FE0,0010"
|
147
|
+
# Change name and type of pixel data tag if it does not contain data itself:
|
148
|
+
name = "Encapsulated Pixel Data"
|
149
|
+
level_type = "SQ"
|
150
|
+
@enc_image = true
|
151
|
+
end
|
152
|
+
end # of if length.to_i > 0
|
153
|
+
# Set the hiearchy level of this tag:
|
154
|
+
set_level(level_type, length, label, name)
|
155
|
+
# Transfer the gathered data to arrays and return true:
|
156
|
+
@names += [name]
|
157
|
+
@labels += [label]
|
158
|
+
@types += [type]
|
159
|
+
@lengths += [length]
|
160
|
+
@values += [value]
|
161
|
+
@raw += [raw]
|
162
|
+
return true
|
163
|
+
end # of method process_tag
|
294
164
|
|
295
165
|
|
296
166
|
# Reads and returns TAG LABEL (4 first bytes of tag).
|
297
167
|
def read_label()
|
298
|
-
bin1
|
299
|
-
bin2
|
300
|
-
#
|
301
|
-
if
|
168
|
+
bin1 = @file.read(2)
|
169
|
+
bin2 = @file.read(2)
|
170
|
+
# Do not proceed if we have reached end of file:
|
171
|
+
if bin2 == nil
|
302
172
|
return false
|
303
173
|
end
|
304
174
|
# Add the length of the tag label. If this was the first label read from file, we need to add the header length too:
|
@@ -310,236 +180,195 @@ module DICOM
|
|
310
180
|
@integrated_lengths += [@integrated_lengths[@integrated_lengths.length-1] + 4]
|
311
181
|
end
|
312
182
|
# Unpack the blobs:
|
313
|
-
label1=bin1.unpack('h*').to_s.reverse.upcase
|
314
|
-
label2=bin2.unpack('h*').to_s.reverse.upcase
|
315
|
-
#
|
183
|
+
label1 = bin1.unpack('h*').to_s.reverse.upcase
|
184
|
+
label2 = bin2.unpack('h*').to_s.reverse.upcase
|
185
|
+
# Whether DICOM file is big or little endian, the first 0002 group is always little endian encoded.
|
186
|
+
# In case of big endian system:
|
316
187
|
if @sys_endian
|
317
|
-
# Rearrange the numbers:
|
188
|
+
# Rearrange the numbers (# This has never been tested btw.):
|
318
189
|
label1 = label1[2..3]+label1[0..1]
|
319
190
|
label2 = label2[2..3]+label2[0..1]
|
320
|
-
# Has this been verified? Suspect unintended consequence.
|
321
191
|
end
|
322
|
-
#
|
323
|
-
if label1 != "0002"
|
324
|
-
|
325
|
-
@file_endian = @rest_endian
|
326
|
-
@explicit = @rest_explicit
|
327
|
-
#Update the endian-relationship variable:
|
328
|
-
if @sys_endian == @file_endian
|
329
|
-
@endian = true
|
330
|
-
else
|
331
|
-
@endian = false
|
332
|
-
end
|
333
|
-
# Do we need to rearrange?
|
334
|
-
if @endian
|
335
|
-
# No action needed
|
336
|
-
else
|
337
|
-
# Need to rearrange the first and second part of each string:
|
338
|
-
label1 = label1[2..3]+label1[0..1]
|
339
|
-
label2 = label2[2..3]+label2[0..1]
|
340
|
-
end
|
192
|
+
# When we shift from group 0002 to another group we need to update our endian/explicitness variables:
|
193
|
+
if label1 != "0002" and @switched == false
|
194
|
+
switch_syntax()
|
341
195
|
end
|
342
|
-
#
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
196
|
+
# Perhaps we need to rearrange the labels?:
|
197
|
+
if not @endian
|
198
|
+
# Need to rearrange the first and second part of each string:
|
199
|
+
label1 = label1[2..3]+label1[0..1]
|
200
|
+
label2 = label2[2..3]+label2[0..1]
|
201
|
+
end
|
202
|
+
# Join the label group and label element together to the final string and return:
|
203
|
+
return label1+","+label2
|
204
|
+
end # of method read_label
|
348
205
|
|
349
206
|
|
350
207
|
# Reads and returns TAG TYPE (2 bytes) and TAG LENGTH (Varying length).
|
351
208
|
def read_type_length(type,label)
|
352
|
-
# Structure will differ, dependent on whether we have explicit or implicit
|
353
|
-
# EXPLICIT
|
209
|
+
# Structure will differ, dependent on whether we have explicit or implicit encoding:
|
210
|
+
# *****EXPLICIT*****:
|
354
211
|
if @explicit == true
|
355
|
-
#
|
356
|
-
|
357
|
-
bin=@file.read(4)
|
358
|
-
@integrated_lengths[@integrated_lengths.length-1] += 4
|
359
|
-
length = get_SL(bin)
|
360
|
-
else
|
212
|
+
# Step 1: Read VR (if it exists)
|
213
|
+
unless label == "FFFE,E000" or label == "FFFE,E00D" or label == "FFFE,E0DD"
|
361
214
|
# Read tag type field (2 bytes - since we are not dealing with an item related tag):
|
362
|
-
bin
|
215
|
+
bin = @file.read(2)
|
363
216
|
@integrated_lengths[@integrated_lengths.length-1] += 2
|
364
|
-
type=bin.unpack('a*').to_s
|
217
|
+
type = bin.unpack('a*').to_s
|
365
218
|
end
|
366
|
-
#
|
219
|
+
# Step 2: Read length
|
220
|
+
# Three possible structures for value length here, dependent on tag type:
|
367
221
|
case type
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
222
|
+
when "OB","OW","SQ","UN"
|
223
|
+
# 6 bytes total:
|
224
|
+
# Two empty first:
|
225
|
+
bin = @file.read(2)
|
226
|
+
@integrated_lengths[@integrated_lengths.length-1] += 2
|
227
|
+
# Value length (4 bytes):
|
228
|
+
bin = @file.read(4)
|
229
|
+
@integrated_lengths[@integrated_lengths.length-1] += 4
|
230
|
+
length = bin.unpack(@ul)[0]
|
231
|
+
when "()"
|
232
|
+
# 4 bytes:
|
233
|
+
# For labels "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
|
234
|
+
bin = @file.read(4)
|
235
|
+
@integrated_lengths[@integrated_lengths.length-1] += 4
|
236
|
+
length = bin.unpack(@ul)[0]
|
237
|
+
else
|
238
|
+
# 2 bytes:
|
239
|
+
# For all the other tag types, value length is 2 bytes:
|
240
|
+
bin = @file.read(2)
|
241
|
+
@integrated_lengths[@integrated_lengths.length-1] += 2
|
242
|
+
length = bin.unpack(@us)[0]
|
383
243
|
end
|
384
244
|
else
|
385
|
-
#IMPLICIT
|
386
|
-
#
|
387
|
-
|
245
|
+
# *****IMPLICIT*****:
|
246
|
+
# No VR (retrieved from library based on the tag's label)
|
247
|
+
# Reading value length (4 bytes):
|
248
|
+
bin = @file.read(4)
|
388
249
|
@integrated_lengths[@integrated_lengths.length-1] += 4
|
389
|
-
length =
|
250
|
+
length = bin.unpack(@ul)[0]
|
390
251
|
end
|
391
252
|
# For encapsulated data, the tag length will not be defined. To convey this,
|
392
|
-
# the hex sequence 'ff ff ff ff' is used (-1 converted to signed long).
|
393
|
-
if length ==
|
394
|
-
length =
|
253
|
+
# the hex sequence 'ff ff ff ff' is used (-1 converted to signed long, 4294967295 converted to unsigned long).
|
254
|
+
if length == 4294967295
|
255
|
+
length = @undef
|
395
256
|
elsif length%2 >0
|
396
257
|
# According to the DICOM standard, all tag lengths should be an even number.
|
397
258
|
# If it is not, it may indicate a file that is not standards compliant or it might even not be a DICOM file.
|
398
|
-
@msg+=["Warning: Odd number of bytes in tag length occured. This is a violation of the DICOM standard, but program will attempt to
|
259
|
+
@msg += ["Warning: Odd number of bytes in tag length occured. This is a violation of the DICOM standard, but program will still attempt to read the rest of the file."]
|
399
260
|
end
|
400
|
-
return [type,length]
|
401
|
-
end
|
402
|
-
# END BYTE TYPE and TAG LENGTH
|
261
|
+
return [type, length]
|
262
|
+
end # of method read_type_length
|
403
263
|
|
404
264
|
|
405
|
-
# Reads and returns TAG DATA (
|
265
|
+
# Reads and returns TAG DATA (Of varying length - which is determined at an earlier stage).
|
406
266
|
def read_data(type, length)
|
407
|
-
#
|
267
|
+
# Read the data:
|
268
|
+
bin = @file.read(length)
|
269
|
+
@integrated_lengths[@integrated_lengths.size-1] += length
|
270
|
+
# Decoding of content will naturally depend on what kind of content (VR) we have.
|
408
271
|
case type
|
409
272
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
end
|
421
|
-
|
422
|
-
# Signed long: (4 bytes)
|
423
|
-
when "SL"
|
424
|
-
bin = @file.read(length)
|
425
|
-
if length <= 4
|
426
|
-
data = get_SL(bin)
|
427
|
-
else
|
428
|
-
data = process_numbers(length, type, bin)
|
429
|
-
end
|
273
|
+
# Normally the "number tags" will contain just one number, but in some cases,
|
274
|
+
# they contain multiple numbers. In such cases we will read each number and store
|
275
|
+
# them all in a string separated by "/".
|
276
|
+
# Unsigned long: (4 bytes)
|
277
|
+
when "UL"
|
278
|
+
if length <= 4
|
279
|
+
data = bin.unpack(@ul)[0]
|
280
|
+
else
|
281
|
+
data = bin.unpack(@ul).join("/")
|
282
|
+
end
|
430
283
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
end
|
284
|
+
# Signed long: (4 bytes)
|
285
|
+
when "SL"
|
286
|
+
if length <= 4
|
287
|
+
data = bin.unpack(@sl)[0]
|
288
|
+
else
|
289
|
+
data = bin.unpack(@sl).join("/")
|
290
|
+
end
|
439
291
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
end
|
292
|
+
# Unsigned short: (2 bytes)
|
293
|
+
when "US"
|
294
|
+
if length <= 2
|
295
|
+
data = bin.unpack(@us)[0]
|
296
|
+
else
|
297
|
+
data = bin.unpack(@us).join("/")
|
298
|
+
end
|
448
299
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
end
|
300
|
+
# Signed short: (2 bytes)
|
301
|
+
when "SS"
|
302
|
+
if length <= 2
|
303
|
+
data = bin.unpack(@ss)[0]
|
304
|
+
else
|
305
|
+
data = bin.unpack(@ss).join("/")
|
306
|
+
end
|
457
307
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
# The tag has no content in itself, the file starts directly on a new tag adress.
|
466
|
-
data="(Sequence of Elements)"
|
467
|
-
|
468
|
-
# Item tag:
|
469
|
-
when "()"
|
470
|
-
# Tag may have a length, but no content belonging to this tag itself. They are to be read
|
471
|
-
# for this item's subtags.
|
472
|
-
data = "(Sequence of Tags)"
|
473
|
-
|
474
|
-
# The tag contains a tag adress (4 bytes):
|
475
|
-
when "AT"
|
476
|
-
if length != 4
|
477
|
-
@msg+=["Warning: Unexpected tag length, expected 4 bytes for tag type 'AT'!"]
|
478
|
-
end
|
479
|
-
temp=Array.new(4)
|
480
|
-
4.times do |i|
|
481
|
-
bin=@file.read(1)
|
482
|
-
temp[i]=bin.unpack('H*')[0]
|
483
|
-
end
|
484
|
-
# Put together, mix up the order to get it correct:
|
485
|
-
data=temp[1].to_s+temp[0].to_s+"."+temp[3].to_s+temp[2].to_s
|
486
|
-
# This has not been tested with other than Little endian system/file:
|
487
|
-
if @file_endian or @system_endian
|
488
|
-
@msg+=["Warning: Handling for tag type 'AT' has not been verified for other than default endianness."]
|
489
|
-
end
|
308
|
+
# Floating point single: (4 bytes)
|
309
|
+
when "FL"
|
310
|
+
if length <= 4
|
311
|
+
data = bin.unpack(@fs)[0]
|
312
|
+
else
|
313
|
+
data = bin.unpack(@fs).join("/")
|
314
|
+
end
|
490
315
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
when "OW"
|
498
|
-
# We need to know what kind of bith depth the pixel data is saved with:
|
499
|
-
bit_depth=get_value('0028,0100')
|
500
|
-
# Proceed to read the image binary data:
|
501
|
-
bin=@file.read(length)
|
502
|
-
# Number of bytes used per pixel will determine how to unpack this:
|
503
|
-
case bit_depth
|
504
|
-
when 8
|
505
|
-
data=get_BYTE(bin) # Byte/Character/Fixnum (1 byte)
|
506
|
-
when 16
|
507
|
-
data=get_US(bin) # Unsigned short (2 bytes)
|
508
|
-
when 12
|
509
|
-
# 12 BIT SIMPLY NOT WORKING YET!
|
510
|
-
# This one is a bit more tricky to extract.
|
511
|
-
# I havent really given this priority so far as 12 bit image data is rather rare.
|
512
|
-
@msg+=["Warning: Bit depth 12 is not working correctly at this time!"]
|
513
|
-
data=Array.new(length)
|
514
|
-
(length).times do |i|
|
515
|
-
hex=bin.unpack('H3')
|
516
|
-
hex4="0"+hex[0]
|
517
|
-
num=hex[0].unpack('v')
|
518
|
-
data[i]=num
|
316
|
+
# Floating point double: (8 bytes)
|
317
|
+
when "FD"
|
318
|
+
if length <= 8
|
319
|
+
data = bin.unpack(@fd)[0]
|
320
|
+
else
|
321
|
+
data = bin.unpack(@fd).join("/")
|
519
322
|
end
|
520
|
-
else
|
521
|
-
raise "Bit depth "+bit_depth.to_s+" has not received implementation in this procedure yet."
|
522
|
-
end
|
523
323
|
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
324
|
+
# The tag contains a tag label (4 bytes):
|
325
|
+
when "AT"
|
326
|
+
# Bytes read in following order: 1 0 , 3 2 (And Hex nibbles read in this order: Hh)
|
327
|
+
# NB! This probably needs to be modified when dealing with something other than little endian.
|
328
|
+
# Tag label is unpacked to a string in the format GGGGEEEE.
|
329
|
+
data = (bin.unpack("xHXhX2HXh").join + bin.unpack("x3HXhX2HXh").join).upcase
|
330
|
+
#data = (bin.unpack("xHXhX2HXh").join + "," + bin.unpack("x3HXhX2HXh").join).upcase
|
331
|
+
|
332
|
+
# We have a number of VRs that are decoded as string:
|
333
|
+
when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT' #,'VR'
|
334
|
+
data = bin.unpack('a*').to_s
|
335
|
+
|
336
|
+
# NB!
|
337
|
+
# FOLLOWING TAG TYPES WILL NOT BE DECODED.
|
338
|
+
# DECODING OF PIXEL DATA IS MOVED TO DOBJECT FOR PERFORMANCE REASONS.
|
339
|
+
|
340
|
+
# Unknown information, header element is not recognised from local database:
|
341
|
+
when "UN"
|
342
|
+
#data=bin.unpack('H*')[0]
|
343
|
+
|
344
|
+
# Other byte string, 1-byte integers
|
345
|
+
when "OB"
|
346
|
+
#data = bin.unpack('H*')[0]
|
347
|
+
|
348
|
+
# Other float string, 4-byte floating point numbers
|
349
|
+
when "OF"
|
350
|
+
# NB! This tag type has not been tested yet with an actual DICOM file.
|
351
|
+
#data = bin.unpack(@fs)
|
352
|
+
|
353
|
+
# Image data:
|
354
|
+
# Other word string, 2-byte integers
|
355
|
+
when "OW"
|
356
|
+
# empty
|
357
|
+
|
358
|
+
# Unknown VR:
|
359
|
+
else
|
360
|
+
@msg += ["Warning: Tag type #{type} does not have a reading method assigned to it. Please contact the author."]
|
361
|
+
#data = bin.unpack('H*')[0]
|
362
|
+
end # of case type
|
533
363
|
|
534
364
|
# Return the data:
|
535
|
-
return [data,bin]
|
536
|
-
end
|
537
|
-
# END TAG DATA
|
365
|
+
return [data, bin]
|
366
|
+
end # of method read_data
|
538
367
|
|
539
368
|
|
540
369
|
# Sets the level of the current tag in the hiearchy.
|
541
370
|
# The default (top) level is zero.
|
542
|
-
def set_level(type, length, label)
|
371
|
+
def set_level(type, length, label, name)
|
543
372
|
# Set the level of this tag:
|
544
373
|
@levels += [@current_level]
|
545
374
|
# Determine if there is a level change for the following tag:
|
@@ -550,7 +379,7 @@ module DICOM
|
|
550
379
|
# not in its sub-tags, we should not increase the level. (This is fixed in the process_tag method.)
|
551
380
|
if type == "SQ"
|
552
381
|
increase = true
|
553
|
-
elsif
|
382
|
+
elsif name == "Item"
|
554
383
|
increase = true
|
555
384
|
else
|
556
385
|
increase = false
|
@@ -574,9 +403,11 @@ module DICOM
|
|
574
403
|
# If it is an array (of length and position), then we need to check the integrated_lengths array
|
575
404
|
# to see if the current sub-level has expired.
|
576
405
|
if @hierarchy.size > 0
|
577
|
-
|
406
|
+
# Do not perform this check for Pixel Data Items or Sequence Delimitation Items:
|
407
|
+
# (If performed, it will give false errors for the case when we have Encapsulated Pixel Data)
|
408
|
+
check_level_end() unless name == "Pixel Data Item" or label == "FFFE,E0DD"
|
578
409
|
end
|
579
|
-
end
|
410
|
+
end # of method set_level
|
580
411
|
|
581
412
|
|
582
413
|
# Checks how far we've read in the DICOM file to determine if we have reached a point
|
@@ -607,266 +438,166 @@ module DICOM
|
|
607
438
|
end
|
608
439
|
end
|
609
440
|
end
|
610
|
-
end
|
441
|
+
end # of method check_level_end
|
611
442
|
|
612
443
|
|
613
|
-
#
|
614
|
-
def
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
if (id.is_a? Integer)
|
624
|
-
if id >= 0 and id <= @lastIndex
|
625
|
-
# The id supplied is a valid position, return its corresponding value:
|
626
|
-
return @values[id]
|
444
|
+
# Tests if the file is readable and opens it.
|
445
|
+
def open_file(file)
|
446
|
+
if File.exist?(file)
|
447
|
+
if File.readable?(file)
|
448
|
+
if not File.directory?(file)
|
449
|
+
if File.size(file) > 8
|
450
|
+
@file = File.new(file, "rb")
|
451
|
+
else
|
452
|
+
@msg += ["Error! File is too small to contain DICOM information. Returning. (#{file})"]
|
453
|
+
end
|
627
454
|
else
|
628
|
-
|
455
|
+
@msg += ["Error! File is a directory. Returning. (#{file})"]
|
629
456
|
end
|
630
457
|
else
|
631
|
-
|
458
|
+
@msg += ["Error! File exists but I don't have permission to read it. Returning. (#{file})"]
|
632
459
|
end
|
633
460
|
else
|
634
|
-
|
635
|
-
return @values[pos]
|
461
|
+
@msg += ["Error! The file you have supplied does not exist. Returning. (#{file})"]
|
636
462
|
end
|
637
|
-
end
|
463
|
+
end # of method open_file
|
638
464
|
|
639
465
|
|
640
|
-
#
|
641
|
-
def
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
when "SS"
|
652
|
-
temp1 = get_SS(bin)
|
653
|
-
when "FD"
|
654
|
-
temp1 = get_FD(bin)
|
655
|
-
else
|
656
|
-
@msg+=["Warning: Type "+type+"not supported in method process_numbers()."]
|
657
|
-
end
|
658
|
-
remain = (length-size)/size
|
659
|
-
remain.times do
|
660
|
-
bin = @file.read(size)
|
661
|
-
case type
|
662
|
-
when "UL"
|
663
|
-
temp2 = get_UL(bin)
|
664
|
-
when "SL"
|
665
|
-
temp2 = get_SL(bin)
|
666
|
-
when "US"
|
667
|
-
temp2 = get_US(bin)
|
668
|
-
when "SS"
|
669
|
-
temp2 = get_SS(bin)
|
670
|
-
when "FD"
|
671
|
-
temp2 = get_FD(bin)
|
672
|
-
else
|
673
|
-
@msg+=["Warning: Type "+type+"not supported in method process_numbers()."]
|
674
|
-
end
|
675
|
-
data = temp1.to_s+"/"+temp2.to_s
|
676
|
-
temp1 = data
|
677
|
-
end
|
678
|
-
return data
|
679
|
-
end
|
680
|
-
|
681
|
-
# Returns a byte integer (1 byte), from the supplied variable.
|
682
|
-
def get_BYTE(bin)
|
683
|
-
# If bin contains several numbers, unpack and return in an array. If just one number, return the number:
|
684
|
-
elements = bin.size
|
685
|
-
if @endian
|
686
|
-
# Native byte order:
|
687
|
-
if elements > 1
|
688
|
-
num=bin.unpack('C*')
|
689
|
-
else
|
690
|
-
num=bin.unpack('C*')[0]
|
691
|
-
end
|
692
|
-
else
|
693
|
-
# Network byte order: (Unknown what to use here)
|
694
|
-
@msg+=["Warning: Method get_BYTE not tested with this endian yet!"]
|
695
|
-
if elements > 1
|
696
|
-
num=bin.unpack('C*')
|
697
|
-
else
|
698
|
-
num=bin.unpack('C*')[0]
|
699
|
-
end
|
700
|
-
end
|
701
|
-
return num
|
702
|
-
end
|
703
|
-
|
704
|
-
|
705
|
-
# Returns a unsigned short (2 bytes), from the supplied variable.
|
706
|
-
def get_US(bin)
|
707
|
-
# If bin contains several numbers, unpack and return in an array. If just one number, return the number:
|
708
|
-
elements = bin.size/2
|
709
|
-
if @endian
|
710
|
-
# Native byte order:
|
711
|
-
if elements > 1
|
712
|
-
num=bin.unpack('S*') # or v (little endian (?))
|
713
|
-
else
|
714
|
-
num=bin.unpack('S*')[0]
|
715
|
-
end
|
716
|
-
else
|
717
|
-
# Network byte order:
|
718
|
-
if elements > 1
|
719
|
-
num=bin.unpack('n*')
|
720
|
-
else
|
721
|
-
num=bin.unpack('n*')[0]
|
722
|
-
end
|
723
|
-
end
|
724
|
-
return num
|
725
|
-
end
|
726
|
-
|
727
|
-
|
728
|
-
# Returns a signed short (2 bytes), from the supplied variable.
|
729
|
-
def get_SS(bin)
|
730
|
-
elements = bin.size/2
|
731
|
-
# If bin contains several numbers, unpack and return in an array. If just one number, return the number:
|
732
|
-
if @endian
|
733
|
-
# Native byte order
|
734
|
-
if elements > 1
|
735
|
-
num=bin.unpack('s*')
|
736
|
-
else
|
737
|
-
num=bin.unpack('s*')[0]
|
738
|
-
end
|
739
|
-
else
|
740
|
-
# Unknown what unpack code to use here:
|
741
|
-
if elements > 1
|
742
|
-
num=bin.unpack('s*')
|
743
|
-
else
|
744
|
-
num=bin.unpack('s*')[0]
|
745
|
-
end
|
746
|
-
@msg+=["Warning: Oppositve endian for signed short is not working yet!"]
|
747
|
-
end
|
748
|
-
return num
|
749
|
-
end
|
750
|
-
|
751
|
-
|
752
|
-
# Returns an unsigned long (4 bytes), from the supplied variable.
|
753
|
-
def get_UL(bin)
|
754
|
-
elements = bin.size/4
|
755
|
-
# If bin contains several numbers, unpack and return in an array. If just one number, return the number:
|
756
|
-
if @endian
|
757
|
-
# Unsigned native integer:
|
758
|
-
if elements > 1
|
759
|
-
num=bin.unpack('I*')
|
760
|
-
else
|
761
|
-
num=bin.unpack('I*')[0]
|
762
|
-
end
|
466
|
+
# Changes encoding variables as the file reading proceeds past the initial 0002 group of the DICOM file.
|
467
|
+
def switch_syntax()
|
468
|
+
# The information read from the Transfer syntax tag (if present), needs to be processed:
|
469
|
+
process_transfer_syntax()
|
470
|
+
# We only plan to run this method once:
|
471
|
+
@switched = true
|
472
|
+
# Update endian, explicitness and unpack variables:
|
473
|
+
@file_endian = @rest_endian
|
474
|
+
@explicit = @rest_explicit
|
475
|
+
if @sys_endian == @file_endian
|
476
|
+
@endian = true
|
763
477
|
else
|
764
|
-
|
765
|
-
if elements > 1
|
766
|
-
num=bin.unpack('N*')
|
767
|
-
else
|
768
|
-
num=bin.unpack('N*')[0]
|
769
|
-
end
|
478
|
+
@endian = false
|
770
479
|
end
|
771
|
-
|
480
|
+
set_unpack_strings()
|
772
481
|
end
|
773
482
|
|
774
483
|
|
775
|
-
#
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
if
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
484
|
+
# Checks the Transfer Syntax UID tag and updates class variables to prepare for correct reading of DICOM file.
|
485
|
+
# A lot of code here is duplicated in DWrite class. Should move as much of this code as possible to DLibrary I think.
|
486
|
+
def process_transfer_syntax()
|
487
|
+
ts_pos = @labels.index("0002,0010")
|
488
|
+
if ts_pos != nil
|
489
|
+
ts_value = @raw[ts_pos].unpack('a*').to_s.rstrip
|
490
|
+
valid = @lib.check_ts_validity(ts_value)
|
491
|
+
if not valid
|
492
|
+
@msg+=["Warning: Invalid/unknown transfer syntax! Will try reading the file, but errors may occur."]
|
493
|
+
end
|
494
|
+
case ts_value
|
495
|
+
# Some variations with uncompressed pixel data:
|
496
|
+
when "1.2.840.10008.1.2"
|
497
|
+
# Implicit VR, Little Endian
|
498
|
+
@rest_explicit = false
|
499
|
+
@rest_endian = false
|
500
|
+
when "1.2.840.10008.1.2.1"
|
501
|
+
# Explicit VR, Little Endian
|
502
|
+
@rest_explicit = true
|
503
|
+
@rest_endian = false
|
504
|
+
when "1.2.840.10008.1.2.1.99"
|
505
|
+
# Deflated Explicit VR, Little Endian
|
506
|
+
@msg += ["Warning: Transfer syntax 'Deflated Explicit VR, Little Endian' is untested. Unknown if this is handled correctly!"]
|
507
|
+
@rest_explicit = true
|
508
|
+
@rest_endian = false
|
509
|
+
when "1.2.840.10008.1.2.2"
|
510
|
+
# Explicit VR, Big Endian
|
511
|
+
@rest_explicit = true
|
512
|
+
@rest_endian = true
|
513
|
+
else
|
514
|
+
# For everything else, assume compressed pixel data, with Explicit VR, Little Endian:
|
515
|
+
@rest_explicit = true
|
516
|
+
@rest_endian = false
|
517
|
+
end # of case ts_value
|
518
|
+
end # of if ts_pos != nil
|
519
|
+
end # of method process_syntax
|
796
520
|
|
797
521
|
|
798
|
-
#
|
799
|
-
def
|
800
|
-
elements = bin.size/8
|
801
|
-
# If bin contains several numbers, unpack and return in an array. If just one number, return the number:
|
522
|
+
# Sets the unpack format strings that will be used for numbers depending on endianness of file/system.
|
523
|
+
def set_unpack_strings
|
802
524
|
if @endian
|
803
|
-
#
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
525
|
+
# System endian equals file endian:
|
526
|
+
# Native byte order.
|
527
|
+
@by = "C*" # Byte (1 byte)
|
528
|
+
@us = "S*" # Unsigned short (2 bytes)
|
529
|
+
@ss = "s*" # Signed short (2 bytes)
|
530
|
+
@ul = "I*" # Unsigned long (4 bytes)
|
531
|
+
@sl = "l*" # Signed long (4 bytes)
|
532
|
+
@fs = "e*" # Floating point single (4 bytes)
|
533
|
+
@fd = "E*" # Floating point double ( 8 bytes)
|
809
534
|
else
|
810
|
-
#
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
535
|
+
# System endian not equal to file endian:
|
536
|
+
# Network byte order.
|
537
|
+
@by = "C*"
|
538
|
+
@us = "n*"
|
539
|
+
@ss = "n*" # Not correct (gives US)
|
540
|
+
@ul = "N*"
|
541
|
+
@sl = "N*" # Not correct (gives UL)
|
542
|
+
@fs = "g*"
|
543
|
+
@fd = "G*"
|
816
544
|
end
|
817
|
-
return num
|
818
545
|
end
|
819
546
|
|
820
547
|
|
821
|
-
#
|
822
|
-
def
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
548
|
+
# Initiates the variables that are used during file reading.
|
549
|
+
def init_variables()
|
550
|
+
# Variables that hold data that will be available to the DObject class.
|
551
|
+
# Arrays that will hold information from the tags of the DICOM file:
|
552
|
+
@names = Array.new()
|
553
|
+
@labels = Array.new()
|
554
|
+
@types = Array.new()
|
555
|
+
@lengths = Array.new()
|
556
|
+
@values = Array.new()
|
557
|
+
@raw = Array.new()
|
558
|
+
@levels = Array.new()
|
559
|
+
# Array that will holde any messages generated while reading the DICOM file:
|
560
|
+
@msg = Array.new()
|
561
|
+
# Variables that contain properties of the DICOM file:
|
562
|
+
# Variable to keep track of whether the image pixel data in this file are compressed or not, and if it exists at all:
|
563
|
+
# Default explicitness of start of DICOM file::
|
564
|
+
@explicit = true
|
565
|
+
# Default endianness of start of DICOM files is little endian:
|
566
|
+
@file_endian = false
|
567
|
+
# Variable used to tell whether file was read succesfully or not:
|
568
|
+
@success = false
|
569
|
+
|
570
|
+
# Variables used internally when reading through the DICOM file:
|
571
|
+
# Array for keeping track of how many bytes have been read from the file up to and including each tag:
|
572
|
+
# (This is necessary for tracking the hiearchy in some DICOM files)
|
573
|
+
@integrated_lengths = Array.new()
|
574
|
+
@header_length = 0
|
575
|
+
# Array to keep track of the hierarchy of tags (this will be used to determine when a sequence or item is finished):
|
576
|
+
@hierarchy = Array.new()
|
577
|
+
@hierarchy_error = false
|
578
|
+
# Explicitness of the remaining groups after the initial 0002 group:
|
579
|
+
@rest_explicit = false
|
580
|
+
# Endianness of the remaining groups after the first group:
|
581
|
+
@rest_endian = false
|
582
|
+
# When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
|
583
|
+
@switched = false
|
584
|
+
# Use a "relationship endian" variable to guide reading of file:
|
585
|
+
if @sys_endian == @file_endian
|
586
|
+
@endian = true
|
851
587
|
else
|
852
|
-
|
853
|
-
@rest_explicit = true
|
854
|
-
@rest_endian = false
|
855
|
-
@compression = true
|
588
|
+
@endian = false
|
856
589
|
end
|
590
|
+
# Set which format strings to use when unpacking numbers:
|
591
|
+
set_unpack_strings
|
592
|
+
# A length variable will be used at the end to check whether the last tag was read correctly, or whether the file endend unexpectedly:
|
593
|
+
@data_length = 0
|
594
|
+
# Keeping track of the tag level while reading through the file:
|
595
|
+
@current_level = 0
|
596
|
+
# This variable's string will be inserted as the length of items/sq that dont have a specified length:
|
597
|
+
@undef = "UNDEFINED"
|
598
|
+
# Items contained under the pixel data tag may contain data directly, so we need a variable to keep track of this:
|
599
|
+
@enc_image = false
|
857
600
|
end
|
858
601
|
|
859
|
-
|
860
|
-
|
861
|
-
def check_sys_endian()
|
862
|
-
x = 0xdeadbeef
|
863
|
-
endian_type = {
|
864
|
-
Array(x).pack("V*") => false, #:little
|
865
|
-
Array(x).pack("N*") => true #:big
|
866
|
-
}
|
867
|
-
return endian_type[Array(x).pack("L*")]
|
868
|
-
end
|
869
|
-
|
870
|
-
|
871
|
-
end # End of class.
|
872
|
-
end # End of module.
|
602
|
+
end # End of class
|
603
|
+
end # End of module
|