dicom 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,465 +1,465 @@
1
- # Copyright 2008-2014 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
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