dicom 0.9.3 → 0.9.4

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.
@@ -1,539 +1,451 @@
1
- # === TODO:
2
- #
3
- # * The retrieve file network functionality (#get_image in DClient class) has not been tested.
4
- # * Make the networking code more intelligent in its handling of unexpected network communication.
5
- # * Full support for compressed image data.
6
- # * Read/Write 12 bit image data.
7
- # * Full color support (RGB and PALETTE COLOR with #image already implemented).
8
- # * Support for extraction of multiple encapsulated pixel data frames in #pixels and #narray.
9
- # * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
10
- # * More robust and flexible options for reorienting extracted pixel arrays?
11
- # * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
12
- # * Add these as github issues and remove this list!
13
-
14
-
15
- # Copyright 2008-2012 Christoffer Lervag
16
- #
17
- # This program is free software: you can redistribute it and/or modify
18
- # it under the terms of the GNU General Public License as published by
19
- # the Free Software Foundation, either version 3 of the License, or
20
- # (at your option) any later version.
21
- #
22
- # This program is distributed in the hope that it will be useful,
23
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
24
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
- # GNU General Public License for more details.
26
- #
27
- # You should have received a copy of the GNU General Public License
28
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
29
- #
30
- module DICOM
31
-
32
- # The DObject class is the main class for interacting with the DICOM object.
33
- # Reading from and writing to files is executed from instances of this class.
34
- #
35
- # === Inheritance
36
- #
37
- # As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class,
38
- # all ImageItem and Parent methods are also available to instances of DObject.
39
- #
40
- class DObject < ImageItem
41
- include Logging
42
-
43
- # Creates a DObject instance by downloading a DICOM file
44
- # specified by a hyperlink, and parsing the retrieved file.
45
- #
46
- # === Restrictions
47
- #
48
- # * Highly experimental and un-tested!
49
- # * Designed for HTTP protocol only.
50
- # * Whether this method should be included or removed from ruby-dicom is up for debate.
51
- #
52
- # === Parameters
53
- #
54
- # * <tt>link</tt> -- A hyperlink string which specifies remote location of the DICOM file to be loaded.
55
- #
56
- def self.get(link)
57
- raise ArgumentError, "Invalid argument 'link'. Expected String, got #{link.class}." unless link.is_a?(String)
58
- raise ArgumentError, "Invalid argument 'link'. Expected a string starting with 'http', got #{link}." unless link.index('http') == 0
59
- require 'open-uri'
60
- bin = nil
61
- file = nil
62
- # Try to open the remote file using open-uri:
63
- retrials = 0
64
- begin
65
- file = open(link, 'rb') # binary encoding (ASCII-8BIT)
66
- rescue Exception => e
67
- if retrials > 3
68
- retrials = 0
69
- raise "Unable to retrieve the file. File does not exist?"
70
- else
71
- logger.warn("Exception in ruby-dicom when loading a dicom file from: #{file}")
72
- logger.debug("Retrying... #{retrials}")
73
- retrials += 1
74
- retry
75
- end
76
- end
77
- bin = File.open(file, "rb") { |f| f.read }
78
- # Parse the file contents and create the DICOM object:
79
- if bin
80
- dcm = self.parse(bin)
81
- else
82
- dcm = self.new
83
- dcm.read_success = false
84
- end
85
- return dcm
86
- end
87
-
88
- # Creates a DObject instance by parsing an encoded binary DICOM string.
89
- #
90
- # === Parameters
91
- #
92
- # * <tt>string</tt> -- An encoded binary string containing DICOM information.
93
- # * <tt>options</tt> -- A hash of parameters.
94
- #
95
- # === Options
96
- #
97
- # * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
98
- # * <tt>:syntax</tt> -- String. If a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the binary string.
99
- #
100
- def self.parse(string, options={})
101
- syntax = options[:syntax]
102
- no_header = options[:no_meta]
103
- raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
104
- raise ArgumentError, "Invalid option :syntax. Expected String, got #{syntax.class}." if syntax && !syntax.is_a?(String)
105
- dcm = self.new
106
- dcm.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
107
- return dcm
108
- end
109
-
110
- # Creates a DObject instance by reading and parsing a DICOM file.
111
- #
112
- # === Parameters
113
- #
114
- # * <tt>file</tt> -- A string which specifies the path of the DICOM file to be loaded.
115
- #
116
- def self.read(file)
117
- raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
118
- # Read the file content:
119
- bin = nil
120
- unless File.exist?(file)
121
- logger.error("Invalid (non-existing) file: #{file}")
122
- else
123
- unless File.readable?(file)
124
- logger.error("File exists but I don't have permission to read it: #{file}")
125
- else
126
- if File.directory?(file)
127
- logger.error("Expected a file, got a directory: #{file}")
128
- else
129
- if File.size(file) < 8
130
- logger.error("This file is too small to contain any DICOM information: #{file}.")
131
- else
132
- bin = File.open(file, "rb") { |f| f.read }
133
- end
134
- end
135
- end
136
- end
137
- # Parse the file contents and create the DICOM object:
138
- if bin
139
- dcm = self.parse(bin)
140
- else
141
- dcm = self.new
142
- dcm.read_success = false
143
- end
144
- return dcm
145
- end
146
-
147
- # A boolean set as false. This attribute is included to provide consistency with other object types for the internal methods which use it.
148
- attr_reader :parent
149
- # A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
150
- attr_accessor :read_success
151
- # The Stream instance associated with this DObject instance (this attribute is mostly used internally).
152
- attr_reader :stream
153
- # A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
154
- attr_reader :write_success
155
-
156
- alias_method :read?, :read_success
157
- alias_method :written?, :write_success
158
-
159
- # Creates a DObject instance (DObject is an abbreviation for "DICOM object").
160
- #
161
- # === Notes
162
- #
163
- # The DObject instance holds references to the different types of objects (Element, Item, Sequence)
164
- # that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a
165
- # binary string, but can also be buildt from an empty state by the user.
166
- #
167
- # To customize logging behaviour, refer to the Logging module documentation.
168
- #
169
- # === Parameters
170
- #
171
- # * <tt>string</tt> -- (Deprecated) A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed. The parameter defaults to nil, in which case an empty DObject instance is created.
172
- # * <tt>options</tt> -- A hash of parameters.
173
- #
174
- # === Options
175
- #
176
- # * <tt>:bin</tt> -- (Deprecated) Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
177
- # * <tt>:syntax</tt> -- (Deprecated) String. If a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the file/binary string.
178
- #
179
- # === Examples
180
- #
181
- # # Load a DICOM file (Deprecated: please use DObject.read() instead):
182
- # require 'dicom'
183
- # dcm = DICOM::DObject.new("test.dcm")
184
- # # Read a DICOM file that has already been loaded into memory in a binary string (with a known transfer syntax):
185
- # # (Deprecated: please use DObject.parse() instead)
186
- # dcm = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
187
- # # Create an empty DICOM object
188
- # dcm = DICOM::DObject.new
189
- # # Increasing the log message threshold (default level is INFO):
190
- # DICOM.logger.level = Logger::WARN
191
- #
192
- def initialize(string=nil, options={})
193
- # Deprecation warning:
194
- logger.warn("Calling DOBject#new with a string argument is deprecated. Please use DObject#read (for reading files) or DObject#parse (for parsing strings) instead. Support for DObject#new with a string argument will be removed in a future version.") if string
195
- # Removal warning:
196
- logger.warn("The option :verbose no longer has any meaning. Please specify logger levels instead, e.g. DICOM.logger.level = Logger::WARN (refer to the documentation for more details).") if options[:verbose] == false
197
- # Initialization of variables that DObject share with other parent elements:
198
- initialize_parent
199
- # Structural information (default values):
200
- @explicit = true
201
- @file_endian = false
202
- # Control variables:
203
- @read_success = nil
204
- # Initialize a Stream instance which is used for encoding/decoding:
205
- @stream = Stream.new(nil, @file_endian)
206
- # The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
207
- @parent = nil
208
- # For convenience, call the read method if a string has been supplied:
209
- if string.is_a?(String)
210
- @file = string unless options[:bin]
211
- read(string, options)
212
- elsif string
213
- raise ArgumentError, "Invalid argument. Expected String (or nil), got #{string.class}."
214
- end
215
- end
216
-
217
- # Returns true if the argument is an instance with attributes equal to self.
218
- #
219
- def ==(other)
220
- if other.respond_to?(:to_dcm)
221
- other.send(:state) == state
222
- end
223
- end
224
-
225
- alias_method :eql?, :==
226
-
227
- # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
228
- #
229
- # Returns the encoded binary strings in an array.
230
- #
231
- # === Parameters
232
- #
233
- # * <tt>max_size</tt> -- An integer (Fixnum) which specifies the maximum allowed size of the binary data strings which will be encoded.
234
- # * <tt>transfer_syntax</tt> -- 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. Defaults to the DObject's transfer syntax/Implicit little endian.
235
- #
236
- # === Examples
237
- #
238
- # encoded_strings = dcm.encode_segments(16384)
239
- #
240
- def encode_segments(max_size, transfer_syntax=transfer_syntax)
241
- raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
242
- raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
243
- raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
244
- w = DWrite.new(self, transfer_syntax, file_name=nil)
245
- w.encode_segments(max_size)
246
- # Write process succesful?
247
- @write_success = w.success
248
- return w.segments
249
- end
250
-
251
- # Generates a Fixnum hash value for this instance.
252
- #
253
- def hash
254
- state.hash
255
- end
256
-
257
- # Prints information of interest related to the DICOM object.
258
- # Calls the print() method of Parent as well as the information() method of DObject.
259
- #
260
- def print_all
261
- puts ""
262
- print(:value_max => 30)
263
- summary
264
- end
265
-
266
- # Fills a DICOM object by reading and parsing the specified DICOM file,
267
- # and transfers the DICOM data to the DICOM object (self).
268
- #
269
- # === Notes
270
- #
271
- # * This method is called automatically when initializing the DObject class with a file parameter.
272
- # * In practice this method is rarely called by the user, and in fact it may be removed entirely at some stage.
273
- #
274
- # === Parameters
275
- #
276
- # * <tt>string</tt> -- A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
277
- # * <tt>options</tt> -- A hash of parameters.
278
- #
279
- # === Options
280
- #
281
- # * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
282
- # * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
283
- # * <tt>:syntax</tt> -- String. If a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the file/binary string.
284
- #
285
- def read(string, options={})
286
- raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
287
- # Clear any existing DObject tags, then read:
288
- @tags = Hash.new
289
- r = DRead.new(self, string, options)
290
- # If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
291
- # 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.
292
- if !r.success and !exists?("0002,0010")
293
- logger.debug("First attempt at parsing the file failed.\nAttempting a second pass (assuming Explicit Little Endian transfer syntax).")
294
- # Clear the existing DObject tags:
295
- @tags = Hash.new
296
- r_explicit = DRead.new(self, string, :bin => options[:bin], :no_meta => options[:no_meta], :syntax => EXPLICIT_LITTLE_ENDIAN)
297
- # Only extract information from this new attempt if it was successful:
298
- r = r_explicit if r_explicit.success
299
- end
300
- # Pass along any messages that has been recorded:
301
- r.msg.each { |m| logger.public_send(m.first, m.last) }
302
- # Store the data to the instance variables if the readout was a success:
303
- if r.success
304
- logger.info("The DICOM file has been successfully parsed.")
305
- @read_success = true
306
- # Update instance variables based on the properties of the DICOM object:
307
- @explicit = r.explicit
308
- @file_endian = r.file_endian
309
- @signature = r.signature
310
- @stream.endian = @file_endian
311
- else
312
- logger.warn("Parsing the DICOM file has failed.")
313
- @read_success = false
314
- end
315
- end
316
-
317
- # Gathers key information about the DObject as well as some system data, and prints this information to the screen.
318
- #
319
- # This information includes properties like encoding, byte order, modality and various image properties.
320
- #
321
- #--
322
- # FIXME: Perhaps this method should be split up in one or two separate methods
323
- # which just builds the information arrays, and a third method for printing this to the screen.
324
- #
325
- def summary
326
- sys_info = Array.new
327
- info = Array.new
328
- # Version of Ruby DICOM used:
329
- sys_info << "Ruby DICOM version: #{VERSION}"
330
- # System endian:
331
- cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
332
- sys_info << "Byte Order (CPU): #{cpu}"
333
- # File path/name:
334
- info << "File: #{@file}"
335
- # Modality:
336
- modality = (exists?("0008,0016") ? LIBRARY.get_syntax_description(self["0008,0016"].value) : "SOP Class unknown or not specified!")
337
- info << "Modality: #{modality}"
338
- # Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
339
- transfer_syntax = self["0002,0010"]
340
- if transfer_syntax
341
- syntax_validity, explicit, endian = LIBRARY.process_transfer_syntax(transfer_syntax.value)
342
- if syntax_validity
343
- meta_comment, explicit_comment, encoding_comment = "", "", ""
344
- else
345
- meta_comment = " (But unknown/invalid transfer syntax: #{transfer_syntax})"
346
- explicit_comment = " (Assumed)"
347
- encoding_comment = " (Assumed)"
348
- end
349
- explicitness = (explicit ? "Explicit" : "Implicit")
350
- encoding = (endian ? "Big Endian" : "Little Endian")
351
- meta = "Yes#{meta_comment}"
352
- else
353
- meta = "No"
354
- explicitness = (@explicit == true ? "Explicit" : "Implicit")
355
- encoding = (@file_endian == true ? "Big Endian" : "Little Endian")
356
- explicit_comment = " (Assumed)"
357
- encoding_comment = " (Assumed)"
358
- end
359
- info << "Meta Header: #{meta}"
360
- info << "Value Representation: #{explicitness}#{explicit_comment}"
361
- info << "Byte Order (File): #{encoding}#{encoding_comment}"
362
- # Pixel data:
363
- pixels = self[PIXEL_TAG]
364
- unless pixels
365
- info << "Pixel Data: No"
366
- else
367
- info << "Pixel Data: Yes"
368
- # Image size:
369
- cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
370
- rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
371
- info << "Image Size: #{cols}*#{rows}"
372
- # Frames:
373
- frames = value("0028,0008") || "1"
374
- unless frames == "1" or frames == 1
375
- # Encapsulated or 3D pixel data:
376
- if pixels.is_a?(Element)
377
- frames = frames.to_s + " (3D Pixel Data)"
378
- else
379
- frames = frames.to_s + " (Encapsulated Multiframe Image)"
380
- end
381
- end
382
- info << "Number of frames: #{frames}"
383
- # Color:
384
- colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
385
- info << "Photometry: #{colors}"
386
- # Compression:
387
- if transfer_syntax
388
- compression = LIBRARY.get_compression(transfer_syntax.value)
389
- if compression
390
- compression = LIBRARY.get_syntax_description(transfer_syntax.value) || "Unknown UID!"
391
- else
392
- compression = "No"
393
- end
394
- else
395
- compression = "No (Assumed)"
396
- end
397
- info << "Compression: #{compression}"
398
- # Pixel bits (allocated):
399
- bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
400
- info << "Bits per Pixel: #{bits}"
401
- end
402
- # Print the DICOM object's key properties:
403
- separator = "-------------------------------------------"
404
- puts "System Properties:"
405
- puts separator + "\n"
406
- puts sys_info
407
- puts "\n"
408
- puts "DICOM Object Properties:"
409
- puts separator
410
- puts info
411
- puts separator
412
- return info
413
- end
414
-
415
- # Returns self.
416
- #
417
- def to_dcm
418
- self
419
- end
420
-
421
- # Returns the transfer syntax string of the DObject.
422
- #
423
- # If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
424
- #
425
- def transfer_syntax
426
- return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
427
- end
428
-
429
- # Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
430
- # numerical values if a switch of endianness is implied.
431
- #
432
- # === Restrictions
433
- #
434
- # This method does not change the compressed state of the pixel data element. Changing the transfer syntax between
435
- # an uncompressed and compressed state will NOT change the pixel data accordingly (this must be taken care of manually).
436
- #
437
- # === Parameters
438
- #
439
- # * <tt>new_syntax</tt> -- The new transfer syntax string which will be applied to the DObject.
440
- #
441
- def transfer_syntax=(new_syntax)
442
- valid_ts, new_explicit, new_endian = LIBRARY.process_transfer_syntax(new_syntax)
443
- raise ArgumentError, "Invalid transfer syntax specified: #{new_syntax}" unless valid_ts
444
- # Get the old transfer syntax and write the new one to the DICOM object:
445
- old_syntax = transfer_syntax
446
- valid_ts, old_explicit, old_endian = LIBRARY.process_transfer_syntax(old_syntax)
447
- if exists?("0002,0010")
448
- self["0002,0010"].value = new_syntax
449
- else
450
- add(Element.new("0002,0010", new_syntax))
451
- end
452
- # Update our Stream instance with the new encoding:
453
- @stream.endian = new_endian
454
- # If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
455
- encode_children(old_endian) if old_endian != new_endian
456
- end
457
-
458
- # Passes the DObject to the DWrite class, which traverses the data element
459
- # structure and encodes a proper DICOM binary string, which is finally written to the specified file.
460
- #
461
- # === Parameters
462
- #
463
- # * <tt>file_name</tt> -- A string which identifies the path & name of the DICOM file which is to be written to disk.
464
- # * <tt>options</tt> -- A hash of parameters.
465
- #
466
- # === Options
467
- #
468
- # * <tt>:add_meta</tt> -- Boolean. If set to false, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file.
469
- #
470
- # === Examples
471
- #
472
- # dcm.write(path + "test.dcm")
473
- #
474
- def write(file_name, options={})
475
- raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
476
- insert_missing_meta unless options[:add_meta] == false
477
- w = DWrite.new(self, transfer_syntax, file_name, options)
478
- w.write
479
- # Write process succesful?
480
- @write_success = w.success
481
- end
482
-
483
-
484
- # Following methods are private:
485
- private
486
-
487
-
488
- # Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
489
- # to ensure that a valid DICOM object will be written to file.
490
- #
491
- def insert_missing_meta
492
- # File Meta Information Version:
493
- Element.new("0002,0001", [0,1], :parent => self) unless exists?("0002,0001")
494
- # Media Storage SOP Class UID:
495
- Element.new("0002,0002", value("0008,0016"), :parent => self) unless exists?("0002,0002")
496
- # Media Storage SOP Instance UID:
497
- Element.new("0002,0003", value("0008,0018"), :parent => self) unless exists?("0002,0003")
498
- # Transfer Syntax UID:
499
- Element.new("0002,0010", transfer_syntax, :parent => self) unless exists?("0002,0010")
500
- if !exists?("0002,0012") and !exists?("0002,0013")
501
- # Implementation Class UID:
502
- Element.new("0002,0012", UID, :parent => self)
503
- # Implementation Version Name:
504
- Element.new("0002,0013", NAME, :parent => self)
505
- end
506
- # Source Application Entity Title:
507
- Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
508
- # Group Length: Delete the old one (if it exists) before creating a new one.
509
- delete("0002,0000")
510
- Element.new("0002,0000", meta_group_length, :parent => self)
511
- end
512
-
513
- # Determines and returns the length of the meta group in the DObject instance.
514
- #
515
- def meta_group_length
516
- group_length = 0
517
- meta_elements = group(META_GROUP)
518
- tag = 4
519
- vr = 2
520
- meta_elements.each do |element|
521
- case element.vr
522
- when "OB","OW","OF","SQ","UN","UT"
523
- length = 6
524
- else
525
- length = 2
526
- end
527
- group_length += tag + vr + length + element.bin.length
528
- end
529
- return group_length
530
- end
531
-
532
- # Returns the attributes (children) of this instance (for comparison purposes).
533
- #
534
- def state
535
- @tags
536
- end
537
-
538
- end
539
- end
1
+ # === TODO:
2
+ #
3
+ # * The retrieve file network functionality (#get_image in DClient class) has not been tested.
4
+ # * Make the networking code more intelligent in its handling of unexpected network communication.
5
+ # * Full support for compressed image data.
6
+ # * Read/Write 12 bit image data.
7
+ # * Full color support (RGB and PALETTE COLOR with #image already implemented).
8
+ # * Support for extraction of multiple encapsulated pixel data frames in #pixels and #narray.
9
+ # * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
10
+ # * More robust and flexible options for reorienting extracted pixel arrays?
11
+ # * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
12
+ # * Add these as github issues and remove this list!
13
+
14
+
15
+ # Copyright 2008-2012 Christoffer Lervag
16
+ #
17
+ # This program is free software: you can redistribute it and/or modify
18
+ # it under the terms of the GNU General Public License as published by
19
+ # the Free Software Foundation, either version 3 of the License, or
20
+ # (at your option) any later version.
21
+ #
22
+ # This program is distributed in the hope that it will be useful,
23
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
24
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
+ # GNU General Public License for more details.
26
+ #
27
+ # You should have received a copy of the GNU General Public License
28
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
29
+ #
30
+ module DICOM
31
+
32
+ # The DObject class is the main class for interacting with the DICOM object.
33
+ # Reading from and writing to files is executed from instances of this class.
34
+ #
35
+ # === Inheritance
36
+ #
37
+ # As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class,
38
+ # all ImageItem and Parent methods are also available to instances of DObject.
39
+ #
40
+ class DObject < ImageItem
41
+ include Logging
42
+
43
+ # An attribute set as nil. This attribute is included to provide consistency with the other element types which usually have a parent defined.
44
+ attr_reader :parent
45
+ # A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
46
+ attr_accessor :read_success
47
+ # The Stream instance associated with this DObject instance (this attribute is mostly used internally).
48
+ attr_reader :stream
49
+ # A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
50
+ attr_reader :write_success
51
+
52
+ alias_method :read?, :read_success
53
+ alias_method :written?, :write_success
54
+
55
+ # Creates a DObject instance by downloading a DICOM file
56
+ # specified by a hyperlink, and parsing the retrieved file.
57
+ #
58
+ # @note Highly experimental and un-tested!
59
+ # @note Designed for the HTTP protocol only.
60
+ # @note Whether this method should be included or removed from ruby-dicom is up for debate.
61
+ #
62
+ # @param [String] link a hyperlink string which specifies remote location of the DICOM file to be loaded.
63
+ # @return [DObject] the created DObject instance
64
+ #
65
+ def self.get(link)
66
+ raise ArgumentError, "Invalid argument 'link'. Expected String, got #{link.class}." unless link.is_a?(String)
67
+ raise ArgumentError, "Invalid argument 'link'. Expected a string starting with 'http', got #{link}." unless link.index('http') == 0
68
+ require 'open-uri'
69
+ bin = nil
70
+ file = nil
71
+ # Try to open the remote file using open-uri:
72
+ retrials = 0
73
+ begin
74
+ file = open(link, 'rb') # binary encoding (ASCII-8BIT)
75
+ rescue Exception => e
76
+ if retrials > 3
77
+ retrials = 0
78
+ raise "Unable to retrieve the file. File does not exist?"
79
+ else
80
+ logger.warn("Exception in ruby-dicom when loading a dicom file from: #{file}")
81
+ logger.debug("Retrying... #{retrials}")
82
+ retrials += 1
83
+ retry
84
+ end
85
+ end
86
+ bin = File.open(file, "rb") { |f| f.read }
87
+ # Parse the file contents and create the DICOM object:
88
+ if bin
89
+ dcm = self.parse(bin)
90
+ else
91
+ dcm = self.new
92
+ dcm.read_success = false
93
+ end
94
+ return dcm
95
+ end
96
+
97
+ # Creates a DObject instance by parsing an encoded binary DICOM string.
98
+ #
99
+ # @param [String] string an encoded binary string containing DICOM information
100
+ # @param [Hash] options the options to use for parsing the DICOM string
101
+ # @option options [Boolean] :signature if set as false, the parsing algorithm will not be looking for the DICOM header signature (defaults to true)
102
+ # @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
103
+ # @example Parse a DICOM file that has already been loaded to a binary string
104
+ # require 'dicom'
105
+ # dcm = DICOM::DObject.parse(str)
106
+ # @example Parse a header-less DICOM string with explicit little endian transfer syntax
107
+ # dcm = DICOM::DObject.parse(str, :syntax => '1.2.840.10008.1.2.1')
108
+ #
109
+ def self.parse(string, options={})
110
+ raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
111
+ raise ArgumentError, "Invalid option :syntax. Expected String, got #{options[:syntax].class}." if options[:syntax] && !options[:syntax].is_a?(String)
112
+ signature = options[:signature].nil? ? true : options[:signature]
113
+ dcm = self.new
114
+ dcm.send(:read, string, signature, :syntax => options[:syntax])
115
+ if dcm.read?
116
+ logger.debug("DICOM string successfully parsed.")
117
+ else
118
+ logger.warn("Failed to parse this string as DICOM.")
119
+ end
120
+ return dcm
121
+ end
122
+
123
+ # Creates a DObject instance by reading and parsing a DICOM file.
124
+ #
125
+ # @param [String] file a string which specifies the path of the DICOM file to be loaded
126
+ # @example Load a DICOM file
127
+ # require 'dicom'
128
+ # dcm = DICOM::DObject.read('test.dcm')
129
+ #
130
+ def self.read(file)
131
+ raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
132
+ # Read the file content:
133
+ bin = nil
134
+ unless File.exist?(file)
135
+ logger.error("Invalid (non-existing) file: #{file}")
136
+ else
137
+ unless File.readable?(file)
138
+ logger.error("File exists but I don't have permission to read it: #{file}")
139
+ else
140
+ if File.directory?(file)
141
+ logger.error("Expected a file, got a directory: #{file}")
142
+ else
143
+ if File.size(file) < 8
144
+ logger.error("This file is too small to contain any DICOM information: #{file}.")
145
+ else
146
+ bin = File.open(file, "rb") { |f| f.read }
147
+ end
148
+ end
149
+ end
150
+ end
151
+ # Parse the file contents and create the DICOM object:
152
+ if bin
153
+ dcm = self.parse(bin)
154
+ # If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
155
+ # 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.
156
+ if !dcm.read? and !dcm.exists?("0002,0010")
157
+ logger.info("Attempting a second decode pass (assuming Explicit Little Endian transfer syntax).")
158
+ dcm = self.parse(bin, :syntax => EXPLICIT_LITTLE_ENDIAN)
159
+ end
160
+ else
161
+ dcm = self.new
162
+ end
163
+ if dcm.read?
164
+ logger.info("DICOM file successfully read: #{file}")
165
+ else
166
+ logger.warn("Reading DICOM file failed: #{file}")
167
+ end
168
+ return dcm
169
+ end
170
+
171
+ # Creates a DObject instance (DObject is an abbreviation for "DICOM object").
172
+ #
173
+ # The DObject instance holds references to the different types of objects (Element, Item, Sequence)
174
+ # that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a binary
175
+ # string (with DObject::read or ::parse), but can also be buildt from an empty state by this method.
176
+ #
177
+ # To customize logging behaviour, refer to the Logging module documentation.
178
+ #
179
+ # @example Create an empty DICOM object
180
+ # require 'dicom'
181
+ # dcm = DICOM::DObject.new
182
+ # @example Increasing the log message threshold (default level is INFO)
183
+ # DICOM.logger.level = Logger::ERROR
184
+ #
185
+ def initialize
186
+ # Initialization of variables that DObject share with other parent elements:
187
+ initialize_parent
188
+ # Structural information (default values):
189
+ @explicit = true
190
+ @str_endian = false
191
+ # Control variables:
192
+ @read_success = nil
193
+ # Initialize a Stream instance which is used for encoding/decoding:
194
+ @stream = Stream.new(nil, @str_endian)
195
+ # The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
196
+ @parent = nil
197
+ end
198
+
199
+ # Checks for equality.
200
+ #
201
+ # Other and self are considered equivalent if they are
202
+ # of compatible types and their attributes are equivalent.
203
+ #
204
+ # @param other an object to be compared with self.
205
+ # @return [Boolean] true if self and other are considered equivalent
206
+ #
207
+ def ==(other)
208
+ if other.respond_to?(:to_dcm)
209
+ other.send(:state) == state
210
+ end
211
+ end
212
+
213
+ alias_method :eql?, :==
214
+
215
+ # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
216
+ #
217
+ # Returns the encoded binary strings in an array.
218
+ #
219
+ # @param [Integer] max_size the maximum allowed size of the binary data strings to be encoded
220
+ # @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.
221
+ # @return [Array<String>] the encoded DICOM strings
222
+ # @example Encode the DObject to strings of max length 2^14 bytes
223
+ # encoded_strings = dcm.encode_segments(16384)
224
+ #
225
+ def encode_segments(max_size, transfer_syntax=IMPLICIT_LITTLE_ENDIAN)
226
+ raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
227
+ raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
228
+ raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
229
+ encode_in_segments(max_size, :syntax => transfer_syntax)
230
+ end
231
+
232
+ # Computes a hash code for this object.
233
+ #
234
+ # @note Two objects with the same attributes will have the same hash code.
235
+ #
236
+ # @return [Fixnum] the object's hash code
237
+ #
238
+ def hash
239
+ state.hash
240
+ end
241
+
242
+ # Prints information of interest related to the DICOM object.
243
+ # Calls the Parent#print method as well as DObject#summary.
244
+ #
245
+ def print_all
246
+ puts ""
247
+ print(:value_max => 30)
248
+ summary
249
+ end
250
+
251
+ # Gathers key information about the DObject as well as some system data, and prints this information to the screen.
252
+ # This information includes properties like encoding, byte order, modality and various image properties.
253
+ #
254
+ # @return [Array<String>] strings describing the properties of the DICOM object
255
+ #
256
+ def summary
257
+ # FIXME: Perhaps this method should be split up in one or two separate methods
258
+ # which just builds the information arrays, and a third method for printing this to the screen.
259
+ sys_info = Array.new
260
+ info = Array.new
261
+ # Version of Ruby DICOM used:
262
+ sys_info << "Ruby DICOM version: #{VERSION}"
263
+ # System endian:
264
+ cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
265
+ sys_info << "Byte Order (CPU): #{cpu}"
266
+ # File path/name:
267
+ info << "File: #{@file}"
268
+ # Modality:
269
+ modality = (LIBRARY.uid(value('0008,0016')) ? LIBRARY.uid(value('0008,0016')).name : "SOP Class unknown or not specified!")
270
+ info << "Modality: #{modality}"
271
+ # Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
272
+ ts_status = self['0002,0010'] ? '' : ' (Assumed)'
273
+ ts = LIBRARY.uid(transfer_syntax)
274
+ explicit = ts ? ts.explicit? : true
275
+ endian = ts ? ts.big_endian? : false
276
+ meta_comment = ts ? "" : " (But unknown/invalid transfer syntax: #{transfer_syntax})"
277
+ info << "Meta Header: #{self['0002,0010'] ? 'Yes' : 'No'}#{meta_comment}"
278
+ info << "Value Representation: #{explicit ? 'Explicit' : 'Implicit'}#{ts_status}"
279
+ info << "Byte Order (File): #{endian ? 'Big Endian' : 'Little Endian'}#{ts_status}"
280
+ # Pixel data:
281
+ pixels = self[PIXEL_TAG]
282
+ unless pixels
283
+ info << "Pixel Data: No"
284
+ else
285
+ info << "Pixel Data: Yes"
286
+ # Image size:
287
+ cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
288
+ rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
289
+ info << "Image Size: #{cols}*#{rows}"
290
+ # Frames:
291
+ frames = value("0028,0008") || "1"
292
+ unless frames == "1" or frames == 1
293
+ # Encapsulated or 3D pixel data:
294
+ if pixels.is_a?(Element)
295
+ frames = frames.to_s + " (3D Pixel Data)"
296
+ else
297
+ frames = frames.to_s + " (Encapsulated Multiframe Image)"
298
+ end
299
+ end
300
+ info << "Number of frames: #{frames}"
301
+ # Color:
302
+ colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
303
+ info << "Photometry: #{colors}"
304
+ # Compression:
305
+ compression = (ts ? (ts.compressed_pixels? ? ts.name : 'No') : 'No' )
306
+ info << "Compression: #{compression}#{ts_status}"
307
+ # Pixel bits (allocated):
308
+ bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
309
+ info << "Bits per Pixel: #{bits}"
310
+ end
311
+ # Print the DICOM object's key properties:
312
+ separator = "-------------------------------------------"
313
+ puts "System Properties:"
314
+ puts separator + "\n"
315
+ puts sys_info
316
+ puts "\n"
317
+ puts "DICOM Object Properties:"
318
+ puts separator
319
+ puts info
320
+ puts separator
321
+ return info
322
+ end
323
+
324
+ # Returns self.
325
+ #
326
+ # @return [DObject] self
327
+ #
328
+ def to_dcm
329
+ self
330
+ end
331
+
332
+ # Gives the transfer syntax string of the DObject.
333
+ #
334
+ # If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
335
+ #
336
+ # @return [String] the DObject's transfer syntax
337
+ #
338
+ def transfer_syntax
339
+ return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
340
+ end
341
+
342
+ # Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
343
+ # numerical values if a switch of endianness is implied.
344
+ #
345
+ # @note This method does not change the compressed state of the pixel data element. Changing
346
+ # the transfer syntax between an uncompressed and compressed state will NOT change the pixel
347
+ # data accordingly (this must be taken care of manually).
348
+ #
349
+ # @param [String] new_syntax the new transfer syntax string to be applied to the DObject
350
+ #
351
+ def transfer_syntax=(new_syntax)
352
+ # Verify old and new transfer syntax:
353
+ new_uid = LIBRARY.uid(new_syntax)
354
+ old_uid = LIBRARY.uid(transfer_syntax)
355
+ raise ArgumentError, "Invalid/unknown transfer syntax specified: #{new_syntax}" unless new_uid && new_uid.transfer_syntax?
356
+ 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?
357
+ # Set the new transfer syntax:
358
+ if exists?("0002,0010")
359
+ self["0002,0010"].value = new_syntax
360
+ else
361
+ add(Element.new("0002,0010", new_syntax))
362
+ end
363
+ # Update our Stream instance with the new encoding:
364
+ @stream.endian = new_uid.big_endian?
365
+ # If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
366
+ encode_children(old_uid.big_endian?) if old_uid.big_endian? != new_uid.big_endian?
367
+ end
368
+
369
+ # Writes the DICOM object to file.
370
+ #
371
+ # @note The goal of the Ruby DICOM library is to yield maximum conformance with the DICOM
372
+ # standard when outputting DICOM files. Therefore, when encoding the DICOM file, manipulation
373
+ # of items such as the meta group, group lengths and header signature may occur. Therefore,
374
+ # the file that is written may not be an exact bitwise copy of the file that was read, even if no
375
+ # DObject manipulation has been done by the user.
376
+ #
377
+ # @param [String] file_name the path of the DICOM file which is to be written to disk
378
+ # @param [Hash] options the options to use for writing the DICOM file
379
+ # @option options [Boolean] :add_meta <DEPRECATED> if set to false, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file
380
+ # @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
381
+ # @example Encode a DICOM file from a DObject
382
+ # dcm.write('C:/dicom/test.dcm')
383
+ #
384
+ def write(file_name, options={})
385
+ logger.warn("Option :add_meta => false is deprecated. Use option :ignore_meta => true instead.") if options[:add_meta] == false
386
+ raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
387
+ insert_missing_meta unless options[:add_meta] == false or options[:ignore_meta]
388
+ write_elements(:file_name => file_name, :signature => true, :syntax => transfer_syntax)
389
+ end
390
+
391
+
392
+ private
393
+
394
+
395
+ # Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
396
+ # to ensure that a valid DICOM object is encoded.
397
+ #
398
+ def insert_missing_meta
399
+ # File Meta Information Version:
400
+ Element.new("0002,0001", [0,1], :parent => self) unless exists?("0002,0001")
401
+ # Media Storage SOP Class UID:
402
+ Element.new("0002,0002", value("0008,0016"), :parent => self) unless exists?("0002,0002")
403
+ # Media Storage SOP Instance UID:
404
+ Element.new("0002,0003", value("0008,0018"), :parent => self) unless exists?("0002,0003")
405
+ # Transfer Syntax UID:
406
+ Element.new("0002,0010", transfer_syntax, :parent => self) unless exists?("0002,0010")
407
+ if !exists?("0002,0012") and !exists?("0002,0013")
408
+ # Implementation Class UID:
409
+ Element.new("0002,0012", UID_ROOT, :parent => self)
410
+ # Implementation Version Name:
411
+ Element.new("0002,0013", NAME, :parent => self)
412
+ end
413
+ # Source Application Entity Title:
414
+ Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
415
+ # Group Length: Delete the old one (if it exists) before creating a new one.
416
+ delete("0002,0000")
417
+ Element.new("0002,0000", meta_group_length, :parent => self)
418
+ end
419
+
420
+ # Determines the length of the meta group in the DObject instance.
421
+ #
422
+ # @return [Integer] the length of the file meta group string
423
+ #
424
+ def meta_group_length
425
+ group_length = 0
426
+ meta_elements = group(META_GROUP)
427
+ tag = 4
428
+ vr = 2
429
+ meta_elements.each do |element|
430
+ case element.vr
431
+ when "OB","OW","OF","SQ","UN","UT"
432
+ length = 6
433
+ else
434
+ length = 2
435
+ end
436
+ group_length += tag + vr + length + element.bin.length
437
+ end
438
+ return group_length
439
+ end
440
+
441
+ # Collects the attributes of this instance.
442
+ #
443
+ # @return [Array<Element, Sequence>] an array of elements and sequences
444
+ #
445
+ def state
446
+ @tags
447
+ end
448
+
449
+ end
450
+
451
+ end