dicom 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,420 @@
1
+ # Copyright 2008-2010 Christoffer Lervag
2
+ #
3
+ # === Notes
4
+ #
5
+ # The philosophy of the Ruby DICOM library is to feature maximum conformance to the DICOM standard.
6
+ # As such, the class which writes DICOM files may manipulate the meta group, remove/change group lengths and add a header signature.
7
+ #
8
+ # Therefore, the file that is written may not be an exact bitwise copy of the file that was read,
9
+ # even if no DObject manipulation has been done on the part of the user.
10
+ #
11
+ # Remember: If this behaviour for some reason is not wanted, it is easy to modify the source code to avoid it.
12
+ #
13
+ # It is important to note, that while the goal is to be fully DICOM compliant, no guarantees are given
14
+ # that this is actually achieved. You are encouraged to thouroughly test your files for compatibility after creation.
15
+
16
+ module DICOM
17
+
18
+ # The DWrite class handles the encoding of a DObject instance to a valid DICOM string.
19
+ # The String is either written to file or returned in segments to be used for network transmission.
20
+ #
21
+ class DWrite
22
+
23
+ # An array which records any status messages that are generated while encoding/writing the DICOM string.
24
+ attr_reader :msg
25
+ # An array of partial DICOM strings.
26
+ attr_reader :segments
27
+ # A boolean which reports whether the DICOM string was encoded/written successfully (true) or not (false).
28
+ attr_reader :success
29
+ # A boolean which reports the endianness of the post-meta group part of the DICOM string (true for big endian, false for little endian).
30
+ attr_reader :rest_endian
31
+ # A boolean which reports the explicitness of the DICOM string, true if explicit and false if implicit.
32
+ attr_reader :rest_explicit
33
+
34
+ # Creates a DWrite instance.
35
+ #
36
+ # === Parameters
37
+ #
38
+ # * <tt>obj</tt> -- A DObject instance which will be used to encode a DICOM string.
39
+ # * <tt>transfer_syntax</tt> -- String. The transfer syntax used for the encoding settings of the post-meta part of the DICOM string.
40
+ # * <tt>file_name</tt> -- A string, either specifying the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
41
+ # * <tt>options</tt> -- A hash of parameters.
42
+ #
43
+ # === Options
44
+ #
45
+ # * <tt>:signature</tt> -- Boolean. If set as false, the DICOM header signature will not be written to the DICOM file.
46
+ #
47
+ def initialize(obj, transfer_syntax, file_name=nil, options={})
48
+ @obj = obj
49
+ @transfer_syntax = transfer_syntax
50
+ @file_name = file_name
51
+ # As default, signature will be written and meta header added:
52
+ @signature = (options[:signature] == false ? false : true)
53
+ # Array for storing error/warning messages:
54
+ @msg = Array.new
55
+ end
56
+
57
+ # Handles the encoding of DICOM information to string as well as writing it to file.
58
+ #
59
+ # === Parameters
60
+ #
61
+ # * <tt>body</tt> -- A DICOM binary string which is duped to file, instead of the normal procedure of encoding element by element.
62
+ #
63
+ #--
64
+ # FIXME: It may seem that the body argument is not used anymore, and should be considered for removal.
65
+ #
66
+ def write(body=nil)
67
+ # Check if we are able to create given file:
68
+ open_file(@file_name)
69
+ # Go ahead and write if the file was opened successfully:
70
+ if @file
71
+ # Initiate necessary variables:
72
+ init_variables
73
+ # Create a Stream instance to handle the encoding of content to a binary string:
74
+ @stream = Stream.new(nil, @file_endian)
75
+ # Tell the Stream instance which file to write to:
76
+ @stream.set_file(@file)
77
+ # Write the DICOM signature:
78
+ write_signature if @signature
79
+ # Write either body or data elements:
80
+ if body
81
+ @stream.add_last(body)
82
+ else
83
+ elements = @obj.children
84
+ write_data_elements(elements)
85
+ end
86
+ # As file has been written successfully, it can be closed.
87
+ @file.close
88
+ # Mark this write session as successful:
89
+ @success = true
90
+ end
91
+ end
92
+
93
+ # Writes DICOM content to a series of size-limited binary strings, which is returned in an array.
94
+ # This is typically used in preparation of transmitting DICOM objects through network connections.
95
+ #
96
+ # === Parameters
97
+ #
98
+ # * <tt>max_size</tt> -- Fixnum. The maximum segment string length.
99
+ #
100
+ def encode_segments(max_size)
101
+ # Initiate necessary variables:
102
+ init_variables
103
+ @max_size = max_size
104
+ @segments = Array.new
105
+ elements = @obj.children
106
+ # When sending a DICOM file across the network, no header or meta information is needed.
107
+ # We must therefore find the position of the first tag which is not a meta information tag.
108
+ first_pos = first_non_meta(elements)
109
+ selected_elements = elements[first_pos..-1]
110
+ # Create a Stream instance to handle the encoding of content to
111
+ # the binary string that will eventually be saved to file:
112
+ @stream = Stream.new(nil, @file_endian)
113
+ write_data_elements(selected_elements)
114
+ # Extract the remaining string in our stream instance to our array of strings:
115
+ @segments << @stream.export
116
+ # Mark this write session as successful:
117
+ @success = true
118
+ end
119
+
120
+
121
+ # Following methods are private:
122
+ private
123
+
124
+
125
+ # Adds a binary string to (the end of) either the instance file or string.
126
+ #
127
+ def add(string)
128
+ if @file
129
+ @stream.write(string)
130
+ else
131
+ # Are we writing to a single (big) string, or multiple (smaller) strings?
132
+ unless @segments
133
+ @stream.add_last(string)
134
+ else
135
+ # As the encoded DICOM string will be cut in multiple, smaller pieces, we need to monitor the length of our encoded strings:
136
+ if (string.length + @stream.length) > @max_size
137
+ append = string.slice!(0, @max_size-@stream.length)
138
+ # Join these strings together and add them to the segments:
139
+ @segments << @stream.export + append
140
+ if (30 + string.length) > @max_size
141
+ # The remaining part of the string is bigger than the max limit, fill up more segments:
142
+ # How many full segments will this string fill?
143
+ number = (string.length/@max_size.to_f).floor
144
+ number.times {@segments << string.slice!(0, @max_size)}
145
+ # The remaining part is added to the stream:
146
+ @stream.add_last(string)
147
+ else
148
+ # The rest of the string is small enough that it can be added to the stream:
149
+ @stream.add_last(string)
150
+ end
151
+ elsif (30 + @stream.length) > @max_size
152
+ # End the current segment, and start on a new segment for this string.
153
+ @segments << @stream.export
154
+ @stream.add_last(string)
155
+ else
156
+ # We are nowhere near the limit, simply add the string:
157
+ @stream.add_last(string)
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ # Writes the DICOM header signature (128 bytes + 'DICM').
164
+ #
165
+ def write_signature
166
+ # Write the string "DICM" which along with the empty bytes that
167
+ # will be put before it, identifies this as a valid DICOM file:
168
+ identifier = @stream.encode("DICM", "STR")
169
+ # Fill in 128 empty bytes:
170
+ filler = @stream.encode("00"*128, "HEX")
171
+ @stream.write(filler)
172
+ @stream.write(identifier)
173
+ end
174
+
175
+ # Iterates through the data elements, encoding/writing one by one.
176
+ # If an element has children, this is method is repeated recursively.
177
+ #
178
+ # === Notes
179
+ #
180
+ # * Group length data elements are NOT written (they have been deprecated/retired in the DICOM standard).
181
+ #
182
+ # === Parameters
183
+ #
184
+ # * <tt>elements</tt> -- An array of data elements (sorted by their tags).
185
+ #
186
+ def write_data_elements(elements)
187
+ elements.each do |element|
188
+ # If this particular element has children, write these (recursively) before proceeding with elements at the current level:
189
+ if element.is_parent?
190
+ if element.children?
191
+ # Sequence/Item with child elements:
192
+ element.reset_length unless @enc_image
193
+ write_data_element(element)
194
+ write_data_elements(element.children)
195
+ if @enc_image
196
+ write_delimiter(element) if element.tag == PIXEL_TAG # (Write a delimiter for the pixel tag, but not for it's items)
197
+ else
198
+ write_delimiter(element)
199
+ end
200
+ else
201
+ # Empty sequence/item or item with binary data (We choose not to write empty, childless parents):
202
+ if element.bin
203
+ write_data_element(element) if element.bin.length > 0
204
+ end
205
+ end
206
+ else
207
+ # Ordinary Data Element:
208
+ if element.tag.group_length?
209
+ # Among group length elements, only write the meta group element (the others have been retired in the DICOM standard):
210
+ write_data_element(element) if element.tag == "0002,0000"
211
+ else
212
+ write_data_element(element)
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ # Encodes and writes a single data element.
219
+ #
220
+ # === Parameters
221
+ #
222
+ # * <tt>element</tt> -- A data element (DataElement, Sequence or Item).
223
+ #
224
+ def write_data_element(element)
225
+ # Step 1: Write tag:
226
+ write_tag(element.tag)
227
+ # Step 2: Write [VR] and value length:
228
+ write_vr_length(element.tag, element.vr, element.length)
229
+ # Step 3: Write value (Insert the already encoded binary string):
230
+ write_value(element.bin)
231
+ check_encapsulated_image(element)
232
+ end
233
+
234
+ # Encodes and writes an Item or Sequence delimiter.
235
+ #
236
+ # === Parameters
237
+ #
238
+ # * <tt>element</tt> -- A parent element (Item or Sequence).
239
+ #
240
+ def write_delimiter(element)
241
+ delimiter_tag = (element.tag == ITEM_TAG ? ITEM_DELIMITER : SEQUENCE_DELIMITER)
242
+ write_tag(delimiter_tag)
243
+ write_vr_length(delimiter_tag, ITEM_VR, 0)
244
+ end
245
+
246
+ # Encodes and writes a tag (the first part of the data element).
247
+ #
248
+ # === Parameters
249
+ #
250
+ # * <tt>tag</tt> -- String. A data element tag.
251
+ #
252
+ def write_tag(tag)
253
+ # Group 0002 is always little endian, but the rest of the file may be little or big endian.
254
+ # When we shift from group 0002 to another group we need to update our endian/explicitness variables:
255
+ switch_syntax if tag.group != META_GROUP and @switched == false
256
+ # Write to binary string:
257
+ bin_tag = @stream.encode_tag(tag)
258
+ add(bin_tag)
259
+ end
260
+
261
+ # Encodes and writes the value representation (if it is to be written) and length value.
262
+ # The encoding scheme to be applied here depends on explicitness, data element type and vr.
263
+ #
264
+ # === Parameters
265
+ #
266
+ # * <tt>tag</tt> -- String. The tag of this data element.
267
+ # * <tt>vr</tt> -- String. The value representation of this data element.
268
+ # * <tt>length</tt> -- Fixnum. The data element's length.
269
+ #
270
+ def write_vr_length(tag, vr, length)
271
+ # Encode the length value (cover both scenarios of 2 and 4 bytes):
272
+ length4 = @stream.encode(length, "SL")
273
+ length2 = @stream.encode(length, "US")
274
+ # Structure will differ, dependent on whether we have explicit or implicit encoding:
275
+ # *****EXPLICIT*****:
276
+ if @explicit == true
277
+ # Step 1: Write VR (if it is to be written)
278
+ unless ITEM_TAGS.include?(tag)
279
+ # Write data element VR (2 bytes - since we are not dealing with an item related element):
280
+ add(@stream.encode(vr, "STR"))
281
+ end
282
+ # Step 2: Write length
283
+ # Three possible structures for value length here, dependent on data element vr:
284
+ case vr
285
+ when "OB","OW","OF","SQ","UN","UT"
286
+ if @enc_image # (4 bytes)
287
+ # Item under an encapsulated Pixel Data (7FE0,0010).
288
+ add(length4)
289
+ else # (6 bytes total)
290
+ # Two reserved bytes first:
291
+ add(@stream.encode("00"*2, "HEX"))
292
+ # Value length (4 bytes):
293
+ add(length4)
294
+ end
295
+ when ITEM_VR # (4 bytes)
296
+ # For the item elements: "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
297
+ add(length4)
298
+ else # (2 bytes)
299
+ # For all the other data element vr, value length is 2 bytes:
300
+ add(length2)
301
+ end
302
+ else
303
+ # *****IMPLICIT*****:
304
+ # No VR written.
305
+ # Writing value length (4 bytes):
306
+ add(length4)
307
+ end
308
+ end
309
+
310
+ # Writes the data element's pre-encoded value.
311
+ #
312
+ # === Parameters
313
+ #
314
+ # * <tt>bin</tt> -- The binary string value of this data element.
315
+ #
316
+ def write_value(bin)
317
+ # This is pretty straightforward, just dump the binary data to the file/string:
318
+ add(bin)
319
+ end
320
+
321
+
322
+ # Tests if the path/file is writable, creates any folders if necessary, and opens the file for writing.
323
+ #
324
+ # === Parameters
325
+ #
326
+ # * <tt>file</tt> -- A path/file string.
327
+ #
328
+ def open_file(file)
329
+ # Check if file already exists:
330
+ if File.exist?(file)
331
+ # Is it writable?
332
+ if File.writable?(file)
333
+ @file = File.new(file, "wb")
334
+ else
335
+ # Existing file is not writable:
336
+ @msg << "Error! The program does not have permission or resources to create the file you specified: (#{file})"
337
+ end
338
+ else
339
+ # File does not exist.
340
+ # Check if this file's path contains a folder that does not exist, and therefore needs to be created:
341
+ folders = file.split(File::SEPARATOR)
342
+ if folders.length > 1
343
+ # Remove last element (which should be the file string):
344
+ folders.pop
345
+ path = folders.join(File::SEPARATOR)
346
+ # Check if this path exists:
347
+ unless File.directory?(path)
348
+ # We need to create (parts of) this path:
349
+ require 'fileutils'
350
+ FileUtils.mkdir_p path
351
+ end
352
+ end
353
+ # The path to this non-existing file is verified, and we can proceed to create the file:
354
+ @file = File.new(file, "wb")
355
+ end
356
+ end
357
+
358
+ # Toggles the status for enclosed pixel data.
359
+ #
360
+ # === Parameters
361
+ #
362
+ # * <tt>element</tt> -- A data element (DataElement, Sequence or Item).
363
+ #
364
+ def check_encapsulated_image(element)
365
+ # If DICOM object contains encapsulated pixel data, we need some special handling for its items:
366
+ if element.tag == PIXEL_TAG and element.parent.is_a?(DObject)
367
+ @enc_image = true if element.length <= 0
368
+ end
369
+ end
370
+
371
+ # Changes encoding variables as the file writing proceeds past the initial meta group part (0002,xxxx) of the DICOM object.
372
+ #
373
+ def switch_syntax
374
+ # The information from the Transfer syntax element (if present), needs to be processed:
375
+ valid_syntax, @rest_explicit, @rest_endian = LIBRARY.process_transfer_syntax(@transfer_syntax)
376
+ unless valid_syntax
377
+ @msg << "Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."
378
+ end
379
+ # We only plan to run this method once:
380
+ @switched = true
381
+ # Update explicitness and endianness (pack/unpack variables):
382
+ @explicit = @rest_explicit
383
+ @file_endian = @rest_endian
384
+ @stream.endian = @rest_endian
385
+ end
386
+
387
+ # Identifies and returns the index of the first data element that does not have a meta group ("0002,xxxx") tag.
388
+ #
389
+ # === Parameters
390
+ #
391
+ # * <tt>elements</tt> -- An array of data elements.
392
+ #
393
+ def first_non_meta(elements)
394
+ non_meta_index = 0
395
+ elements.each_index do |i|
396
+ if elements[i].tag.group != META_GROUP
397
+ non_meta_index = i
398
+ break
399
+ end
400
+ end
401
+ return non_meta_index
402
+ end
403
+
404
+ # Creates various variables used when encoding the DICOM string.
405
+ #
406
+ def init_variables
407
+ # Until a DICOM write has completed successfully the status is 'unsuccessful':
408
+ @success = false
409
+ # Default explicitness of start of DICOM file:
410
+ @explicit = true
411
+ # Default endianness of start of DICOM files (little endian):
412
+ @file_endian = false
413
+ # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
414
+ @switched = false
415
+ # Items contained under the Pixel Data element needs some special attention to write correctly:
416
+ @enc_image = false
417
+ end
418
+
419
+ end
420
+ end
@@ -0,0 +1,175 @@
1
+ # Copyright 2010 Christoffer Lervag
2
+
3
+ module DICOM
4
+
5
+ # The DataElement class handles information related to ordinary (non-parent) data elements.
6
+ #
7
+ class DataElement
8
+
9
+ # Include the Elements mix-in module:
10
+ include Elements
11
+
12
+ # The (decoded) value of the data element.
13
+ attr_reader :value
14
+
15
+ # Creates a DataElement instance.
16
+ #
17
+ # === Notes
18
+ #
19
+ # * In the case where the DataElement is given a binary instead of value, the DataElement will not have a formatted value (value = nil).
20
+ # * Private data elements will have their names listed as "Private".
21
+ # * Non-private data elements that are not found in the dictionary will be listed as "Unknown".
22
+ #
23
+ # === Parameters
24
+ #
25
+ # * <tt>tag</tt> -- A string which identifies the tag of the data element.
26
+ # * <tt>value</tt> -- A custom value to be encoded as the data element binary string, or in some cases (specified by options), a pre-encoded binary string.
27
+ # * <tt>options</tt> -- A hash of parameters.
28
+ #
29
+ # === Options
30
+ #
31
+ # * <tt>:bin</tt> -- String. If you already have the value pre-encoded to a binary string, the string can be supplied with this option to avoid it being encoded a second time.
32
+ # * <tt>:encoded</tt> -- Boolean. If the value parameter contains a pre-encoded binary, this boolean must to be set as true.
33
+ # * <tt>:name</tt> - String. The name of the DataElement may be specified upon creation. If it is not, the name will be retrieved from the dictionary.
34
+ # * <tt>:parent</tt> - Item or DObject instance which the DataElement instance shall belong to.
35
+ # * <tt>:vr</tt> -- String. If a private DataElement is created with a custom value, this must be specified to enable the encoding of the value. If it is not specified, the vr will be retrieved from the dictionary.
36
+ #
37
+ # === Examples
38
+ #
39
+ # # Create a new data element and connect it to a DObject instance:
40
+ # patient_name = DataElement.new("0010,0010", "John Doe", :parent => obj)
41
+ # # Create a "Pixel Data" element and insert image data that you have already encoded elsewhere:
42
+ # pixel_data = DataElement.new("7FE0,0010", processed_pixel_data, :encoded => true, :parent => obj)
43
+ # # Create a private data element:
44
+ # private_data = DataElement.new("0011,2102", some_data, :parent => obj, :vr => "LO")
45
+ #
46
+ def initialize(tag, value, options={})
47
+ # Set instance variables:
48
+ @tag = tag
49
+ # We may beed to retrieve name and vr from the library:
50
+ if options[:name] and options[:vr]
51
+ @name = options[:name]
52
+ @vr = options[:vr].upcase
53
+ else
54
+ name, vr = LIBRARY.get_name_vr(tag)
55
+ @name = options[:name] || name
56
+ @vr = (options[:vr] ? options[:vr].upcase : vr)
57
+ end
58
+ # Value may in some cases be the binary string:
59
+ unless options[:encoded]
60
+ @value = value
61
+ # The Data Element may have a value, have no value and no binary, or have no value and only binary:
62
+ if value
63
+ # Is binary value provided or do we need to encode it?
64
+ if options[:bin]
65
+ @bin = options[:bin]
66
+ else
67
+ @bin = encode(value)
68
+ end
69
+ else
70
+ # When no value is present, we set the binary as an empty string, unless the binary is specified:
71
+ @bin = options[:bin] || ""
72
+ end
73
+ else
74
+ @bin = value
75
+ end
76
+ # Let the binary decide the length:
77
+ @length = @bin.length
78
+ # Manage the parent relation if specified:
79
+ if options[:parent]
80
+ @parent = options[:parent]
81
+ @parent.add(self)
82
+ end
83
+ end
84
+
85
+ # Sets the binary string of a DataElement.
86
+ #
87
+ # === Notes
88
+ #
89
+ # If the specified binary has an odd length, a proper pad byte will automatically be appended
90
+ # to give it an even length (which is needed to conform with the DICOM standard).
91
+ #
92
+ # === Parameters
93
+ #
94
+ # * <tt>new_bin</tt> -- A binary string of encoded data.
95
+ #
96
+ def bin=(new_bin)
97
+ if new_bin.is_a?(String)
98
+ # Add a zero byte at the end if the length of the binary is odd:
99
+ if new_bin.length[0] == 1
100
+ @bin = new_bin + stream.pad_byte[@vr]
101
+ else
102
+ @bin = new_bin
103
+ end
104
+ @value = nil
105
+ @length = @bin.length
106
+ else
107
+ raise "Invalid parameter type. String was expected, got #{new_bin.class}."
108
+ end
109
+ end
110
+
111
+ # Checks if an element actually has any child elements.
112
+ # Returns false, as DataElement instances can not have children.
113
+ #
114
+ def children?
115
+ return false
116
+ end
117
+
118
+ # Checks if an element is a parent.
119
+ # Returns false, as DataElement instance can not be parents.
120
+ #
121
+ def is_parent?
122
+ return false
123
+ end
124
+
125
+ # Sets the value of the DataElement instance.
126
+ #
127
+ # === Notes
128
+ #
129
+ # In addition to updating the value attribute, the specified value is encoded and used to
130
+ # update both the DataElement's binary and length attributes too.
131
+ #
132
+ # The specified value must be of a type that is compatible with the DataElement's value representation (vr).
133
+ #
134
+ # === Parameters
135
+ #
136
+ # * <tt>new_value</tt> -- A custom value (String, Fixnum, etc..) that is assigned to the DataElement.
137
+ #
138
+ def value=(new_value)
139
+ @bin = encode(new_value)
140
+ @value = new_value
141
+ @length = @bin.length
142
+ end
143
+
144
+
145
+ # Following methods are private.
146
+ private
147
+
148
+
149
+ # Encodes a formatted value to a binary string and returns it.
150
+ #
151
+ # === Parameters
152
+ #
153
+ # * <tt>formatted_value</tt> -- A custom value (String, Fixnum, etc..).
154
+ #
155
+ def encode(formatted_value)
156
+ return stream.encode_value(formatted_value, @vr)
157
+ end
158
+
159
+ # Returns a Stream instance which can be used for encoding a value to binary.
160
+ #
161
+ # === Notes
162
+ #
163
+ # * Retrieves the Stream instance of the top parent DObject instance.
164
+ # If this fails, a new Stream instance is created (with Little Endian encoding assumed).
165
+ #
166
+ def stream
167
+ if top_parent.is_a?(DObject)
168
+ return top_parent.stream
169
+ else
170
+ return Stream.new(nil, file_endian=false)
171
+ end
172
+ end
173
+
174
+ end
175
+ end