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