dicom 0.9.6 → 0.9.7
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.
- checksums.yaml +5 -13
- data/CHANGELOG.md +390 -376
- data/COPYING +674 -674
- data/Gemfile +2 -2
- data/Gemfile.lock +30 -28
- data/README.md +154 -152
- data/dicom.gemspec +30 -30
- data/lib/dicom/anonymizer.rb +677 -654
- data/lib/dicom/audit_trail.rb +109 -109
- data/lib/dicom/d_library.rb +269 -265
- data/lib/dicom/d_object.rb +465 -465
- data/lib/dicom/d_read.rb +21 -8
- data/lib/dicom/d_server.rb +329 -329
- data/lib/dicom/d_write.rb +355 -355
- data/lib/dicom/dictionary/elements.tsv +597 -86
- data/lib/dicom/dictionary/uids.tsv +4 -2
- data/lib/dicom/elemental_parent.rb +63 -63
- data/lib/dicom/extensions/array.rb +56 -56
- data/lib/dicom/extensions/hash.rb +30 -30
- data/lib/dicom/extensions/string.rb +125 -125
- data/lib/dicom/file_handler.rb +121 -121
- data/lib/dicom/general/constants.rb +210 -210
- data/lib/dicom/general/deprecated.rb +0 -320
- data/lib/dicom/general/logging.rb +155 -155
- data/lib/dicom/general/methods.rb +98 -82
- data/lib/dicom/general/variables.rb +28 -28
- data/lib/dicom/general/version.rb +5 -5
- data/lib/dicom/image_item.rb +836 -836
- data/lib/dicom/image_processor.rb +79 -79
- data/lib/dicom/image_processor_mini_magick.rb +71 -71
- data/lib/dicom/image_processor_r_magick.rb +106 -106
- data/lib/dicom/link.rb +1529 -1528
- data/rakefile.rb +29 -30
- metadata +43 -49
data/lib/dicom/d_object.rb
CHANGED
@@ -1,465 +1,465 @@
|
|
1
|
-
# Copyright 2008-
|
2
|
-
#
|
3
|
-
# This program is free software: you can redistribute it and/or modify
|
4
|
-
# it under the terms of the GNU General Public License as published by
|
5
|
-
# the Free Software Foundation, either version 3 of the License, or
|
6
|
-
# (at your option) any later version.
|
7
|
-
#
|
8
|
-
# This program is distributed in the hope that it will be useful,
|
9
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
-
# GNU General Public License for more details.
|
12
|
-
#
|
13
|
-
# You should have received a copy of the GNU General Public License
|
14
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
-
#
|
16
|
-
module DICOM
|
17
|
-
|
18
|
-
# The DObject class is the main class for interacting with the DICOM object.
|
19
|
-
# Reading from and writing to files is executed from instances of this class.
|
20
|
-
#
|
21
|
-
# === Inheritance
|
22
|
-
#
|
23
|
-
# As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class,
|
24
|
-
# all ImageItem and Parent methods are also available to instances of DObject.
|
25
|
-
#
|
26
|
-
class DObject < ImageItem
|
27
|
-
include Logging
|
28
|
-
|
29
|
-
# An attribute set as nil. This attribute is included to provide consistency with the other element types which usually have a parent defined.
|
30
|
-
attr_reader :parent
|
31
|
-
# A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
|
32
|
-
attr_accessor :read_success
|
33
|
-
# The source of the DObject (nil, :str or file name string).
|
34
|
-
attr_accessor :source
|
35
|
-
# The Stream instance associated with this DObject instance (this attribute is mostly used internally).
|
36
|
-
attr_reader :stream
|
37
|
-
# An attribute (used by e.g. DICOM.load) to indicate that a DObject-type instance was given to the load method (instead of e.g. a file).
|
38
|
-
attr_accessor :was_dcm_on_input
|
39
|
-
# A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
|
40
|
-
attr_reader :write_success
|
41
|
-
|
42
|
-
alias_method :read?, :read_success
|
43
|
-
alias_method :written?, :write_success
|
44
|
-
|
45
|
-
# Creates a DObject instance by downloading a DICOM file
|
46
|
-
# specified by a hyperlink, and parsing the retrieved file.
|
47
|
-
#
|
48
|
-
# @note Highly experimental and un-tested!
|
49
|
-
# @note Designed for the HTTP protocol only.
|
50
|
-
# @note Whether this method should be included or removed from ruby-dicom is up for debate.
|
51
|
-
#
|
52
|
-
# @param [String] link a hyperlink string which specifies remote location of the DICOM file to be loaded
|
53
|
-
# @return [DObject] the created DObject instance
|
54
|
-
#
|
55
|
-
def self.get(link)
|
56
|
-
raise ArgumentError, "Invalid argument 'link'. Expected String, got #{link.class}." unless link.is_a?(String)
|
57
|
-
raise ArgumentError, "Invalid argument 'link'. Expected a string starting with 'http', got #{link}." unless link.index('http') == 0
|
58
|
-
require 'open-uri'
|
59
|
-
bin = nil
|
60
|
-
file = nil
|
61
|
-
# Try to open the remote file using open-uri:
|
62
|
-
retrials = 0
|
63
|
-
begin
|
64
|
-
file = open(link, 'rb') # binary encoding (ASCII-8BIT)
|
65
|
-
rescue Exception => e
|
66
|
-
if retrials > 3
|
67
|
-
retrials = 0
|
68
|
-
raise "Unable to retrieve the file. File does not exist?"
|
69
|
-
else
|
70
|
-
logger.warn("Exception in ruby-dicom when loading a dicom file from: #{file}")
|
71
|
-
logger.debug("Retrying... #{retrials}")
|
72
|
-
retrials += 1
|
73
|
-
retry
|
74
|
-
end
|
75
|
-
end
|
76
|
-
bin = File.open(file, "rb") { |f| f.read }
|
77
|
-
# Parse the file contents and create the DICOM object:
|
78
|
-
if bin
|
79
|
-
dcm = self.parse(bin)
|
80
|
-
else
|
81
|
-
dcm = self.new
|
82
|
-
dcm.read_success = false
|
83
|
-
end
|
84
|
-
dcm.source = link
|
85
|
-
return dcm
|
86
|
-
end
|
87
|
-
|
88
|
-
# Creates a DObject instance by parsing an encoded binary DICOM string.
|
89
|
-
#
|
90
|
-
# @param [String] string an encoded binary string containing DICOM information
|
91
|
-
# @param [Hash] options the options to use for parsing the DICOM string
|
92
|
-
# @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
|
93
|
-
# @option options [Boolean] :signature if set as false, the parsing algorithm will not be looking for the DICOM header signature (defaults to true)
|
94
|
-
# @option options [String] :syntax if a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the binary string
|
95
|
-
# @example Parse a DICOM file that has already been loaded to a binary string
|
96
|
-
# require 'dicom'
|
97
|
-
# dcm = DICOM::DObject.parse(str)
|
98
|
-
# @example Parse a header-less DICOM string with explicit little endian transfer syntax
|
99
|
-
# dcm = DICOM::DObject.parse(str, :syntax => '1.2.840.10008.1.2.1')
|
100
|
-
#
|
101
|
-
def self.parse(string, options={})
|
102
|
-
raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
|
103
|
-
raise ArgumentError, "Invalid option :syntax. Expected String, got #{options[:syntax].class}." if options[:syntax] && !options[:syntax].is_a?(String)
|
104
|
-
signature = options[:signature].nil? ? true : options[:signature]
|
105
|
-
dcm = self.new
|
106
|
-
dcm.send(:read, string, signature, :overwrite => options[:overwrite], :syntax => options[:syntax])
|
107
|
-
if dcm.read?
|
108
|
-
logger.debug("DICOM string successfully parsed.")
|
109
|
-
else
|
110
|
-
logger.warn("Failed to parse this string as DICOM.")
|
111
|
-
end
|
112
|
-
dcm.source = :str
|
113
|
-
return dcm
|
114
|
-
end
|
115
|
-
|
116
|
-
# Creates a DObject instance by reading and parsing a DICOM file.
|
117
|
-
#
|
118
|
-
# @param [String] file a string which specifies the path of the DICOM file to be loaded
|
119
|
-
# @param [Hash] options the options to use for reading the DICOM file
|
120
|
-
# @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
|
121
|
-
# @example Load a DICOM file
|
122
|
-
# require 'dicom'
|
123
|
-
# dcm = DICOM::DObject.read('test.dcm')
|
124
|
-
#
|
125
|
-
def self.read(file, options={})
|
126
|
-
raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
|
127
|
-
# Read the file content:
|
128
|
-
bin = nil
|
129
|
-
unless File.exist?(file)
|
130
|
-
logger.error("Invalid (non-existing) file: #{file}")
|
131
|
-
else
|
132
|
-
unless File.readable?(file)
|
133
|
-
logger.error("File exists but I don't have permission to read it: #{file}")
|
134
|
-
else
|
135
|
-
if File.directory?(file)
|
136
|
-
logger.error("Expected a file, got a directory: #{file}")
|
137
|
-
else
|
138
|
-
if File.size(file) < 8
|
139
|
-
logger.error("This file is too small to contain any DICOM information: #{file}.")
|
140
|
-
else
|
141
|
-
bin = File.open(file, "rb") { |f| f.read }
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
# Parse the file contents and create the DICOM object:
|
147
|
-
if bin
|
148
|
-
dcm = self.parse(bin, options)
|
149
|
-
# If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
|
150
|
-
# This will help for some rare cases where the DICOM file is saved (erroneously, Im sure) with explicit encoding without specifying the transfer syntax tag.
|
151
|
-
if !dcm.read? and !dcm.exists?("0002,0010")
|
152
|
-
logger.info("Attempting a second decode pass (assuming Explicit Little Endian transfer syntax).")
|
153
|
-
options[:syntax] = EXPLICIT_LITTLE_ENDIAN
|
154
|
-
dcm = self.parse(bin, options)
|
155
|
-
end
|
156
|
-
else
|
157
|
-
dcm = self.new
|
158
|
-
end
|
159
|
-
if dcm.read?
|
160
|
-
logger.info("DICOM file successfully read: #{file}")
|
161
|
-
else
|
162
|
-
logger.warn("Reading DICOM file failed: #{file}")
|
163
|
-
end
|
164
|
-
dcm.source = file
|
165
|
-
return dcm
|
166
|
-
end
|
167
|
-
|
168
|
-
# Creates a DObject instance (DObject is an abbreviation for "DICOM object").
|
169
|
-
#
|
170
|
-
# The DObject instance holds references to the different types of objects (Element, Item, Sequence)
|
171
|
-
# that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a binary
|
172
|
-
# string (with DObject::read or ::parse), but can also be buildt from an empty state by this method.
|
173
|
-
#
|
174
|
-
# To customize logging behaviour, refer to the Logging module documentation.
|
175
|
-
#
|
176
|
-
# @example Create an empty DICOM object
|
177
|
-
# require 'dicom'
|
178
|
-
# dcm = DICOM::DObject.new
|
179
|
-
# @example Increasing the log message threshold (default level is INFO)
|
180
|
-
# DICOM.logger.level = Logger::ERROR
|
181
|
-
#
|
182
|
-
def initialize
|
183
|
-
# Initialization of variables that DObject share with other parent elements:
|
184
|
-
initialize_parent
|
185
|
-
# Structural information (default values):
|
186
|
-
@explicit = true
|
187
|
-
@str_endian = false
|
188
|
-
# Control variables:
|
189
|
-
@read_success = nil
|
190
|
-
# Initialize a Stream instance which is used for encoding/decoding:
|
191
|
-
@stream = Stream.new(nil, @str_endian)
|
192
|
-
# The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
|
193
|
-
@parent = nil
|
194
|
-
end
|
195
|
-
|
196
|
-
# Checks for equality.
|
197
|
-
#
|
198
|
-
# Other and self are considered equivalent if they are
|
199
|
-
# of compatible types and their attributes are equivalent.
|
200
|
-
#
|
201
|
-
# @param other an object to be compared with self.
|
202
|
-
# @return [Boolean] true if self and other are considered equivalent
|
203
|
-
#
|
204
|
-
def ==(other)
|
205
|
-
if other.respond_to?(:to_dcm)
|
206
|
-
other.send(:state) == state
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
alias_method :eql?, :==
|
211
|
-
|
212
|
-
# Performs de-identification (anonymization) on the DICOM object.
|
213
|
-
#
|
214
|
-
# @param [Anonymizer] a an Anonymizer instance to use for the anonymization
|
215
|
-
#
|
216
|
-
def anonymize(a=Anonymizer.new)
|
217
|
-
a.to_anonymizer.anonymize(self)
|
218
|
-
end
|
219
|
-
|
220
|
-
# Encodes the DICOM object into a series of binary string segments with a specified maximum length.
|
221
|
-
#
|
222
|
-
# Returns the encoded binary strings in an array.
|
223
|
-
#
|
224
|
-
# @param [Integer] max_size the maximum allowed size of the binary data strings to be encoded
|
225
|
-
# @param [String] transfer_syntax the transfer syntax string to be used when encoding the DICOM object to string segments. When this method is used for making network packets, the transfer_syntax is not part of the object, and thus needs to be specified.
|
226
|
-
# @return [Array<String>] the encoded DICOM strings
|
227
|
-
# @example Encode the DObject to strings of max length 2^14 bytes
|
228
|
-
# encoded_strings = dcm.encode_segments(16384)
|
229
|
-
#
|
230
|
-
def encode_segments(max_size, transfer_syntax=IMPLICIT_LITTLE_ENDIAN)
|
231
|
-
raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
|
232
|
-
raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
|
233
|
-
raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
|
234
|
-
encode_in_segments(max_size, :syntax => transfer_syntax)
|
235
|
-
end
|
236
|
-
|
237
|
-
# Computes a hash code for this object.
|
238
|
-
#
|
239
|
-
# @note Two objects with the same attributes will have the same hash code.
|
240
|
-
#
|
241
|
-
# @return [Fixnum] the object's hash code
|
242
|
-
#
|
243
|
-
def hash
|
244
|
-
state.hash
|
245
|
-
end
|
246
|
-
|
247
|
-
# Prints information of interest related to the DICOM object.
|
248
|
-
# Calls the Parent#print method as well as DObject#summary.
|
249
|
-
#
|
250
|
-
def print_all
|
251
|
-
puts ""
|
252
|
-
print(:value_max => 30)
|
253
|
-
summary
|
254
|
-
end
|
255
|
-
|
256
|
-
# Gathers key information about the DObject as well as some system data, and prints this information to the screen.
|
257
|
-
# This information includes properties like encoding, byte order, modality and various image properties.
|
258
|
-
#
|
259
|
-
# @return [Array<String>] strings describing the properties of the DICOM object
|
260
|
-
#
|
261
|
-
def summary
|
262
|
-
# FIXME: Perhaps this method should be split up in one or two separate methods
|
263
|
-
# which just builds the information arrays, and a third method for printing this to the screen.
|
264
|
-
sys_info = Array.new
|
265
|
-
info = Array.new
|
266
|
-
# Version of Ruby DICOM used:
|
267
|
-
sys_info << "Ruby DICOM version: #{VERSION}"
|
268
|
-
# System endian:
|
269
|
-
cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
|
270
|
-
sys_info << "Byte Order (CPU): #{cpu}"
|
271
|
-
# Source (file name):
|
272
|
-
if @source
|
273
|
-
if @source == :str
|
274
|
-
source = "Binary string #{@read_success ? '(successfully parsed)' : '(failed to parse)'}"
|
275
|
-
else
|
276
|
-
source = "File #{@read_success ? '(successfully read)' : '(failed to read)'}: #{@source}"
|
277
|
-
end
|
278
|
-
else
|
279
|
-
source = 'Created from scratch'
|
280
|
-
end
|
281
|
-
info << "Source: #{source}"
|
282
|
-
# Modality:
|
283
|
-
modality = (LIBRARY.uid(value('0008,0016')) ? LIBRARY.uid(value('0008,0016')).name : "SOP Class unknown or not specified!")
|
284
|
-
info << "Modality: #{modality}"
|
285
|
-
# Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
|
286
|
-
ts_status = self['0002,0010'] ? '' : ' (Assumed)'
|
287
|
-
ts = LIBRARY.uid(transfer_syntax)
|
288
|
-
explicit = ts ? ts.explicit? : true
|
289
|
-
endian = ts ? ts.big_endian? : false
|
290
|
-
meta_comment = ts ? "" : " (But unknown/invalid transfer syntax: #{transfer_syntax})"
|
291
|
-
info << "Meta Header: #{self['0002,0010'] ? 'Yes' : 'No'}#{meta_comment}"
|
292
|
-
info << "Value Representation: #{explicit ? 'Explicit' : 'Implicit'}#{ts_status}"
|
293
|
-
info << "Byte Order (File): #{endian ? 'Big Endian' : 'Little Endian'}#{ts_status}"
|
294
|
-
# Pixel data:
|
295
|
-
pixels = self[PIXEL_TAG]
|
296
|
-
unless pixels
|
297
|
-
info << "Pixel Data: No"
|
298
|
-
else
|
299
|
-
info << "Pixel Data: Yes"
|
300
|
-
# Image size:
|
301
|
-
cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
|
302
|
-
rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
|
303
|
-
info << "Image Size: #{cols}*#{rows}"
|
304
|
-
# Frames:
|
305
|
-
frames = value("0028,0008") || "1"
|
306
|
-
unless frames == "1" or frames == 1
|
307
|
-
# Encapsulated or 3D pixel data:
|
308
|
-
if pixels.is_a?(Element)
|
309
|
-
frames = frames.to_s + " (3D Pixel Data)"
|
310
|
-
else
|
311
|
-
frames = frames.to_s + " (Encapsulated Multiframe Image)"
|
312
|
-
end
|
313
|
-
end
|
314
|
-
info << "Number of frames: #{frames}"
|
315
|
-
# Color:
|
316
|
-
colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
|
317
|
-
info << "Photometry: #{colors}"
|
318
|
-
# Compression:
|
319
|
-
compression = (ts ? (ts.compressed_pixels? ? ts.name : 'No') : 'No' )
|
320
|
-
info << "Compression: #{compression}#{ts_status}"
|
321
|
-
# Pixel bits (allocated):
|
322
|
-
bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
|
323
|
-
info << "Bits per Pixel: #{bits}"
|
324
|
-
end
|
325
|
-
# Print the DICOM object's key properties:
|
326
|
-
separator = "-------------------------------------------"
|
327
|
-
puts "System Properties:"
|
328
|
-
puts separator + "\n"
|
329
|
-
puts sys_info
|
330
|
-
puts "\n"
|
331
|
-
puts "DICOM Object Properties:"
|
332
|
-
puts separator
|
333
|
-
puts info
|
334
|
-
puts separator
|
335
|
-
return info
|
336
|
-
end
|
337
|
-
|
338
|
-
# Returns self.
|
339
|
-
#
|
340
|
-
# @return [DObject] self
|
341
|
-
#
|
342
|
-
def to_dcm
|
343
|
-
self
|
344
|
-
end
|
345
|
-
|
346
|
-
# Gives the transfer syntax string of the DObject.
|
347
|
-
#
|
348
|
-
# If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
|
349
|
-
#
|
350
|
-
# @return [String] the DObject's transfer syntax
|
351
|
-
#
|
352
|
-
def transfer_syntax
|
353
|
-
return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
|
354
|
-
end
|
355
|
-
|
356
|
-
# Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
|
357
|
-
# numerical values if a switch of endianness is implied.
|
358
|
-
#
|
359
|
-
# @note This method does not change the compressed state of the pixel data element. Changing
|
360
|
-
# the transfer syntax between an uncompressed and compressed state will NOT change the pixel
|
361
|
-
# data accordingly (this must be taken care of manually).
|
362
|
-
#
|
363
|
-
# @param [String] new_syntax the new transfer syntax string to be applied to the DObject
|
364
|
-
#
|
365
|
-
def transfer_syntax=(new_syntax)
|
366
|
-
# Verify old and new transfer syntax:
|
367
|
-
new_uid = LIBRARY.uid(new_syntax)
|
368
|
-
old_uid = LIBRARY.uid(transfer_syntax)
|
369
|
-
raise ArgumentError, "Invalid/unknown transfer syntax specified: #{new_syntax}" unless new_uid && new_uid.transfer_syntax?
|
370
|
-
raise ArgumentError, "Invalid/unknown existing transfer syntax: #{new_syntax} Unable to reliably handle byte order encoding. Modify the transfer syntax element directly instead." unless old_uid && old_uid.transfer_syntax?
|
371
|
-
# Set the new transfer syntax:
|
372
|
-
if exists?("0002,0010")
|
373
|
-
self["0002,0010"].value = new_syntax
|
374
|
-
else
|
375
|
-
add(Element.new("0002,0010", new_syntax))
|
376
|
-
end
|
377
|
-
# Update our Stream instance with the new encoding:
|
378
|
-
@stream.endian = new_uid.big_endian?
|
379
|
-
# If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
|
380
|
-
encode_children(old_uid.big_endian?) if old_uid.big_endian? != new_uid.big_endian?
|
381
|
-
end
|
382
|
-
|
383
|
-
# Writes the DICOM object to file.
|
384
|
-
#
|
385
|
-
# @note The goal of the Ruby DICOM library is to yield maximum conformance with the DICOM
|
386
|
-
# standard when outputting DICOM files. Therefore, when encoding the DICOM file, manipulation
|
387
|
-
# of items such as the meta group, group lengths and header signature may occur. Therefore,
|
388
|
-
# the file that is written may not be an exact bitwise copy of the file that was read, even if no
|
389
|
-
# DObject manipulation has been done by the user.
|
390
|
-
#
|
391
|
-
# @param [String] file_name the path of the DICOM file which is to be written to disk
|
392
|
-
# @param [Hash] options the options to use for writing the DICOM file
|
393
|
-
# @option options [Boolean] :ignore_meta if true, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file
|
394
|
-
# @option options [Boolean] :include_empty_parents if true, childless parents (sequences & items) are written to the DICOM file
|
395
|
-
# @example Encode a DICOM file from a DObject
|
396
|
-
# dcm.write('C:/dicom/test.dcm')
|
397
|
-
#
|
398
|
-
def write(file_name, options={})
|
399
|
-
raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
|
400
|
-
@include_empty_parents = options[:include_empty_parents]
|
401
|
-
insert_missing_meta unless options[:ignore_meta]
|
402
|
-
write_elements(:file_name => file_name, :signature => true, :syntax => transfer_syntax)
|
403
|
-
end
|
404
|
-
|
405
|
-
|
406
|
-
private
|
407
|
-
|
408
|
-
|
409
|
-
# Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
|
410
|
-
# to ensure that a valid DICOM object is encoded.
|
411
|
-
#
|
412
|
-
def insert_missing_meta
|
413
|
-
{
|
414
|
-
'0002,0001' => [0,1], # File Meta Information Version
|
415
|
-
'0002,0002' => value('0008,0016'), # Media Storage SOP Class UID
|
416
|
-
'0002,0003' => value('0008,0018'), # Media Storage SOP Instance UID
|
417
|
-
'0002,0010' => transfer_syntax, # Transfer Syntax UID
|
418
|
-
'0002,0016' => DICOM.source_app_title, # Source Application Entity Title
|
419
|
-
}.each_pair do |tag, value|
|
420
|
-
add_element(tag, value) unless exists?(tag)
|
421
|
-
end
|
422
|
-
if !exists?("0002,0012") && !exists?("0002,0013")
|
423
|
-
# Implementation Class UID:
|
424
|
-
add_element("0002,0012", UID_ROOT)
|
425
|
-
# Implementation Version Name:
|
426
|
-
add_element("0002,0013", NAME)
|
427
|
-
end
|
428
|
-
# Delete the old group length first (if it exists) to avoid a miscount
|
429
|
-
# in the coming group length determination.
|
430
|
-
delete("0002,0000")
|
431
|
-
add_element("0002,0000", meta_group_length)
|
432
|
-
end
|
433
|
-
|
434
|
-
# Determines the length of the meta group in the DObject instance.
|
435
|
-
#
|
436
|
-
# @return [Integer] the length of the file meta group string
|
437
|
-
#
|
438
|
-
def meta_group_length
|
439
|
-
group_length = 0
|
440
|
-
meta_elements = group(META_GROUP)
|
441
|
-
tag = 4
|
442
|
-
vr = 2
|
443
|
-
meta_elements.each do |element|
|
444
|
-
case element.vr
|
445
|
-
when "OB","OW","OF","SQ","UN","UT"
|
446
|
-
length = 6
|
447
|
-
else
|
448
|
-
length = 2
|
449
|
-
end
|
450
|
-
group_length += tag + vr + length + element.bin.length
|
451
|
-
end
|
452
|
-
group_length
|
453
|
-
end
|
454
|
-
|
455
|
-
# Collects the attributes of this instance.
|
456
|
-
#
|
457
|
-
# @return [Array<Element, Sequence>] an array of elements and sequences
|
458
|
-
#
|
459
|
-
def state
|
460
|
-
@tags
|
461
|
-
end
|
462
|
-
|
463
|
-
end
|
464
|
-
|
465
|
-
end
|
1
|
+
# Copyright 2008-2017 Christoffer Lervag
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
#
|
16
|
+
module DICOM
|
17
|
+
|
18
|
+
# The DObject class is the main class for interacting with the DICOM object.
|
19
|
+
# Reading from and writing to files is executed from instances of this class.
|
20
|
+
#
|
21
|
+
# === Inheritance
|
22
|
+
#
|
23
|
+
# As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class,
|
24
|
+
# all ImageItem and Parent methods are also available to instances of DObject.
|
25
|
+
#
|
26
|
+
class DObject < ImageItem
|
27
|
+
include Logging
|
28
|
+
|
29
|
+
# An attribute set as nil. This attribute is included to provide consistency with the other element types which usually have a parent defined.
|
30
|
+
attr_reader :parent
|
31
|
+
# A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
|
32
|
+
attr_accessor :read_success
|
33
|
+
# The source of the DObject (nil, :str or file name string).
|
34
|
+
attr_accessor :source
|
35
|
+
# The Stream instance associated with this DObject instance (this attribute is mostly used internally).
|
36
|
+
attr_reader :stream
|
37
|
+
# An attribute (used by e.g. DICOM.load) to indicate that a DObject-type instance was given to the load method (instead of e.g. a file).
|
38
|
+
attr_accessor :was_dcm_on_input
|
39
|
+
# A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
|
40
|
+
attr_reader :write_success
|
41
|
+
|
42
|
+
alias_method :read?, :read_success
|
43
|
+
alias_method :written?, :write_success
|
44
|
+
|
45
|
+
# Creates a DObject instance by downloading a DICOM file
|
46
|
+
# specified by a hyperlink, and parsing the retrieved file.
|
47
|
+
#
|
48
|
+
# @note Highly experimental and un-tested!
|
49
|
+
# @note Designed for the HTTP protocol only.
|
50
|
+
# @note Whether this method should be included or removed from ruby-dicom is up for debate.
|
51
|
+
#
|
52
|
+
# @param [String] link a hyperlink string which specifies remote location of the DICOM file to be loaded
|
53
|
+
# @return [DObject] the created DObject instance
|
54
|
+
#
|
55
|
+
def self.get(link)
|
56
|
+
raise ArgumentError, "Invalid argument 'link'. Expected String, got #{link.class}." unless link.is_a?(String)
|
57
|
+
raise ArgumentError, "Invalid argument 'link'. Expected a string starting with 'http', got #{link}." unless link.index('http') == 0
|
58
|
+
require 'open-uri'
|
59
|
+
bin = nil
|
60
|
+
file = nil
|
61
|
+
# Try to open the remote file using open-uri:
|
62
|
+
retrials = 0
|
63
|
+
begin
|
64
|
+
file = open(link, 'rb') # binary encoding (ASCII-8BIT)
|
65
|
+
rescue Exception => e
|
66
|
+
if retrials > 3
|
67
|
+
retrials = 0
|
68
|
+
raise "Unable to retrieve the file. File does not exist?"
|
69
|
+
else
|
70
|
+
logger.warn("Exception in ruby-dicom when loading a dicom file from: #{file}")
|
71
|
+
logger.debug("Retrying... #{retrials}")
|
72
|
+
retrials += 1
|
73
|
+
retry
|
74
|
+
end
|
75
|
+
end
|
76
|
+
bin = File.open(file, "rb") { |f| f.read }
|
77
|
+
# Parse the file contents and create the DICOM object:
|
78
|
+
if bin
|
79
|
+
dcm = self.parse(bin)
|
80
|
+
else
|
81
|
+
dcm = self.new
|
82
|
+
dcm.read_success = false
|
83
|
+
end
|
84
|
+
dcm.source = link
|
85
|
+
return dcm
|
86
|
+
end
|
87
|
+
|
88
|
+
# Creates a DObject instance by parsing an encoded binary DICOM string.
|
89
|
+
#
|
90
|
+
# @param [String] string an encoded binary string containing DICOM information
|
91
|
+
# @param [Hash] options the options to use for parsing the DICOM string
|
92
|
+
# @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
|
93
|
+
# @option options [Boolean] :signature if set as false, the parsing algorithm will not be looking for the DICOM header signature (defaults to true)
|
94
|
+
# @option options [String] :syntax if a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the binary string
|
95
|
+
# @example Parse a DICOM file that has already been loaded to a binary string
|
96
|
+
# require 'dicom'
|
97
|
+
# dcm = DICOM::DObject.parse(str)
|
98
|
+
# @example Parse a header-less DICOM string with explicit little endian transfer syntax
|
99
|
+
# dcm = DICOM::DObject.parse(str, :syntax => '1.2.840.10008.1.2.1')
|
100
|
+
#
|
101
|
+
def self.parse(string, options={})
|
102
|
+
raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
|
103
|
+
raise ArgumentError, "Invalid option :syntax. Expected String, got #{options[:syntax].class}." if options[:syntax] && !options[:syntax].is_a?(String)
|
104
|
+
signature = options[:signature].nil? ? true : options[:signature]
|
105
|
+
dcm = self.new
|
106
|
+
dcm.send(:read, string, signature, :overwrite => options[:overwrite], :syntax => options[:syntax])
|
107
|
+
if dcm.read?
|
108
|
+
logger.debug("DICOM string successfully parsed.")
|
109
|
+
else
|
110
|
+
logger.warn("Failed to parse this string as DICOM.")
|
111
|
+
end
|
112
|
+
dcm.source = :str
|
113
|
+
return dcm
|
114
|
+
end
|
115
|
+
|
116
|
+
# Creates a DObject instance by reading and parsing a DICOM file.
|
117
|
+
#
|
118
|
+
# @param [String] file a string which specifies the path of the DICOM file to be loaded
|
119
|
+
# @param [Hash] options the options to use for reading the DICOM file
|
120
|
+
# @option options [Boolean] :overwrite for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates
|
121
|
+
# @example Load a DICOM file
|
122
|
+
# require 'dicom'
|
123
|
+
# dcm = DICOM::DObject.read('test.dcm')
|
124
|
+
#
|
125
|
+
def self.read(file, options={})
|
126
|
+
raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
|
127
|
+
# Read the file content:
|
128
|
+
bin = nil
|
129
|
+
unless File.exist?(file)
|
130
|
+
logger.error("Invalid (non-existing) file: #{file}")
|
131
|
+
else
|
132
|
+
unless File.readable?(file)
|
133
|
+
logger.error("File exists but I don't have permission to read it: #{file}")
|
134
|
+
else
|
135
|
+
if File.directory?(file)
|
136
|
+
logger.error("Expected a file, got a directory: #{file}")
|
137
|
+
else
|
138
|
+
if File.size(file) < 8
|
139
|
+
logger.error("This file is too small to contain any DICOM information: #{file}.")
|
140
|
+
else
|
141
|
+
bin = File.open(file, "rb") { |f| f.read }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
# Parse the file contents and create the DICOM object:
|
147
|
+
if bin
|
148
|
+
dcm = self.parse(bin, options)
|
149
|
+
# If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
|
150
|
+
# This will help for some rare cases where the DICOM file is saved (erroneously, Im sure) with explicit encoding without specifying the transfer syntax tag.
|
151
|
+
if !dcm.read? and !dcm.exists?("0002,0010")
|
152
|
+
logger.info("Attempting a second decode pass (assuming Explicit Little Endian transfer syntax).")
|
153
|
+
options[:syntax] = EXPLICIT_LITTLE_ENDIAN
|
154
|
+
dcm = self.parse(bin, options)
|
155
|
+
end
|
156
|
+
else
|
157
|
+
dcm = self.new
|
158
|
+
end
|
159
|
+
if dcm.read?
|
160
|
+
logger.info("DICOM file successfully read: #{file}")
|
161
|
+
else
|
162
|
+
logger.warn("Reading DICOM file failed: #{file}")
|
163
|
+
end
|
164
|
+
dcm.source = file
|
165
|
+
return dcm
|
166
|
+
end
|
167
|
+
|
168
|
+
# Creates a DObject instance (DObject is an abbreviation for "DICOM object").
|
169
|
+
#
|
170
|
+
# The DObject instance holds references to the different types of objects (Element, Item, Sequence)
|
171
|
+
# that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a binary
|
172
|
+
# string (with DObject::read or ::parse), but can also be buildt from an empty state by this method.
|
173
|
+
#
|
174
|
+
# To customize logging behaviour, refer to the Logging module documentation.
|
175
|
+
#
|
176
|
+
# @example Create an empty DICOM object
|
177
|
+
# require 'dicom'
|
178
|
+
# dcm = DICOM::DObject.new
|
179
|
+
# @example Increasing the log message threshold (default level is INFO)
|
180
|
+
# DICOM.logger.level = Logger::ERROR
|
181
|
+
#
|
182
|
+
def initialize
|
183
|
+
# Initialization of variables that DObject share with other parent elements:
|
184
|
+
initialize_parent
|
185
|
+
# Structural information (default values):
|
186
|
+
@explicit = true
|
187
|
+
@str_endian = false
|
188
|
+
# Control variables:
|
189
|
+
@read_success = nil
|
190
|
+
# Initialize a Stream instance which is used for encoding/decoding:
|
191
|
+
@stream = Stream.new(nil, @str_endian)
|
192
|
+
# The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
|
193
|
+
@parent = nil
|
194
|
+
end
|
195
|
+
|
196
|
+
# Checks for equality.
|
197
|
+
#
|
198
|
+
# Other and self are considered equivalent if they are
|
199
|
+
# of compatible types and their attributes are equivalent.
|
200
|
+
#
|
201
|
+
# @param other an object to be compared with self.
|
202
|
+
# @return [Boolean] true if self and other are considered equivalent
|
203
|
+
#
|
204
|
+
def ==(other)
|
205
|
+
if other.respond_to?(:to_dcm)
|
206
|
+
other.send(:state) == state
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
alias_method :eql?, :==
|
211
|
+
|
212
|
+
# Performs de-identification (anonymization) on the DICOM object.
|
213
|
+
#
|
214
|
+
# @param [Anonymizer] a an Anonymizer instance to use for the anonymization
|
215
|
+
#
|
216
|
+
def anonymize(a=Anonymizer.new)
|
217
|
+
a.to_anonymizer.anonymize(self)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Encodes the DICOM object into a series of binary string segments with a specified maximum length.
|
221
|
+
#
|
222
|
+
# Returns the encoded binary strings in an array.
|
223
|
+
#
|
224
|
+
# @param [Integer] max_size the maximum allowed size of the binary data strings to be encoded
|
225
|
+
# @param [String] transfer_syntax the transfer syntax string to be used when encoding the DICOM object to string segments. When this method is used for making network packets, the transfer_syntax is not part of the object, and thus needs to be specified.
|
226
|
+
# @return [Array<String>] the encoded DICOM strings
|
227
|
+
# @example Encode the DObject to strings of max length 2^14 bytes
|
228
|
+
# encoded_strings = dcm.encode_segments(16384)
|
229
|
+
#
|
230
|
+
def encode_segments(max_size, transfer_syntax=IMPLICIT_LITTLE_ENDIAN)
|
231
|
+
raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
|
232
|
+
raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
|
233
|
+
raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
|
234
|
+
encode_in_segments(max_size, :syntax => transfer_syntax)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Computes a hash code for this object.
|
238
|
+
#
|
239
|
+
# @note Two objects with the same attributes will have the same hash code.
|
240
|
+
#
|
241
|
+
# @return [Fixnum] the object's hash code
|
242
|
+
#
|
243
|
+
def hash
|
244
|
+
state.hash
|
245
|
+
end
|
246
|
+
|
247
|
+
# Prints information of interest related to the DICOM object.
|
248
|
+
# Calls the Parent#print method as well as DObject#summary.
|
249
|
+
#
|
250
|
+
def print_all
|
251
|
+
puts ""
|
252
|
+
print(:value_max => 30)
|
253
|
+
summary
|
254
|
+
end
|
255
|
+
|
256
|
+
# Gathers key information about the DObject as well as some system data, and prints this information to the screen.
|
257
|
+
# This information includes properties like encoding, byte order, modality and various image properties.
|
258
|
+
#
|
259
|
+
# @return [Array<String>] strings describing the properties of the DICOM object
|
260
|
+
#
|
261
|
+
def summary
|
262
|
+
# FIXME: Perhaps this method should be split up in one or two separate methods
|
263
|
+
# which just builds the information arrays, and a third method for printing this to the screen.
|
264
|
+
sys_info = Array.new
|
265
|
+
info = Array.new
|
266
|
+
# Version of Ruby DICOM used:
|
267
|
+
sys_info << "Ruby DICOM version: #{VERSION}"
|
268
|
+
# System endian:
|
269
|
+
cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
|
270
|
+
sys_info << "Byte Order (CPU): #{cpu}"
|
271
|
+
# Source (file name):
|
272
|
+
if @source
|
273
|
+
if @source == :str
|
274
|
+
source = "Binary string #{@read_success ? '(successfully parsed)' : '(failed to parse)'}"
|
275
|
+
else
|
276
|
+
source = "File #{@read_success ? '(successfully read)' : '(failed to read)'}: #{@source}"
|
277
|
+
end
|
278
|
+
else
|
279
|
+
source = 'Created from scratch'
|
280
|
+
end
|
281
|
+
info << "Source: #{source}"
|
282
|
+
# Modality:
|
283
|
+
modality = (LIBRARY.uid(value('0008,0016')) ? LIBRARY.uid(value('0008,0016')).name : "SOP Class unknown or not specified!")
|
284
|
+
info << "Modality: #{modality}"
|
285
|
+
# Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
|
286
|
+
ts_status = self['0002,0010'] ? '' : ' (Assumed)'
|
287
|
+
ts = LIBRARY.uid(transfer_syntax)
|
288
|
+
explicit = ts ? ts.explicit? : true
|
289
|
+
endian = ts ? ts.big_endian? : false
|
290
|
+
meta_comment = ts ? "" : " (But unknown/invalid transfer syntax: #{transfer_syntax})"
|
291
|
+
info << "Meta Header: #{self['0002,0010'] ? 'Yes' : 'No'}#{meta_comment}"
|
292
|
+
info << "Value Representation: #{explicit ? 'Explicit' : 'Implicit'}#{ts_status}"
|
293
|
+
info << "Byte Order (File): #{endian ? 'Big Endian' : 'Little Endian'}#{ts_status}"
|
294
|
+
# Pixel data:
|
295
|
+
pixels = self[PIXEL_TAG]
|
296
|
+
unless pixels
|
297
|
+
info << "Pixel Data: No"
|
298
|
+
else
|
299
|
+
info << "Pixel Data: Yes"
|
300
|
+
# Image size:
|
301
|
+
cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
|
302
|
+
rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
|
303
|
+
info << "Image Size: #{cols}*#{rows}"
|
304
|
+
# Frames:
|
305
|
+
frames = value("0028,0008") || "1"
|
306
|
+
unless frames == "1" or frames == 1
|
307
|
+
# Encapsulated or 3D pixel data:
|
308
|
+
if pixels.is_a?(Element)
|
309
|
+
frames = frames.to_s + " (3D Pixel Data)"
|
310
|
+
else
|
311
|
+
frames = frames.to_s + " (Encapsulated Multiframe Image)"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
info << "Number of frames: #{frames}"
|
315
|
+
# Color:
|
316
|
+
colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
|
317
|
+
info << "Photometry: #{colors}"
|
318
|
+
# Compression:
|
319
|
+
compression = (ts ? (ts.compressed_pixels? ? ts.name : 'No') : 'No' )
|
320
|
+
info << "Compression: #{compression}#{ts_status}"
|
321
|
+
# Pixel bits (allocated):
|
322
|
+
bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
|
323
|
+
info << "Bits per Pixel: #{bits}"
|
324
|
+
end
|
325
|
+
# Print the DICOM object's key properties:
|
326
|
+
separator = "-------------------------------------------"
|
327
|
+
puts "System Properties:"
|
328
|
+
puts separator + "\n"
|
329
|
+
puts sys_info
|
330
|
+
puts "\n"
|
331
|
+
puts "DICOM Object Properties:"
|
332
|
+
puts separator
|
333
|
+
puts info
|
334
|
+
puts separator
|
335
|
+
return info
|
336
|
+
end
|
337
|
+
|
338
|
+
# Returns self.
|
339
|
+
#
|
340
|
+
# @return [DObject] self
|
341
|
+
#
|
342
|
+
def to_dcm
|
343
|
+
self
|
344
|
+
end
|
345
|
+
|
346
|
+
# Gives the transfer syntax string of the DObject.
|
347
|
+
#
|
348
|
+
# If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
|
349
|
+
#
|
350
|
+
# @return [String] the DObject's transfer syntax
|
351
|
+
#
|
352
|
+
def transfer_syntax
|
353
|
+
return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
|
354
|
+
end
|
355
|
+
|
356
|
+
# Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
|
357
|
+
# numerical values if a switch of endianness is implied.
|
358
|
+
#
|
359
|
+
# @note This method does not change the compressed state of the pixel data element. Changing
|
360
|
+
# the transfer syntax between an uncompressed and compressed state will NOT change the pixel
|
361
|
+
# data accordingly (this must be taken care of manually).
|
362
|
+
#
|
363
|
+
# @param [String] new_syntax the new transfer syntax string to be applied to the DObject
|
364
|
+
#
|
365
|
+
def transfer_syntax=(new_syntax)
|
366
|
+
# Verify old and new transfer syntax:
|
367
|
+
new_uid = LIBRARY.uid(new_syntax)
|
368
|
+
old_uid = LIBRARY.uid(transfer_syntax)
|
369
|
+
raise ArgumentError, "Invalid/unknown transfer syntax specified: #{new_syntax}" unless new_uid && new_uid.transfer_syntax?
|
370
|
+
raise ArgumentError, "Invalid/unknown existing transfer syntax: #{new_syntax} Unable to reliably handle byte order encoding. Modify the transfer syntax element directly instead." unless old_uid && old_uid.transfer_syntax?
|
371
|
+
# Set the new transfer syntax:
|
372
|
+
if exists?("0002,0010")
|
373
|
+
self["0002,0010"].value = new_syntax
|
374
|
+
else
|
375
|
+
add(Element.new("0002,0010", new_syntax))
|
376
|
+
end
|
377
|
+
# Update our Stream instance with the new encoding:
|
378
|
+
@stream.endian = new_uid.big_endian?
|
379
|
+
# If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
|
380
|
+
encode_children(old_uid.big_endian?) if old_uid.big_endian? != new_uid.big_endian?
|
381
|
+
end
|
382
|
+
|
383
|
+
# Writes the DICOM object to file.
|
384
|
+
#
|
385
|
+
# @note The goal of the Ruby DICOM library is to yield maximum conformance with the DICOM
|
386
|
+
# standard when outputting DICOM files. Therefore, when encoding the DICOM file, manipulation
|
387
|
+
# of items such as the meta group, group lengths and header signature may occur. Therefore,
|
388
|
+
# the file that is written may not be an exact bitwise copy of the file that was read, even if no
|
389
|
+
# DObject manipulation has been done by the user.
|
390
|
+
#
|
391
|
+
# @param [String] file_name the path of the DICOM file which is to be written to disk
|
392
|
+
# @param [Hash] options the options to use for writing the DICOM file
|
393
|
+
# @option options [Boolean] :ignore_meta if true, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file
|
394
|
+
# @option options [Boolean] :include_empty_parents if true, childless parents (sequences & items) are written to the DICOM file
|
395
|
+
# @example Encode a DICOM file from a DObject
|
396
|
+
# dcm.write('C:/dicom/test.dcm')
|
397
|
+
#
|
398
|
+
def write(file_name, options={})
|
399
|
+
raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
|
400
|
+
@include_empty_parents = options[:include_empty_parents]
|
401
|
+
insert_missing_meta unless options[:ignore_meta]
|
402
|
+
write_elements(:file_name => file_name, :signature => true, :syntax => transfer_syntax)
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
private
|
407
|
+
|
408
|
+
|
409
|
+
# Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
|
410
|
+
# to ensure that a valid DICOM object is encoded.
|
411
|
+
#
|
412
|
+
def insert_missing_meta
|
413
|
+
{
|
414
|
+
'0002,0001' => [0,1], # File Meta Information Version
|
415
|
+
'0002,0002' => value('0008,0016'), # Media Storage SOP Class UID
|
416
|
+
'0002,0003' => value('0008,0018'), # Media Storage SOP Instance UID
|
417
|
+
'0002,0010' => transfer_syntax, # Transfer Syntax UID
|
418
|
+
'0002,0016' => DICOM.source_app_title, # Source Application Entity Title
|
419
|
+
}.each_pair do |tag, value|
|
420
|
+
add_element(tag, value) unless exists?(tag)
|
421
|
+
end
|
422
|
+
if !exists?("0002,0012") && !exists?("0002,0013")
|
423
|
+
# Implementation Class UID:
|
424
|
+
add_element("0002,0012", UID_ROOT)
|
425
|
+
# Implementation Version Name:
|
426
|
+
add_element("0002,0013", NAME)
|
427
|
+
end
|
428
|
+
# Delete the old group length first (if it exists) to avoid a miscount
|
429
|
+
# in the coming group length determination.
|
430
|
+
delete("0002,0000")
|
431
|
+
add_element("0002,0000", meta_group_length)
|
432
|
+
end
|
433
|
+
|
434
|
+
# Determines the length of the meta group in the DObject instance.
|
435
|
+
#
|
436
|
+
# @return [Integer] the length of the file meta group string
|
437
|
+
#
|
438
|
+
def meta_group_length
|
439
|
+
group_length = 0
|
440
|
+
meta_elements = group(META_GROUP)
|
441
|
+
tag = 4
|
442
|
+
vr = 2
|
443
|
+
meta_elements.each do |element|
|
444
|
+
case element.vr
|
445
|
+
when "OB","OW","OF","SQ","UN","UT"
|
446
|
+
length = 6
|
447
|
+
else
|
448
|
+
length = 2
|
449
|
+
end
|
450
|
+
group_length += tag + vr + length + element.bin.length
|
451
|
+
end
|
452
|
+
group_length
|
453
|
+
end
|
454
|
+
|
455
|
+
# Collects the attributes of this instance.
|
456
|
+
#
|
457
|
+
# @return [Array<Element, Sequence>] an array of elements and sequences
|
458
|
+
#
|
459
|
+
def state
|
460
|
+
@tags
|
461
|
+
end
|
462
|
+
|
463
|
+
end
|
464
|
+
|
465
|
+
end
|