dicom 0.6.1 → 0.7

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,4 +1,4 @@
1
- # Copyright 2009 Christoffer Lervag
1
+ # Copyright 2009-2010 Christoffer Lervag
2
2
 
3
3
  # This file contains the Stream class, which handles all encoding to and
4
4
  # decoding from binary strings. It is used by the other components of
@@ -0,0 +1,47 @@
1
+ # This file contains extensions to the Ruby library which are used by Ruby DICOM.
2
+
3
+ class Array
4
+
5
+ # Searching all indices, or a subset of indices, in an array, and returning all indices
6
+ # where the array's value equals the queried value.
7
+ def all_indices(array, value)
8
+ result = []
9
+ self.each do |pos|
10
+ result << pos if array[pos] == value
11
+ end
12
+ return result
13
+ end
14
+
15
+ # Similar to method above, but this one returns the position of all strings that
16
+ # contain the query string (exact match not required).
17
+ def all_indices_partial_match(array, value)
18
+ result = []
19
+ self.each do |pos|
20
+ result << pos if array[pos].include?(value)
21
+ end
22
+ return result
23
+ end
24
+
25
+ end
26
+
27
+ class String
28
+
29
+ # Check if a given string appears to be a valid tag (GGGG,EEEE) by regexp matching.
30
+ # The method tests that the string is exactly composed of 4 HEX characters, followed by
31
+ # a comma, then 4 new HEX characters, which constitutes the tag format used by Ruby DICOM.
32
+ def is_a_tag?
33
+ result = false
34
+ #result = true if self =~ /\A\h{4},\h{4}\z/ # (turns out the hex reference '\h' isnt compatible with ruby 1.8)
35
+ result = true if self =~ /\A[a-fA-F\d]{4},[a-fA-F\d]{4}\z/
36
+ return result
37
+ end
38
+
39
+ # Check if a given tag string indicates a private tag (Odd group number) by doing a regexp matching.
40
+ def private?
41
+ result = false
42
+ #result = true if self.upcase =~ /\A\h{3}[1,3,5,7,9,B,D,F],\h{4}\z/ # (incompatible with ruby 1.8)
43
+ result = true if self.upcase =~ /\A[a-fA-F\d]{3}[1,3,5,7,9,B,D,F],[a-fA-F\d]{4}\z/
44
+ return result
45
+ end
46
+
47
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dicom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: "0.7"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christoffer Lervag
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-08-23 00:00:00 +02:00
12
+ date: 2010-02-28 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -22,20 +22,21 @@ extensions: []
22
22
  extra_rdoc_files: []
23
23
 
24
24
  files:
25
- - lib/DObject.rb
26
- - lib/DRead.rb
27
- - lib/DWrite.rb
28
25
  - lib/dicom.rb
29
- - lib/DServer.rb
30
- - lib/DClient.rb
31
- - lib/Stream.rb
32
- - lib/Anonymizer.rb
33
- - lib/Link.rb
34
- - lib/DLibrary.rb
35
- - lib/ruby_extensions.rb
36
- - lib/Dictionary.rb
37
- - DOCUMENTATION
26
+ - lib/dicom/DObject.rb
27
+ - lib/dicom/Link.rb
28
+ - lib/dicom/DLibrary.rb
29
+ - lib/dicom/DWrite.rb
30
+ - lib/dicom/Dictionary.rb
31
+ - lib/dicom/FileHandler.rb
32
+ - lib/dicom/DServer.rb
33
+ - lib/dicom/DClient.rb
34
+ - lib/dicom/Anonymizer.rb
35
+ - lib/dicom/Stream.rb
36
+ - lib/dicom/ruby_extensions.rb
37
+ - lib/dicom/DRead.rb
38
38
  - README
39
+ - DOCUMENTATION
39
40
  - COPYING
40
41
  - CHANGELOG
41
42
  has_rdoc: true
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  requirements: []
63
64
 
64
65
  rubyforge_project: dicom
65
- rubygems_version: 1.3.3
66
+ rubygems_version: 1.3.5
66
67
  signing_key:
67
68
  specification_version: 3
68
69
  summary: Library for handling DICOM files and DICOM network communication.
