dicom 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,416 +1,511 @@
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 get_object_magick() already implemented).
8
- # * Support for extraction of multiple encapsulated pixel data frames in get_image() and get_image_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-2011 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
-
42
- # An array which contain any notices/warnings/errors that have been recorded for the DObject instance.
43
- attr_reader :errors
44
- # A boolean set as false. This attribute is included to provide consistency with other object types for the internal methods which use it.
45
- attr_reader :parent
46
- # A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
47
- attr_reader :read_success
48
- # The Stream instance associated with this DObject instance (this attribute is mostly used internally).
49
- attr_reader :stream
50
- # A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
51
- attr_reader :write_success
52
-
53
- alias_method :read?, :read_success
54
- alias_method :written?, :write_success
55
-
56
- # Creates a DObject instance (DObject is an abbreviation for "DICOM object").
57
- #
58
- # The DObject instance holds references to the different types of objects (Element, Item, Sequence)
59
- # that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a
60
- # binary string, but can also be buildt from an empty state by the user.
61
- #
62
- # === Parameters
63
- #
64
- # * <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. The parameter defaults to nil, in which case an empty DObject instance is created.
65
- # * <tt>options</tt> -- A hash of parameters.
66
- #
67
- # === Options
68
- #
69
- # * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
70
- # * <tt>:syntax</tt> -- String. If a syntax string is specified, the DRead class will be forced to use this transfer syntax when decoding the file/binary string.
71
- # * <tt>:verbose</tt> -- Boolean. If set to false, the DObject instance will run silently and not output warnings and error messages to the screen. Defaults to true.
72
- #
73
- # === Examples
74
- #
75
- # # Load a DICOM file:
76
- # require 'dicom'
77
- # obj = DICOM::DObject.new("test.dcm")
78
- # # Read a DICOM file that has already been loaded into memory in a binary string (with a known transfer syntax):
79
- # obj = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
80
- # # Create an empty DICOM object & choose non-verbose behaviour:
81
- # obj = DICOM::DObject.new(nil, :verbose => false)
82
- #
83
- def initialize(string=nil, options={})
84
- # Process option values, setting defaults for the ones that are not specified:
85
- # Default verbosity is true if verbosity hasn't been specified (nil):
86
- @verbose = (options[:verbose] == false ? false : true)
87
- # Initialization of variables that DObject share with other parent elements:
88
- initialize_parent
89
- # Messages (errors, warnings or notices) will be accumulated in an array:
90
- @errors = Array.new
91
- # Structural information (default values):
92
- @explicit = true
93
- @file_endian = false
94
- # Control variables:
95
- @read_success = nil
96
- # Initialize a Stream instance which is used for encoding/decoding:
97
- @stream = Stream.new(nil, @file_endian)
98
- # The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
99
- @parent = nil
100
- # For convenience, call the read method if a string has been supplied:
101
- if string.is_a?(String)
102
- @file = string unless options[:bin]
103
- read(string, options)
104
- elsif string
105
- raise ArgumentError, "Invalid argument. Expected String (or nil), got #{string.class}."
106
- end
107
- end
108
-
109
- # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
110
- #
111
- # Returns the encoded binary strings in an array.
112
- #
113
- # === Parameters
114
- #
115
- # * <tt>max_size</tt> -- An integer (Fixnum) which specifies the maximum allowed size of the binary data strings which will be encoded.
116
- # * <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.
117
- #
118
- # === Examples
119
- #
120
- # encoded_strings = obj.encode_segments(16384)
121
- #
122
- def encode_segments(max_size, transfer_syntax=transfer_syntax)
123
- raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
124
- raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
125
- raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
126
- w = DWrite.new(self, transfer_syntax, file_name=nil)
127
- w.encode_segments(max_size)
128
- # Write process succesful?
129
- @write_success = w.success
130
- # If any messages has been recorded, send these to the message handling method:
131
- add_msg(w.msg) if w.msg.length > 0
132
- return w.segments
133
- end
134
-
135
- # Prints information of interest related to the DICOM object.
136
- # Calls the print() method of Parent as well as the information() method of DObject.
137
- #
138
- def print_all
139
- puts ""
140
- print(:value_max => 30)
141
- summary
142
- end
143
-
144
- # Fills a DICOM object by reading and parsing the specified DICOM file,
145
- # and transfers the DICOM data to the DICOM object (self).
146
- #
147
- # === Notes
148
- #
149
- # This method is called automatically when initializing the DObject class with a file parameter.
150
- # In practice this method is rarely called by the user.
151
- #
152
- # === Parameters
153
- #
154
- # * <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.
155
- # * <tt>options</tt> -- A hash of parameters.
156
- #
157
- # === Options
158
- #
159
- # * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
160
- # * <tt>:syntax</tt> -- String. If a syntax string is specified, the DRead class will be forced to use this transfer syntax when decoding the file/binary string.
161
- #
162
- def read(string, options={})
163
- raise ArgumentError, "Invalid argument. Expected String, got #{string.class}." unless string.is_a?(String)
164
- # Clear any existing DObject tags, then read:
165
- @tags = Hash.new
166
- r = DRead.new(self, string, options)
167
- # If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
168
- # 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.
169
- if !r.success and !exists?("0002,0010")
170
- # Clear the existing DObject tags:
171
- @tags = Hash.new
172
- r_explicit = DRead.new(self, string, :bin => options[:bin], :syntax => EXPLICIT_LITTLE_ENDIAN)
173
- # Only extract information from this new attempt if it was successful:
174
- r = r_explicit if r_explicit.success
175
- end
176
- # Store the data to the instance variables if the readout was a success:
177
- if r.success
178
- @read_success = true
179
- # Update instance variables based on the properties of the DICOM object:
180
- @explicit = r.explicit
181
- @file_endian = r.file_endian
182
- @signature = r.signature
183
- @stream.endian = @file_endian
184
- else
185
- @read_success = false
186
- end
187
- # If any messages has been recorded, send these to the message handling method:
188
- add_msg(r.msg) if r.msg.length > 0
189
- end
190
-
191
- # Gathers key information about the DObject as well as some system data, and prints this information to the screen.
192
- #
193
- # This information includes properties like encoding, byte order, modality and various image properties.
194
- #
195
- #--
196
- # FIXME: Perhaps this method should be split up in one or two separate methods
197
- # which just builds the information arrays, and a third method for printing this to the screen.
198
- #
199
- def summary
200
- sys_info = Array.new
201
- info = Array.new
202
- # Version of Ruby DICOM used:
203
- sys_info << "Ruby DICOM version: #{VERSION}"
204
- # System endian:
205
- cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
206
- sys_info << "Byte Order (CPU): #{cpu}"
207
- # File path/name:
208
- info << "File: #{@file}"
209
- # Modality:
210
- modality = (exists?("0008,0016") ? LIBRARY.get_syntax_description(self["0008,0016"].value) : "SOP Class unknown or not specified!")
211
- info << "Modality: #{modality}"
212
- # Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
213
- transfer_syntax = self["0002,0010"]
214
- if transfer_syntax
215
- syntax_validity, explicit, endian = LIBRARY.process_transfer_syntax(transfer_syntax.value)
216
- if syntax_validity
217
- meta_comment, explicit_comment, encoding_comment = "", "", ""
218
- else
219
- meta_comment = " (But unknown/invalid transfer syntax: #{transfer_syntax})"
220
- explicit_comment = " (Assumed)"
221
- encoding_comment = " (Assumed)"
222
- end
223
- explicitness = (explicit ? "Explicit" : "Implicit")
224
- encoding = (endian ? "Big Endian" : "Little Endian")
225
- meta = "Yes#{meta_comment}"
226
- else
227
- meta = "No"
228
- explicitness = (@explicit == true ? "Explicit" : "Implicit")
229
- encoding = (@file_endian == true ? "Big Endian" : "Little Endian")
230
- explicit_comment = " (Assumed)"
231
- encoding_comment = " (Assumed)"
232
- end
233
- info << "Meta Header: #{meta}"
234
- info << "Value Representation: #{explicitness}#{explicit_comment}"
235
- info << "Byte Order (File): #{encoding}#{encoding_comment}"
236
- # Pixel data:
237
- pixels = self[PIXEL_TAG]
238
- unless pixels
239
- info << "Pixel Data: No"
240
- else
241
- info << "Pixel Data: Yes"
242
- # Image size:
243
- cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
244
- rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
245
- info << "Image Size: #{cols}*#{rows}"
246
- # Frames:
247
- frames = value("0028,0008") || "1"
248
- unless frames == "1" or frames == 1
249
- # Encapsulated or 3D pixel data:
250
- if pixels.is_a?(Element)
251
- frames = frames.to_s + " (3D Pixel Data)"
252
- else
253
- frames = frames.to_s + " (Encapsulated Multiframe Image)"
254
- end
255
- end
256
- info << "Number of frames: #{frames}"
257
- # Color:
258
- colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
259
- info << "Photometry: #{colors}"
260
- # Compression:
261
- if transfer_syntax
262
- compression = LIBRARY.get_compression(transfer_syntax.value)
263
- if compression
264
- compression = LIBRARY.get_syntax_description(transfer_syntax.value) || "Unknown UID!"
265
- else
266
- compression = "No"
267
- end
268
- else
269
- compression = "No (Assumed)"
270
- end
271
- info << "Compression: #{compression}"
272
- # Pixel bits (allocated):
273
- bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
274
- info << "Bits per Pixel: #{bits}"
275
- end
276
- # Print the DICOM object's key properties:
277
- separator = "-------------------------------------------"
278
- puts "System Properties:"
279
- puts separator + "\n"
280
- puts sys_info
281
- puts "\n"
282
- puts "DICOM Object Properties:"
283
- puts separator
284
- puts info
285
- puts separator
286
- return info
287
- end
288
-
289
- # Returns the transfer syntax string of the DObject.
290
- #
291
- # If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
292
- #
293
- def transfer_syntax
294
- return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
295
- end
296
-
297
- # Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
298
- # numerical values if a switch of endianness is implied.
299
- #
300
- # === Restrictions
301
- #
302
- # This method does not change the compressed state of the pixel data element. Changing the transfer syntax between
303
- # an uncompressed and compressed state will NOT change the pixel data accordingly (this must be taken care of manually).
304
- #
305
- # === Parameters
306
- #
307
- # * <tt>new_syntax</tt> -- The new transfer syntax string which will be applied to the DObject.
308
- #
309
- def transfer_syntax=(new_syntax)
310
- valid_ts, new_explicit, new_endian = LIBRARY.process_transfer_syntax(new_syntax)
311
- raise ArgumentError, "Invalid transfer syntax specified: #{new_syntax}" unless valid_ts
312
- # Get the old transfer syntax and write the new one to the DICOM object:
313
- old_syntax = transfer_syntax
314
- valid_ts, old_explicit, old_endian = LIBRARY.process_transfer_syntax(old_syntax)
315
- if exists?("0002,0010")
316
- self["0002,0010"].value = new_syntax
317
- else
318
- add(Element.new("0002,0010", new_syntax))
319
- end
320
- # Update our Stream instance with the new encoding:
321
- @stream.endian = new_endian
322
- # If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
323
- encode_children(old_endian) if old_endian != new_endian
324
- end
325
-
326
- # Passes the DObject to the DWrite class, which traverses the data element
327
- # structure and encodes a proper DICOM binary string, which is finally written to the specified file.
328
- #
329
- # === Parameters
330
- #
331
- # * <tt>file_name</tt> -- A string which identifies the path & name of the DICOM file which is to be written to disk.
332
- # * <tt>options</tt> -- A hash of parameters.
333
- #
334
- # === Options
335
- #
336
- # * <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.
337
- #
338
- # === Examples
339
- #
340
- # obj.write(path + "test.dcm")
341
- #
342
- def write(file_name, options={})
343
- raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
344
- insert_missing_meta unless options[:add_meta] == false
345
- w = DWrite.new(self, transfer_syntax, file_name, options)
346
- w.write
347
- # Write process succesful?
348
- @write_success = w.success
349
- # If any messages has been recorded, send these to the message handling method:
350
- add_msg(w.msg) if w.msg.length > 0
351
- end
352
-
353
-
354
- # Following methods are private:
355
- private
356
-
357
-
358
- # Adds one or more status messages to the instance array holding messages, and if the verbose instance variable
359
- # is true, the status message(s) are printed to the screen as well.
360
- #
361
- # === Parameters
362
- #
363
- # * <tt>msg</tt> -- Status message string, or an array containing one or more status message strings.
364
- #
365
- def add_msg(msg)
366
- puts msg if @verbose
367
- @errors << msg
368
- @errors.flatten!
369
- end
370
-
371
- # Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
372
- # to ensure that a valid DICOM object will be written to file.
373
- #
374
- def insert_missing_meta
375
- # File Meta Information Version:
376
- Element.new("0002,0001", [0,1], :parent => self) unless exists?("0002,0001")
377
- # Media Storage SOP Class UID:
378
- Element.new("0002,0002", value("0008,0016"), :parent => self) unless exists?("0002,0002")
379
- # Media Storage SOP Instance UID:
380
- Element.new("0002,0003", value("0008,0018"), :parent => self) unless exists?("0002,0003")
381
- # Transfer Syntax UID:
382
- Element.new("0002,0010", transfer_syntax, :parent => self) unless exists?("0002,0010")
383
- if !exists?("0002,0012") and !exists?("0002,0013")
384
- # Implementation Class UID:
385
- Element.new("0002,0012", UID, :parent => self)
386
- # Implementation Version Name:
387
- Element.new("0002,0013", NAME, :parent => self)
388
- end
389
- # Source Application Entity Title:
390
- Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
391
- # Group Length: Remove the old one (if it exists) before creating a new one.
392
- remove("0002,0000")
393
- Element.new("0002,0000", meta_group_length, :parent => self)
394
- end
395
-
396
- # Determines and returns the length of the meta group in the DObject instance.
397
- #
398
- def meta_group_length
399
- group_length = 0
400
- meta_elements = group(META_GROUP)
401
- tag = 4
402
- vr = 2
403
- meta_elements.each do |element|
404
- case element.vr
405
- when "OB","OW","OF","SQ","UN","UT"
406
- length = 6
407
- else
408
- length = 2
409
- end
410
- group_length += tag + vr + length + element.bin.length
411
- end
412
- return group_length
413
- end
414
-
415
- end
416
- 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 get_object_magick() already implemented).
8
+ # * Support for extraction of multiple encapsulated pixel data frames in get_image() and get_image_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-2011 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
+ obj = self.parse(bin)
81
+ else
82
+ obj = self.new
83
+ obj.read_success = false
84
+ end
85
+ return obj
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
+ obj = self.new
106
+ obj.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
107
+ return obj
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
+ obj = self.parse(bin)
140
+ else
141
+ obj = self.new
142
+ obj.read_success = false
143
+ end
144
+ return obj
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
+ # obj = 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
+ # obj = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
187
+ # # Create an empty DICOM object
188
+ # obj = 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
+ # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
218
+ #
219
+ # Returns the encoded binary strings in an array.
220
+ #
221
+ # === Parameters
222
+ #
223
+ # * <tt>max_size</tt> -- An integer (Fixnum) which specifies the maximum allowed size of the binary data strings which will be encoded.
224
+ # * <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.
225
+ #
226
+ # === Examples
227
+ #
228
+ # encoded_strings = obj.encode_segments(16384)
229
+ #
230
+ def encode_segments(max_size, transfer_syntax=transfer_syntax)
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
+ w = DWrite.new(self, transfer_syntax, file_name=nil)
235
+ w.encode_segments(max_size)
236
+ # Write process succesful?
237
+ @write_success = w.success
238
+ return w.segments
239
+ end
240
+
241
+ # Prints information of interest related to the DICOM object.
242
+ # Calls the print() method of Parent as well as the information() method of DObject.
243
+ #
244
+ def print_all
245
+ puts ""
246
+ print(:value_max => 30)
247
+ summary
248
+ end
249
+
250
+ # Fills a DICOM object by reading and parsing the specified DICOM file,
251
+ # and transfers the DICOM data to the DICOM object (self).
252
+ #
253
+ # === Notes
254
+ #
255
+ # * This method is called automatically when initializing the DObject class with a file parameter.
256
+ # * In practice this method is rarely called by the user, and in fact it may be removed entirely at some stage.
257
+ #
258
+ # === Parameters
259
+ #
260
+ # * <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.
261
+ # * <tt>options</tt> -- A hash of parameters.
262
+ #
263
+ # === Options
264
+ #
265
+ # * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
266
+ # * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
267
+ # * <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.
268
+ #
269
+ def read(string, options={})
270
+ raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
271
+ # Clear any existing DObject tags, then read:
272
+ @tags = Hash.new
273
+ r = DRead.new(self, string, options)
274
+ # If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
275
+ # 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.
276
+ if !r.success and !exists?("0002,0010")
277
+ logger.debug("First attempt at parsing the file failed.\nAttempting a second pass (assuming Explicit Little Endian transfer syntax).")
278
+ # Clear the existing DObject tags:
279
+ @tags = Hash.new
280
+ r_explicit = DRead.new(self, string, :bin => options[:bin], :no_meta => options[:no_meta], :syntax => EXPLICIT_LITTLE_ENDIAN)
281
+ # Only extract information from this new attempt if it was successful:
282
+ r = r_explicit if r_explicit.success
283
+ end
284
+ # Pass along any messages that has been recorded:
285
+ r.msg.each { |m| logger.public_send(m.first, m.last) }
286
+ # Store the data to the instance variables if the readout was a success:
287
+ if r.success
288
+ logger.info("The DICOM file has been successfully parsed.")
289
+ @read_success = true
290
+ # Update instance variables based on the properties of the DICOM object:
291
+ @explicit = r.explicit
292
+ @file_endian = r.file_endian
293
+ @signature = r.signature
294
+ @stream.endian = @file_endian
295
+ else
296
+ logger.warn("Parsing the DICOM file has failed.")
297
+ @read_success = false
298
+ end
299
+ end
300
+
301
+ # Gathers key information about the DObject as well as some system data, and prints this information to the screen.
302
+ #
303
+ # This information includes properties like encoding, byte order, modality and various image properties.
304
+ #
305
+ #--
306
+ # FIXME: Perhaps this method should be split up in one or two separate methods
307
+ # which just builds the information arrays, and a third method for printing this to the screen.
308
+ #
309
+ def summary
310
+ sys_info = Array.new
311
+ info = Array.new
312
+ # Version of Ruby DICOM used:
313
+ sys_info << "Ruby DICOM version: #{VERSION}"
314
+ # System endian:
315
+ cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
316
+ sys_info << "Byte Order (CPU): #{cpu}"
317
+ # File path/name:
318
+ info << "File: #{@file}"
319
+ # Modality:
320
+ modality = (exists?("0008,0016") ? LIBRARY.get_syntax_description(self["0008,0016"].value) : "SOP Class unknown or not specified!")
321
+ info << "Modality: #{modality}"
322
+ # Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
323
+ transfer_syntax = self["0002,0010"]
324
+ if transfer_syntax
325
+ syntax_validity, explicit, endian = LIBRARY.process_transfer_syntax(transfer_syntax.value)
326
+ if syntax_validity
327
+ meta_comment, explicit_comment, encoding_comment = "", "", ""
328
+ else
329
+ meta_comment = " (But unknown/invalid transfer syntax: #{transfer_syntax})"
330
+ explicit_comment = " (Assumed)"
331
+ encoding_comment = " (Assumed)"
332
+ end
333
+ explicitness = (explicit ? "Explicit" : "Implicit")
334
+ encoding = (endian ? "Big Endian" : "Little Endian")
335
+ meta = "Yes#{meta_comment}"
336
+ else
337
+ meta = "No"
338
+ explicitness = (@explicit == true ? "Explicit" : "Implicit")
339
+ encoding = (@file_endian == true ? "Big Endian" : "Little Endian")
340
+ explicit_comment = " (Assumed)"
341
+ encoding_comment = " (Assumed)"
342
+ end
343
+ info << "Meta Header: #{meta}"
344
+ info << "Value Representation: #{explicitness}#{explicit_comment}"
345
+ info << "Byte Order (File): #{encoding}#{encoding_comment}"
346
+ # Pixel data:
347
+ pixels = self[PIXEL_TAG]
348
+ unless pixels
349
+ info << "Pixel Data: No"
350
+ else
351
+ info << "Pixel Data: Yes"
352
+ # Image size:
353
+ cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
354
+ rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
355
+ info << "Image Size: #{cols}*#{rows}"
356
+ # Frames:
357
+ frames = value("0028,0008") || "1"
358
+ unless frames == "1" or frames == 1
359
+ # Encapsulated or 3D pixel data:
360
+ if pixels.is_a?(Element)
361
+ frames = frames.to_s + " (3D Pixel Data)"
362
+ else
363
+ frames = frames.to_s + " (Encapsulated Multiframe Image)"
364
+ end
365
+ end
366
+ info << "Number of frames: #{frames}"
367
+ # Color:
368
+ colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
369
+ info << "Photometry: #{colors}"
370
+ # Compression:
371
+ if transfer_syntax
372
+ compression = LIBRARY.get_compression(transfer_syntax.value)
373
+ if compression
374
+ compression = LIBRARY.get_syntax_description(transfer_syntax.value) || "Unknown UID!"
375
+ else
376
+ compression = "No"
377
+ end
378
+ else
379
+ compression = "No (Assumed)"
380
+ end
381
+ info << "Compression: #{compression}"
382
+ # Pixel bits (allocated):
383
+ bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
384
+ info << "Bits per Pixel: #{bits}"
385
+ end
386
+ # Print the DICOM object's key properties:
387
+ separator = "-------------------------------------------"
388
+ puts "System Properties:"
389
+ puts separator + "\n"
390
+ puts sys_info
391
+ puts "\n"
392
+ puts "DICOM Object Properties:"
393
+ puts separator
394
+ puts info
395
+ puts separator
396
+ return info
397
+ end
398
+
399
+ # Returns the transfer syntax string of the DObject.
400
+ #
401
+ # If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
402
+ #
403
+ def transfer_syntax
404
+ return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
405
+ end
406
+
407
+ # Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
408
+ # numerical values if a switch of endianness is implied.
409
+ #
410
+ # === Restrictions
411
+ #
412
+ # This method does not change the compressed state of the pixel data element. Changing the transfer syntax between
413
+ # an uncompressed and compressed state will NOT change the pixel data accordingly (this must be taken care of manually).
414
+ #
415
+ # === Parameters
416
+ #
417
+ # * <tt>new_syntax</tt> -- The new transfer syntax string which will be applied to the DObject.
418
+ #
419
+ def transfer_syntax=(new_syntax)
420
+ valid_ts, new_explicit, new_endian = LIBRARY.process_transfer_syntax(new_syntax)
421
+ raise ArgumentError, "Invalid transfer syntax specified: #{new_syntax}" unless valid_ts
422
+ # Get the old transfer syntax and write the new one to the DICOM object:
423
+ old_syntax = transfer_syntax
424
+ valid_ts, old_explicit, old_endian = LIBRARY.process_transfer_syntax(old_syntax)
425
+ if exists?("0002,0010")
426
+ self["0002,0010"].value = new_syntax
427
+ else
428
+ add(Element.new("0002,0010", new_syntax))
429
+ end
430
+ # Update our Stream instance with the new encoding:
431
+ @stream.endian = new_endian
432
+ # If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
433
+ encode_children(old_endian) if old_endian != new_endian
434
+ end
435
+
436
+ # Passes the DObject to the DWrite class, which traverses the data element
437
+ # structure and encodes a proper DICOM binary string, which is finally written to the specified file.
438
+ #
439
+ # === Parameters
440
+ #
441
+ # * <tt>file_name</tt> -- A string which identifies the path & name of the DICOM file which is to be written to disk.
442
+ # * <tt>options</tt> -- A hash of parameters.
443
+ #
444
+ # === Options
445
+ #
446
+ # * <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.
447
+ #
448
+ # === Examples
449
+ #
450
+ # obj.write(path + "test.dcm")
451
+ #
452
+ def write(file_name, options={})
453
+ raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
454
+ insert_missing_meta unless options[:add_meta] == false
455
+ w = DWrite.new(self, transfer_syntax, file_name, options)
456
+ w.write
457
+ # Write process succesful?
458
+ @write_success = w.success
459
+ end
460
+
461
+
462
+ # Following methods are private:
463
+ private
464
+
465
+
466
+ # Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
467
+ # to ensure that a valid DICOM object will be written to file.
468
+ #
469
+ def insert_missing_meta
470
+ # File Meta Information Version:
471
+ Element.new("0002,0001", [0,1], :parent => self) unless exists?("0002,0001")
472
+ # Media Storage SOP Class UID:
473
+ Element.new("0002,0002", value("0008,0016"), :parent => self) unless exists?("0002,0002")
474
+ # Media Storage SOP Instance UID:
475
+ Element.new("0002,0003", value("0008,0018"), :parent => self) unless exists?("0002,0003")
476
+ # Transfer Syntax UID:
477
+ Element.new("0002,0010", transfer_syntax, :parent => self) unless exists?("0002,0010")
478
+ if !exists?("0002,0012") and !exists?("0002,0013")
479
+ # Implementation Class UID:
480
+ Element.new("0002,0012", UID, :parent => self)
481
+ # Implementation Version Name:
482
+ Element.new("0002,0013", NAME, :parent => self)
483
+ end
484
+ # Source Application Entity Title:
485
+ Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
486
+ # Group Length: Remove the old one (if it exists) before creating a new one.
487
+ remove("0002,0000")
488
+ Element.new("0002,0000", meta_group_length, :parent => self)
489
+ end
490
+
491
+ # Determines and returns the length of the meta group in the DObject instance.
492
+ #
493
+ def meta_group_length
494
+ group_length = 0
495
+ meta_elements = group(META_GROUP)
496
+ tag = 4
497
+ vr = 2
498
+ meta_elements.each do |element|
499
+ case element.vr
500
+ when "OB","OW","OF","SQ","UN","UT"
501
+ length = 6
502
+ else
503
+ length = 2
504
+ end
505
+ group_length += tag + vr + length + element.bin.length
506
+ end
507
+ return group_length
508
+ end
509
+
510
+ end
511
+ end