@@ -1,1194 +0,0 @@
1
- # Copyright 2008-2009 Christoffer Lervag
2
- #
3
- # This program is free software: you can redistribute it and/or modify
4
- # it under the terms of the GNU General Public License as published by
5
- # the Free Software Foundation, either version 3 of the License, or
6
- # (at your option) any later version.
7
- #
8
- # This program is distributed in the hope that it will be useful,
9
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
- # GNU General Public License for more details.
12
- #
13
- # You should have received a copy of the GNU General Public License
14
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
- #
16
- #--------------------------------------------------------------------------------------------------
17
-
18
- # TODO:
19
- # -Improve the retrieve file network functionality
20
- # -Make the networking more intelligent in its handling of (unexpected) messages
21
- # -Support for writing complex (hierarchical) DICOM files (basic write support is featured).
22
- # -Full support for compressed image data.
23
- # -Read 12 bit image data correctly.
24
- # -Support for color image data to get_image_narray and get_image_magick.
25
- # -Complete support for Big endian (basic support is already featured).
26
- # -Complete support for multiple frame image data to NArray and RMagick objects (partial support already featured).
27
- # -Make the image handling more intelligent with respect to interpreting data elements that hold information on the image and its properties.
28
-
29
- module DICOM
30
-
31
- # Class for interacting with the DICOM object.
32
- class DObject
33
-
34
- attr_reader :read_success, :write_success, :modality, :errors, :segments,
35
- :names, :tags, :types, :lengths, :values, :raw, :levels
36
-
37
- # Initialize the DObject instance.
38
- def initialize(string=nil, options={})
39
- # Process option values, setting defaults for the ones that are not specified:
40
- @verbose = options[:verbose]
41
- @lib = options[:lib] || DLibrary.new
42
- segment_size = options[:segment_size]
43
- bin = options[:bin]
44
- syntax = options[:syntax]
45
- # Default verbosity is true:
46
- @verbose = true if @verbose == nil
47
-
48
- # Initialize variables that will be used for the DICOM object:
49
- @names = Array.new
50
- @tags = Array.new
51
- @types = Array.new
52
- @lengths = Array.new
53
- @values = Array.new
54
- @raw = Array.new
55
- @levels = Array.new
56
- # Array that will holde any messages generated while reading the DICOM file:
57
- @errors = Array.new
58
- # Array to keep track of sequences/structure of the dicom elements:
59
- @sequence = Array.new
60
- # Index of last element in data element arrays:
61
- @last_index=0
62
- # Structural information (default values):
63
- @compression = false
64
- @color = false
65
- @explicit = true
66
- @file_endian = false
67
- # Information about the DICOM object:
68
- @modality = nil
69
- # Control variables:
70
- @read_success = false
71
- # Initialize a Stream instance which is used for encoding/decoding:
72
- @stream = Stream.new(nil, @file_endian, @explicit)
73
-
74
- # If a (valid) file name string is supplied, call the method to read the DICOM file:
75
- if string.is_a?(String) and string != ""
76
- @file = string
77
- read(string, :bin => bin, :segment_size => segment_size, :syntax => syntax)
78
- end
79
- end # of initialize
80
-
81
-
82
- # Returns a DICOM object by reading the file specified.
83
- # This is accomplished by initliazing the DRead class, which loads DICOM information to arrays.
84
- # For the time being, this method is called automatically when initializing the DObject class,
85
- # but in the future, when write support is added, this method may have to be called manually.
86
- def read(string, options = {})
87
- r = DRead.new(string, :lib => @lib, :sys_endian => @sys_endian, :bin => options[:bin], :syntax => options[:syntax])
88
- # Store the data to the instance variables if the readout was a success:
89
- if r.success
90
- @read_success = true
91
- @names = r.names
92
- @tags = r.tags
93
- @types = r.types
94
- @lengths = r.lengths
95
- @values = r.values
96
- @raw = r.raw
97
- @levels = r.levels
98
- @explicit = r.explicit
99
- @file_endian = r.file_endian
100
- # Update Stream instance with settings from this DICOM file:
101
- @stream.set_endian(@file_endian)
102
- @stream.explicit = @explicit
103
- # Index of last data element in element arrays:
104
- @last_index=@names.length-1
105
- # Update status variables for this object:
106
- check_properties
107
- # Set the modality of the DICOM object:
108
- set_modality
109
- else
110
- @read_success = false
111
- end
112
- # Check if a partial extraction has been requested (used for network communication purposes)
113
- if options[:segment_size]
114
- @segments = r.extract_segments(options[:segment_size])
115
- end
116
- # If any messages has been recorded, send these to the message handling method:
117
- add_msg(r.msg) if r.msg.size != 0
118
- end
119
-
120
-
121
- # Transfers necessary information from the DObject to the DWrite class, which
122
- # will attempt to write this information to a valid DICOM file.
123
- def write(file_name, transfer_syntax = nil)
124
- w = set_write_object(file_name, transfer_syntax)
125
- w.write
126
- # Write process succesful?
127
- @write_success = w.success
128
- # If any messages has been recorded, send these to the message handling method:
129
- add_msg(w.msg) if w.msg.size != 0
130
- end
131
-
132
-
133
- # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
134
- def encode_segments(size)
135
- w = set_write_object
136
- @segments = w.encode_segments(size)
137
- # Write process succesful?
138
- @write_success = w.success
139
- # If any messages has been recorded, send these to the message handling method:
140
- add_msg(w.msg) if w.msg.size != 0
141
- end
142
-
143
-
144
- #################################################
145
- # START OF METHODS FOR READING INFORMATION FROM DICOM OBJECT:
146
- #################################################
147
-
148
-
149
- # Checks the status of the pixel data that has been read from the DICOM file: whether it exists at all and if its greyscale or color.
150
- # Modifies instance variable @color if color image is detected and instance variable @compression if no pixel data is detected.
151
- def check_properties
152
- # Check if pixel data is present:
153
- if @tags.index("7FE0,0010") == nil
154
- # No pixel data in DICOM file:
155
- @compression = nil
156
- else
157
- @compression = @lib.get_compression(get_value("0002,0010", :silent => true))
158
- end
159
- # Set color variable as true if our object contain a color image:
160
- col_string = get_value("0028,0004", :silent => true)
161
- if col_string != false
162
- if (col_string.include? "RGB") or (col_string.include? "COLOR") or (col_string.include? "COLOUR")
163
- @color = true
164
- end
165
- end
166
- end
167
-
168
-
169
- # Returns image data from the provided element index, performing decompression of data if necessary.
170
- def read_image_magick(pos, columns, rows)
171
- if pos == false or columns == false or rows == false
172
- add_msg("Error: Method read_image_magick does not have enough data available to build an image object.")
173
- return false
174
- end
175
- unless @compression
176
- # Non-compressed, just return the array contained on the particular element:
177
- image_data = get_pixels(pos)
178
- image = Magick::Image.new(columns,rows)
179
- image.import_pixels(0, 0, columns, rows, "I", image_data)
180
- return image
181
- else
182
- # Image data is compressed, we will attempt to deflate it using RMagick (ImageMagick):
183
- begin
184
- image = Magick::Image.from_blob(@raw[pos])
185
- return image
186
- rescue
187
- add_msg("RMagick did not succeed in decoding the compressed image data. Returning false.")
188
- return false
189
- end
190
- end
191
- end
192
-
193
-
194
- # Returns a 3d NArray object where the array dimensions are related to [frames, columns, rows].
195
- # To call this method the user needs to have performed " require 'narray' " in advance.
196
- def get_image_narray
197
- # Does pixel data exist at all in the DICOM object?
198
- if @compression == nil
199
- add_msg("It seems pixel data is not present in this DICOM object: returning false.")
200
- return false
201
- end
202
- # No support yet for retrieving compressed data:
203
- if @compression == true
204
- add_msg("Reading compressed data to a NArray object not supported yet: returning false.")
205
- return false
206
- end
207
- # No support yet for retrieving color pixel data:
208
- if @color
209
- add_msg("Warning: Unpacking color pixel data is not supported yet for this method: returning false.")
210
- return false
211
- end
212
- # Gather information about the dimensions of the image data:
213
- rows = get_value("0028,0010")
214
- columns = get_value("0028,0011")
215
- frames = get_frames
216
- image_pos = get_image_pos
217
- # Creating a NArray object using int to make sure we have a big enough range for our numbers:
218
- image = NArray.int(frames,columns,rows)
219
- image_temp = NArray.int(columns,rows)
220
- # Handling of image data will depend on whether we have one or more frames,
221
- # and if it is located in one or more elements:
222
- if image_pos.size == 1
223
- # All of the image data is located in one element:
224
- image_data = get_pixels(image_pos[0])
225
- #image_data = get_image_data(image_pos[0])
226
- (0..frames-1).each do |i|
227
- (0..columns*rows-1).each do |j|
228
- image_temp[j] = image_data[j+i*columns*rows]
229
- end
230
- image[i,true,true] = image_temp
231
- end
232
- else
233
- # Image data is encapsulated in items:
234
- (0..frames-1).each do |i|
235
- image_data=get_value(image_pos[i])
236
- #image_data = get_image_data(image_pos[i])
237
- (0..columns*rows-1).each do |j|
238
- image_temp[j] = image_data[j+i*columns*rows]
239
- end
240
- image[i,true,true] = image_temp
241
- end
242
- end
243
- # Turn around the images to get the expected orientation when displaying on the screen:
244
- (0..frames-1).each do |i|
245
- temp_image=image[i,true,true]
246
- #Transpose the images:
247
- temp_image.transpose(1,0)
248
- #Need to mirror the y-axis:
249
- (0..temp_image.shape[0]-1).each do |j|
250
- temp_image[j,0..temp_image.shape[1]-1] = temp_image[j,temp_image.shape[1]-1..0]
251
- end
252
- # Put the reoriented image back in the image matrixx:
253
- image[i,true,true]=temp_image
254
- end
255
- return image
256
- end # of get_image_narray
257
-
258
-
259
- # Returns an array of RMagick image objects, where the size of the array corresponds to the number of frames in the image data.
260
- # To call this method the user needs to have loaded the ImageMagick library in advance (require 'RMagick').
261
- def get_image_magick
262
- # Does pixel data exist at all in the DICOM object?
263
- if @compression == nil
264
- add_msg("It seems pixel data is not present in this DICOM object: Returning false.")
265
- return false
266
- end
267
- # No support yet for color pixel data:
268
- if @color
269
- add_msg("Warning: Unpacking color pixel data is not supported yet for this method: Aborting.")
270
- return false
271
- end
272
- # Gather information about the dimensions of the image data:
273
- rows = get_value("0028,0010", :array => true)
274
- columns = get_value("0028,0011", :array => true)
275
- rows = [rows] unless rows.is_a?(Array)
276
- columns = [columns] unless columns.is_a?(Array)
277
- frames = get_frames
278
- image_pos = get_image_pos
279
- # Array that will hold the RMagick image objects, one image object for each frame:
280
- image_arr = Array.new(frames)
281
- # A hack for the special case (some MR files), where two images are stored (one is a smaller thumbnail image):
282
- if image_pos.length > 1 and columns.length > 1
283
- image_pos = [image_pos.last]
284
- columns = [columns[0]]
285
- rows = [rows[0]]
286
- end
287
- # Handling of image data will depend on whether we have one or more frames,
288
- if image_pos.size == 1
289
- # All of the image data is located in one data element:
290
- if frames > 1
291
- add_msg("Unfortunately, this method only supports reading the first image frame as of now.")
292
- end
293
- image = read_image_magick(image_pos[0], columns[0], rows[0])
294
- image_arr[0] = image
295
- else
296
- # Image data is encapsulated in items:
297
- (0..frames-1).each do |i|
298
- image = read_image_magick(image_pos[i], columns[0], rows[0])
299
- image_arr[i] = image
300
- end
301
- end
302
- return image_arr
303
- end # of get_image_magick
304
-
305
-
306
- # Returns the number of frames present in the image data in the DICOM file.
307
- def get_frames
308
- frames = get_value("0028,0008", :silent => true)
309
- # If the DICOM object does not specify the number of frames explicitly, assume 1 image frame:
310
- frames = 1 unless frames
311
- return frames.to_i
312
- end
313
-
314
-
315
- # Unpacks and returns pixel data from a specified data element array position:
316
- def get_pixels(pos)
317
- pixels = false
318
- # We need to know what kind of bith depth the pixel data is saved with:
319
- bit_depth = get_value("0028,0100", :array => true)
320
- unless bit_depth == false
321
- # Load the binary pixel data to the Stream instance:
322
- @stream.set_string(get_raw(pos))
323
- bit_depth = bit_depth.first if bit_depth.is_a?(Array)
324
- # Number of bytes used per pixel will determine how to unpack this:
325
- case bit_depth
326
- when 8
327
- pixels = @stream.decode_all("BY") # Byte/Character/Fixnum (1 byte)
328
- when 16
329
- pixels = @stream.decode_all("US") # Unsigned short (2 bytes)
330
- when 12
331
- # 12 BIT SIMPLY NOT WORKING YET!
332
- # This one is a bit more tricky to extract.
333
- # I havent really given this priority so far as 12 bit image data is rather rare.
334
- add_msg("Warning: Bit depth 12 is not working correctly at this time! Please contact the author.")
335
- else
336
- raise "Bit depth ["+bit_depth.to_s+"] has not received implementation in this procedure yet. Please contact the author."
337
- end # of case bit_depth
338
- else
339
- add_msg("Error: DICOM object does not contain the 'Bit Depth' data element (0028,0010).")
340
- end # of if bit_depth ..
341
- return pixels
342
- end
343
-
344
-
345
- # Returns the index(es) of the element(s) that contain image data.
346
- def get_image_pos
347
- image_element_pos = get_pos("7FE0,0010")
348
- item_pos = get_pos("FFFE,E000")
349
- # Proceed only if an image element actually exists:
350
- if image_element_pos == false
351
- return false
352
- else
353
- # Check if we have item elements:
354
- if item_pos == false
355
- return image_element_pos
356
- else
357
- # Extract item positions that occur after the image element position:
358
- late_item_pos = item_pos.select {|item| image_element_pos[0] < item}
359
- # Check if there are items appearing after the image element.
360
- if late_item_pos.size == 0
361
- # None occured after the image element position:
362
- return image_element_pos
363
- else
364
- # Determine which of these late item elements contain image data.
365
- # Usually, there are frames+1 late items, and all except
366
- # the first item contain an image frame:
367
- frames = get_frames
368
- if frames != false # note: function get_frames will never return false
369
- if late_item_pos.size == frames.to_i+1
370
- return late_item_pos[1..late_item_pos.size-1]
371
- else
372
- add_msg("Warning: Unexpected behaviour in DICOM file for method get_image_pos. Expected number of image data items not equal to number of frames+1, returning false.")
373
- return false
374
- end
375
- else
376
- add_msg("Warning: 'Number of Frames' data element not found. Method get_image_pos will return false.")
377
- return false
378
- end
379
- end
380
- end
381
- end
382
- end
383
-
384
-
385
- # Returns an array of the index(es) of the element(s) in the DICOM file that match the supplied element position, tag or name.
386
- # If no match is found, the method will return false.
387
- # Additional options:
388
- # :array => myArray - tells the method to search for matches in this specific array of positions instead of searching
389
- # through the entire DICOM object. If myArray equals false, the method will return false.
390
- # :partial => true - get_pos will not only search for exact matches, but will search the names and tags arrays for
391
- # strings that contain the given search string.
392
- def get_pos(query, options={})
393
- indexes = Array.new
394
- # For convenience, allow query to be a one-element array (its value will be extracted):
395
- if query.is_a?(Array)
396
- if query.length > 1 or query.length == 0
397
- add_msg("Invalid array length supplied to method get_pos.")
398
- return false
399
- else
400
- query = query[0]
401
- end
402
- end
403
- if options[:array] == false
404
- # If the supplied array option equals false, it signals that the user tries to search for an element
405
- # in an invalid position, and as such, this method will also return false:
406
- add_msg("Warning: Attempted to call get_pos with query #{query}, but since keyword :array is false I will return false.")
407
- indexes = false
408
- else
409
- # Check if query is a number (some methods want to have the ability to call get_pos with a number):
410
- if query.is_a?(Integer)
411
- # Return the position if it is valid:
412
- indexes = [query] if query >= 0 and query < @names.length
413
- elsif query.is_a?(String)
414
- # Either use the supplied array, or search the entire DICOM object:
415
- if options[:array].is_a?(Array)
416
- search_array = options[:array]
417
- else
418
- search_array = Array.new(@names.length) {|i| i}
419
- end
420
- # Perform search:
421
- if options[:partial] == true
422
- # Search for partial string matches:
423
- partial_indexes = search_array.all_indices_partial_match(@tags, query.upcase)
424
- if partial_indexes.length > 0
425
- indexes = partial_indexes
426
- else
427
- indexes = search_array.all_indices_partial_match(@names, query)
428
- end
429
- else
430
- # Search for identical matches:
431
- if query[4..4] == ","
432
- indexes = search_array.all_indices(@tags, query.upcase)
433
- else
434
- indexes = search_array.all_indices(@names, query)
435
- end
436
- end
437
- end
438
- # Policy: If no matches found, return false instead of an empty array:
439
- indexes = false if indexes.length == 0
440
- end
441
- return indexes
442
- end # of get_pos
443
-
444
-
445
- # Dumps the binary content of the Pixel Data element to file.
446
- def image_to_file(file)
447
- pos = get_image_pos
448
- if pos
449
- if pos.length == 1
450
- # Pixel data located in one element:
451
- pixel_data = get_raw(pos[0])
452
- f = File.new(file, "wb")
453
- f.write(pixel_data)
454
- f.close
455
- else
456
- # Pixel data located in several elements:
457
- pos.each_index do |i|
458
- pixel_data = get_raw(pos[i])
459
- f = File.new(file + i.to_s, "wb")
460
- f.write(pixel_data)
461
- f.close
462
- end
463
- end
464
- end
465
- end
466
-
467
-
468
- # Returns the positions of all data elements inside the hierarchy of a sequence or an item.
469
- # Options:
470
- # :next_only => true - The method will only search immediately below the specified
471
- # item or sequence (that is, in the level of parent + 1).
472
- def children(element, options={})
473
- # Process option values, setting defaults for the ones that are not specified:
474
- opt_next_only = options[:next_only] || false
475
- value = false
476
- # Retrieve array position:
477
- pos = get_pos(element)
478
- if pos == false
479
- add_msg("Warning: Invalid data element provided to method children. Returning false.")
480
- else
481
- if pos.size > 1
482
- add_msg("Warning: Method children does not allow a query which yields multiple array hits. Please use array position instead of tag/name. Returning false.")
483
- else
484
- # Proceed to find the value:
485
- # First we need to establish in which positions to perform the search:
486
- below_pos = Array.new
487
- pos.each do |p|
488
- parent_level = @levels[p]
489
- remain_array = @levels[p+1..@levels.size-1]
490
- extract = true
491
- remain_array.each_index do |i|
492
- if (remain_array[i] > parent_level) and (extract == true)
493
- # If search is targetted at any specific level, we can just add this position:
494
- if not opt_next_only == true
495
- below_pos << (p+1+i)
496
- else
497
- # As search is restricted to parent level + 1, do a test for this:
498
- if remain_array[i] == parent_level + 1
499
- below_pos << (p+1+i)
500
- end
501
- end
502
- else
503
- # If we encounter a position who's level is not deeper than the original level, we can not extract any more values:
504
- extract = false
505
- end
506
- end
507
- end # of pos.each do..
508
- value = below_pos if below_pos.size != 0
509
- end # of if pos.size..else..
510
- end
511
- return value
512
- end
513
-
514
-
515
- # Returns the value (processed raw data) of the requested DICOM data element.
516
- # Data element may be specified by array position, tag or name.
517
- # Options:
518
- # :array => true - Allows the query of the value of a tag that occurs more than one time in the
519
- # DICOM object. Values will be returned in an array with length equal to the number
520
- # of occurances of the tag. If keyword is not specified, the method returns false in this case.
521
- # :silent => true - As this method is also used internally, we want the possibility of warnings not being
522
- # raised even if verbose is set to true by the user, in order to avoid confusion.
523
- def get_value(element, options={})
524
- value = false
525
- # Retrieve array position:
526
- pos = get_pos(element)
527
- if pos == false
528
- add_msg("Warning: Invalid data element provided to method get_value. Returning false.") unless options[:silent]
529
- else
530
- if pos.size > 1
531
- if options[:array] == true
532
- # Retrieve all values into an array:
533
- value = Array.new
534
- pos.each do |i|
535
- value << @values[i]
536
- end
537
- else
538
- add_msg("Warning: Method get_value does not allow a query which yields multiple array hits. Please use array position instead of tag/name, or use keyword (:array => true). Returning false.") unless options[:silent]
539
- end
540
- else
541
- value = @values[pos[0]]
542
- end
543
- end
544
- return value
545
- end
546
-
547
-
548
- # Returns the raw data of the requested DICOM data element.
549
- # Data element may be specified by array position, tag or name.
550
- # Options:
551
- # :array => true - Allows the query of the value of a tag that occurs more than one time in the
552
- # DICOM object. Values will be returned in an array with length equal to the number
553
- # of occurances of the tag. If keyword is not specified, the method returns false in this case.
554
- def get_raw(element, options={})
555
- value = false
556
- # Retrieve array position:
557
- pos = get_pos(element)
558
- if pos == false
559
- add_msg("Warning: Invalid data element provided to method get_raw. Returning false.")
560
- else
561
- if pos.size > 1
562
- if options[:array] == true
563
- # Retrieve all values into an array:
564
- value = Array.new
565
- pos.each do |i|
566
- value << @raw[i]
567
- end
568
- else
569
- add_msg("Warning: Method get_raw does not allow a query which yields multiple array hits. Please use array position instead of tag/name, or use keyword (:array => true). Returning false.")
570
- end
571
- else
572
- value = @raw[pos[0]]
573
- end
574
- end
575
- return value
576
- end
577
-
578
-
579
- # Returns the position of (possible) parents of the specified data element in the hierarchy structure of the DICOM object.
580
- def parents(element)
581
- value = false
582
- # Retrieve array position:
583
- pos = get_pos(element)
584
- if pos == false
585
- add_msg("Warning: Invalid data element provided to method parents. Returning false.")
586
- else
587
- if pos.length > 1
588
- add_msg("Warning: Method parents does not allow a query which yields multiple array hits. Please use array position instead of tag/name. Returning false.")
589
- else
590
- # Proceed to find the value:
591
- # Get the level of our element:
592
- level = @levels[pos[0]]
593
- # Element can obviously only have parents if it is not a top level element:
594
- unless level == 0
595
- # Search backwards, and record the position every time we encounter an upwards change in the level number.
596
- parents = Array.new
597
- prev_level = level
598
- search_arr = @levels[0..pos[0]-1].reverse
599
- search_arr.each_index do |i|
600
- if search_arr[i] < prev_level
601
- parents << search_arr.length-i-1
602
- prev_level = search_arr[i]
603
- end
604
- end
605
- # When the element has several generations of parents, we want its top parent to be first in the returned array:
606
- parents = parents.reverse
607
- value = parents if parents.length > 0
608
- end
609
- end
610
- end
611
- return value
612
- end
613
-
614
-
615
- ##############################################
616
- ####### START OF METHODS FOR PRINTING INFORMATION:######
617
- ##############################################
618
-
619
-
620
- # Prints the information of all elements stored in the DICOM object.
621
- # This method is kept for backwards compatibility.
622
- # Instead of calling print_all you may use print(true) for the same functionality.
623
- def print_all
624
- print(true)
625
- end
626
-
627
-
628
- # Prints the information of the specified elements: Index, [hierarchy level, tree visualisation,] tag, name, type, length, value
629
- # The supplied variable may be a single position, an array of positions, or true - which will make the method print all elements.
630
- # Optional arguments:
631
- # :levels => true - method will print the level numbers for each element.
632
- # :tree => true - method will print a tree structure for the elements.
633
- # :file => true - method will print to file instead of printing to screen.
634
- def print(pos, options={})
635
- # Process option values, setting defaults for the ones that are not specified:
636
- opt_levels = options[:levels] || false
637
- opt_tree = options[:tree] || false
638
- opt_file = options[:file] || false
639
- # If pos is false, abort, and inform the user:
640
- if pos == false
641
- add_msg("Warning: Method print was supplied false instead of a valid position. Aborting print.")
642
- return
643
- end
644
- if not pos.is_a?(Array) and pos != true
645
- # Convert to array if number:
646
- pos_valid = [pos]
647
- elsif pos == true
648
- # Create a complete array of indices:
649
- pos_valid = Array.new(@names.length) {|i| i}
650
- else
651
- # Use the supplied array of numbers:
652
- pos_valid = pos
653
- end
654
- # Extract the information to be printed from the object arrays:
655
- indices = Array.new
656
- levels = Array.new
657
- tags = Array.new
658
- names = Array.new
659
- types = Array.new
660
- lengths = Array.new
661
- values = Array.new
662
- # There may be a more elegant way to do this.
663
- pos_valid.each do |pos|
664
- tags << @tags[pos]
665
- levels << @levels[pos]
666
- names << @names[pos]
667
- types << @types[pos]
668
- lengths << @lengths[pos].to_s
669
- values << @values[pos].to_s
670
- end
671
- # We have collected the data that is to be printed, now we need to do some string manipulation if hierarchy is to be displayed:
672
- if opt_tree
673
- # Tree structure requested.
674
- front_symbol = "| "
675
- tree_symbol = "|_"
676
- tags.each_index do |i|
677
- if levels[i] != 0
678
- tags[i] = front_symbol*(levels[i]-1) + tree_symbol + tags[i]
679
- end
680
- end
681
- end
682
- # Extract the string lengths which are needed to make the formatting nice:
683
- tag_lengths = Array.new
684
- name_lengths = Array.new
685
- type_lengths = Array.new
686
- length_lengths = Array.new
687
- names.each_index do |i|
688
- tag_lengths[i] = tags[i].length
689
- name_lengths[i] = names[i].length
690
- type_lengths[i] = types[i].length
691
- length_lengths[i] = lengths[i].to_s.length
692
- end
693
- # To give the printed output a nice format we need to check the string lengths of some of these arrays:
694
- index_maxL = pos_valid.max.to_s.length
695
- tag_maxL = tag_lengths.max
696
- name_maxL = name_lengths.max
697
- type_maxL = type_lengths.max
698
- length_maxL = length_lengths.max
699
- # Construct the strings, one for each line of output, where each line contain the information of one data element:
700
- elements = Array.new
701
- # Start of loop which formats the element data:
702
- # (This loop is what consumes most of the computing time of this method)
703
- tags.each_index do |i|
704
- # Configure empty spaces:
705
- s = " "
706
- f0 = " "*(index_maxL-pos_valid[i].to_s.length)
707
- f2 = " "*(tag_maxL-tags[i].length+1)
708
- f3 = " "*(name_maxL-names[i].length+1)
709
- f4 = " "*(type_maxL-types[i].length+1)
710
- f5 = " "*(length_maxL-lengths[i].to_s.length)
711
- # Display levels?
712
- if opt_levels
713
- lev = levels[i].to_s + s
714
- else
715
- lev = ""
716
- end
717
- # Restrict length of value string:
718
- if values[i].length > 28
719
- value = (values[i])[0..27]+" ..."
720
- else
721
- value = (values[i])
722
- end
723
- # Insert descriptive text for elements that hold binary data:
724
- case types[i]
725
- when "OW","OB","UN"
726
- value = "(Binary Data)"
727
- when "SQ","()"
728
- value = "(Encapsulated Elements)"
729
- end
730
- elements << (f0 + pos_valid[i].to_s + s + lev + s + tags[i] + f2 + names[i] + f3 + types[i] + f4 + f5 + lengths[i].to_s + s + s + value.rstrip)
731
- end
732
- # Print to either screen or file, depending on what the user requested:
733
- if opt_file
734
- print_file(elements)
735
- else
736
- print_screen(elements)
737
- end
738
- end # of print
739
-
740
-
741
- # Prints the key structural properties of the DICOM file.
742
- def print_properties
743
- # Explicitness:
744
- if @explicit
745
- explicit = "Explicit"
746
- else
747
- explicit = "Implicit"
748
- end
749
- # Endianness:
750
- if @file_endian
751
- endian = "Big Endian"
752
- else
753
- endian = "Little Endian"
754
- end
755
- # Pixel data:
756
- if @compression == nil
757
- pixels = "No"
758
- else
759
- pixels = "Yes"
760
- end
761
- # Colors:
762
- if @color
763
- image = "Colors"
764
- else
765
- image = "Greyscale"
766
- end
767
- # Compression:
768
- if @compression == true
769
- compression = @lib.get_uid(get_value("0002,0010").rstrip)
770
- else
771
- compression = "No"
772
- end
773
- # Bits per pixel (allocated):
774
- bits = get_value("0028,0100", :array => true)
775
- bits = bits[0].to_s if bits
776
- # Print the file properties:
777
- puts "Key properties of DICOM object:"
778
- puts "-------------------------------"
779
- puts "File: " + @file
780
- puts "Modality: " + @modality.to_s
781
- puts "Value repr.: " + explicit
782
- puts "Byte order: " + endian
783
- puts "Pixel data: " + pixels
784
- if pixels == "Yes"
785
- puts "Image: " + image
786
- puts "Compression: " + compression
787
- puts "Bits per pixel: " + bits
788
- end
789
- puts "-------------------------------"
790
- end # of print_properties
791
-
792
-
793
- ####################################################
794
- ### START OF METHODS FOR WRITING INFORMATION TO THE DICOM OBJECT:
795
- ####################################################
796
-
797
-
798
- # Reads binary information from file and inserts it in the pixel data element:
799
- def set_image_file(file)
800
- # Try to read file:
801
- begin
802
- f = File.new(file, "rb")
803
- bin = f.read(f.stat.size)
804
- rescue
805
- # Reading file was not successful. Register an error message.
806
- add_msg("Reading specified file was not successful for some reason. No data has been added.")
807
- return
808
- end
809
- if bin.length > 0
810
- pos = @tags.index("7FE0,0010")
811
- # Modify element:
812
- set_value(bin, "7FE0,0010", :create => true, :bin => true)
813
- else
814
- add_msg("Content of file is of zero length. Nothing to store.")
815
- end
816
- end
817
-
818
-
819
- # Transfers pixel data from a RMagick object to the pixel data element:
820
- def set_image_magick(magick_obj)
821
- # Export the RMagick object to a standard Ruby array of numbers:
822
- pixel_array = magick_obj.export_pixels(x=0, y=0, columns=magick_obj.columns, rows=magick_obj.rows, map="I")
823
- # Encode this array using the standard class method:
824
- set_value(pixel_array, "7FE0,0010", :create => true)
825
- end
826
-
827
-
828
- # Removes an element from the DICOM object:
829
- def remove(element)
830
- pos = get_pos(element)
831
- if pos != false
832
- if pos.length > 1
833
- add_msg("Warning: Method remove does not allow an element query which yields multiple array hits. Please use array position instead of tag/name. Value NOT removed.")
834
- else
835
- # Extract first array number:
836
- pos = pos[0]
837
- # Update group length:
838
- if @tags[pos][5..8] != "0000"
839
- change = @lengths[pos]
840
- vr = @types[pos]
841
- update_group_length(pos, vr, change, -1)
842
- end
843
- # Remove entry from arrays:
844
- @tags.delete_at(pos)
845
- @levels.delete_at(pos)
846
- @names.delete_at(pos)
847
- @types.delete_at(pos)
848
- @lengths.delete_at(pos)
849
- @values.delete_at(pos)
850
- @raw.delete_at(pos)
851
- end
852
- else
853
- add_msg("Warning: The data element #{element} could not be found in the DICOM object. Method remove has no data element to remove.")
854
- end
855
- end
856
-
857
-
858
- # Sets the value of a data element by modifying an existing element or creating a new one.
859
- # If the supplied value is not binary, it will attempt to encode the value to binary itself.
860
- def set_value(value, element, options={})
861
- # Options:
862
- create = options[:create] # =false means no element creation
863
- bin = options[:bin] # =true means value already encoded
864
- # Retrieve array position:
865
- pos = get_pos(element)
866
- # We do not support changing multiple data elements:
867
- if pos.is_a?(Array)
868
- if pos.length > 1
869
- add_msg("Warning: Method set_value does not allow an element query which yields multiple array hits. Please use array position instead of tag/name. Value NOT saved.")
870
- return
871
- end
872
- end
873
- if pos == false and create == false
874
- # Since user has requested an element shall only be updated, we can not do so as the element position is not valid:
875
- add_msg("Warning: Invalid data element provided to method set_value. Value NOT updated.")
876
- elsif create == false
877
- # Modify element:
878
- modify_element(value, pos[0], :bin => bin)
879
- else
880
- # User wants to create an element (or modify it if it is already present).
881
- unless pos == false
882
- # The data element already exist, so we modify instead of creating:
883
- modify_element(value, pos[0], :bin => bin)
884
- else
885
- # We need to create element:
886
- tag = @lib.get_tag(element)
887
- if tag == false
888
- add_msg("Warning: Method set_value could not create data element, either because data element name was not recognized in the library, or data element tag is invalid (Expected format of tags is 'GGGG,EEEE').")
889
- else
890
- # As we wish to create a new data element, we need to find out where to insert it in the element arrays:
891
- # We will do this by finding the array position of the last element that will (alphabetically/numerically) stay in front of this element.
892
- if @tags.size > 0
893
- # Search the array:
894
- index = -1
895
- quit = false
896
- while quit != true do
897
- if index+1 >= @tags.length # We have reached end of array.
898
- quit = true
899
- elsif tag < @tags[index+1] and @levels[index+1] == 0 # We are past the correct position (only match against top level tags).
900
- quit = true
901
- else # Increase index in anticipation of a 'hit'.
902
- index += 1
903
- end
904
- end # of while
905
- else
906
- # We are dealing with an empty DICOM object:
907
- index = nil
908
- end
909
- # The necessary information is gathered; create new data element:
910
- create_element(value, tag, index, :bin => bin)
911
- end
912
- end
913
- end
914
- end # of set_value
915
-
916
-
917
- ##################################################
918
- ############## START OF PRIVATE METHODS: ########
919
- ##################################################
920
- private
921
-
922
-
923
- # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
924
- def add_msg(msg)
925
- puts msg if @verbose
926
- @errors << msg
927
- @errors.flatten
928
- end
929
-
930
-
931
- # Creates a new data element:
932
- def create_element(value, tag, last_pos, options={})
933
- bin_only = options[:bin]
934
- # Fetch the VR:
935
- info = @lib.get_name_vr(tag)
936
- vr = info[1]
937
- name = info[0]
938
- # Encode binary (if a binary is not provided):
939
- if bin_only == true
940
- # Data already encoded.
941
- bin = value
942
- value = nil
943
- else
944
- if vr != "UN"
945
- # Encode:
946
- bin = encode(value, vr)
947
- else
948
- add_msg("Error. Unable to encode data element value of unknown type (Value Representation)!")
949
- end
950
- end
951
- # Put the information of this data element into the arrays:
952
- if bin
953
- # 4 different scenarios: Array is empty, or: element is put in front, inside array, or at end of array:
954
- # NB! No support for hierarchy at this time! Defaulting to level = 0.
955
- if last_pos == nil
956
- # We have empty DICOM object:
957
- @tags = [tag]
958
- @levels = [0]
959
- @names = [name]
960
- @types = [vr]
961
- @lengths = [bin.length]
962
- @values = [value]
963
- @raw = [bin]
964
- elsif last_pos == -1
965
- # Insert in front of arrays:
966
- @tags = [tag] + @tags
967
- @levels = [0] + @levels
968
- @names = [name] + @names
969
- @types = [vr] + @types
970
- @lengths = [bin.length] + @lengths
971
- @values = [value] + @values
972
- @raw = [bin] + @raw
973
- elsif last_pos == @tags.length-1
974
- # Insert at end arrays:
975
- @tags = @tags + [tag]
976
- @levels = @levels + [0]
977
- @names = @names + [name]
978
- @types = @types + [vr]
979
- @lengths = @lengths + [bin.length]
980
- @values = @values + [value]
981
- @raw = @raw + [bin]
982
- else
983
- # Insert somewhere inside the array:
984
- @tags = @tags[0..last_pos] + [tag] + @tags[(last_pos+1)..(@tags.length-1)]
985
- @levels = @levels[0..last_pos] + [0] + @levels[(last_pos+1)..(@levels.length-1)]
986
- @names = @names[0..last_pos] + [name] + @names[(last_pos+1)..(@names.length-1)]
987
- @types = @types[0..last_pos] + [vr] + @types[(last_pos+1)..(@types.length-1)]
988
- @lengths = @lengths[0..last_pos] + [bin.length] + @lengths[(last_pos+1)..(@lengths.length-1)]
989
- @values = @values[0..last_pos] + [value] + @values[(last_pos+1)..(@values.length-1)]
990
- @raw = @raw[0..last_pos] + [bin] + @raw[(last_pos+1)..(@raw.length-1)]
991
- end
992
- # Update last index variable as we have added to our arrays:
993
- @last_index += 1
994
- # Update group length (as long as it was not a group length element that was created):
995
- pos = @tags.index(tag)
996
- if @tags[pos][5..8] != "0000"
997
- change = bin.length
998
- update_group_length(pos, vr, change, 1)
999
- end
1000
- else
1001
- add_msg("Binary is nil. Nothing to save.")
1002
- end
1003
- end # of create_element
1004
-
1005
-
1006
- # Encodes a value to binary (used for inserting values into a DICOM object).
1007
- def encode(value, vr)
1008
- # VR will decide how to encode this value:
1009
- case vr
1010
- when "AT" # (Data element tag: Assume it has the format "GGGG,EEEE"
1011
- if value.is_a_tag?
1012
- bin = @stream.encode_tag(value)
1013
- else
1014
- add_msg("Invalid tag format (#{value}). Expected format: 'GGGG,EEEE'")
1015
- end
1016
- # We have a number of VRs that are encoded as string:
1017
- when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT'
1018
- # In case we are dealing with a number string element, the supplied value might be a number
1019
- # instead of a string, and as such, we convert to string just to make sure this will work nicely:
1020
- value = value.to_s
1021
- bin = @stream.encode_value(value, "STR")
1022
- # Image related value representations:
1023
- when "OW"
1024
- # What bit depth to use when encoding the pixel data?
1025
- bit_depth = get_value("0028,0100")
1026
- if bit_depth == false
1027
- # Data element not specified:
1028
- add_msg("Attempted to encode pixel data, but 'Bit Depth' data element is missing (0028,0100).")
1029
- else
1030
- # 8,12 or 16 bits?
1031
- case bit_depth
1032
- when 8
1033
- bin = @stream.encode(value, "BY")
1034
- when 12
1035
- # 12 bit not supported yet!
1036
- add_msg("Encoding 12 bit pixel values not supported yet. Please change the bit depth to 8 or 16 bits.")
1037
- when 16
1038
- bin = @stream.encode(value, "US")
1039
- else
1040
- # Unknown bit depth:
1041
- add_msg("Unknown bit depth #{bit_depth}. No data encoded.")
1042
- end
1043
- end
1044
- # All other VR's:
1045
- else
1046
- # Just encode:
1047
- bin = @stream.encode(value, vr)
1048
- end # of case vr
1049
- return bin
1050
- end # of encode
1051
-
1052
-
1053
- # Modifies existing data element:
1054
- def modify_element(value, pos, options={})
1055
- bin_only = options[:bin]
1056
- # Fetch the VR and old length:
1057
- vr = @types[pos]
1058
- old_length = @lengths[pos]
1059
- # Encode binary (if a binary is not provided):
1060
- if bin_only == true
1061
- # Data already encoded.
1062
- bin = value
1063
- value = nil
1064
- else
1065
- if vr != "UN"
1066
- # Encode:
1067
- bin = encode(value, vr)
1068
- else
1069
- add_msg("Error. Unable to encode data element value of unknown type (Value Representation)!")
1070
- end
1071
- end
1072
- # Update the arrays with this new information:
1073
- if bin
1074
- # Replace array entries for this element:
1075
- #@types[pos] = vr # for the time being there is no logic for updating type.
1076
- @lengths[pos] = bin.length
1077
- @values[pos] = value
1078
- @raw[pos] = bin
1079
- # Update group length (as long as it was not the group length that was modified):
1080
- if @tags[pos][5..8] != "0000"
1081
- change = bin.length - old_length
1082
- update_group_length(pos, vr, change, 0)
1083
- end
1084
- else
1085
- add_msg("Binary is nil. Nothing to save.")
1086
- end
1087
- end
1088
-
1089
-
1090
- # Prints the selected elements to an ascii text file.
1091
- # The text file will be saved in the folder of the original DICOM file,
1092
- # with the original file name plus a .txt extension.
1093
- def print_file(elements)
1094
- File.open( @file + '.txt', 'w' ) do |output|
1095
- elements.each do | line |
1096
- output.print line + "\n"
1097
- end
1098
- end
1099
- end
1100
-
1101
-
1102
- # Prints the selected elements to screen.
1103
- def print_screen(elements)
1104
- elements.each do |element|
1105
- puts element
1106
- end
1107
- end
1108
-
1109
-
1110
- # Sets the modality variable of the current DICOM object, by querying the library with the object's SOP Class UID.
1111
- def set_modality
1112
- value = get_value("0008,0016", :silent => true)
1113
- if value == false
1114
- @modality = "Not specified"
1115
- else
1116
- modality = @lib.get_uid(value.rstrip)
1117
- @modality = modality
1118
- end
1119
- end
1120
-
1121
-
1122
- # Handles the creation of a DWrite object, and returns this object to the calling method.
1123
- def set_write_object(file_name = nil, transfer_syntax = nil)
1124
- unless transfer_syntax
1125
- transfer_syntax = get_value("0002,0010", :silent => true)
1126
- transfer_syntax = "1.2.840.10008.1.2" if not transfer_syntax # Default is implicit, little endian
1127
- end
1128
- w = DWrite.new(file_name, :lib => @lib, :sys_endian => @sys_endian, :transfer_syntax => transfer_syntax)
1129
- w.tags = @tags
1130
- w.types = @types
1131
- w.lengths = @lengths
1132
- w.raw = @raw
1133
- w.rest_endian = @file_endian
1134
- w.rest_explicit = @explicit
1135
- return w
1136
- end
1137
-
1138
-
1139
- # Updates the group length value when a data element has been updated, created or removed:
1140
- # The variable change holds the change in value length for the updated data element.
1141
- # (Change should be positive when a data element is removed - it will only be negative when editing an element to a shorter value)
1142
- # The variable existance is -1 if data element has been removed, +1 if element has been added and 0 if it has been updated.
1143
- # (Perhaps in the future this functionality might be moved to the DWrite class, it might give an easier implementation)
1144
- def update_group_length(pos, type, change, existance)
1145
- # Find position of relevant group length (if it exists):
1146
- gl_pos = @tags.index(@tags[pos][0..4] + "0000")
1147
- existance = 0 if existance == nil
1148
- # If it exists, calculate change:
1149
- if gl_pos
1150
- if existance == 0
1151
- # Element has only been updated, so we only need to think about value change:
1152
- value = @values[gl_pos] + change
1153
- else
1154
- # Element has either been created or removed. This means we need to calculate the length of its other parts.
1155
- if @explicit
1156
- # In the explicit scenario it is slightly complex to determine this value:
1157
- element_length = 0
1158
- # VR?:
1159
- unless @tags[pos] == "FFFE,E000" or @tags[pos] == "FFFE,E00D" or @tags[pos] == "FFFE,E0DD"
1160
- element_length += 2
1161
- end
1162
- # Length value:
1163
- case @types[pos]
1164
- when "OB","OW","SQ","UN"
1165
- if pos > @tags.index("7FE0,0010").to_i and @tags.index("7FE0,0010").to_i != 0
1166
- element_length += 4
1167
- else
1168
- element_length += 6
1169
- end
1170
- when "()"
1171
- element_length += 4
1172
- else
1173
- element_length += 2
1174
- end # of case
1175
- else
1176
- # In the implicit scenario it is easier:
1177
- element_length = 4
1178
- end
1179
- # Update group length for creation/deletion scenario:
1180
- change = (4 + element_length + change) * existance
1181
- value = @values[gl_pos] + change
1182
- end
1183
- # Write the new Group Length value:
1184
- # Encode the new value to binary:
1185
- bin = encode(value, "UL")
1186
- # Update arrays:
1187
- @values[gl_pos] = value
1188
- @raw[gl_pos] = bin
1189
- end
1190
- end # of update_group_length
1191
-
1192
-
1193
- end # of class
1194
- end # of module