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 2008-2009 Christoffer Lervag
1
+ # Copyright 2008-2010 Christoffer Lervag
2
2
 
3
3
  # Some notes about this DICOM file reading class:
4
4
  # In addition to reading files that are compliant to DICOM 3 Part 10, the philosophy of this library
@@ -11,20 +11,19 @@ module DICOM
11
11
  # Class for reading the data from a DICOM file:
12
12
  class DRead
13
13
 
14
- attr_reader :success, :names, :tags, :types, :lengths, :values, :raw, :levels, :explicit, :file_endian, :msg
14
+ attr_reader :success, :names, :tags, :vr, :lengths, :values, :bin, :levels, :explicit, :file_endian, :msg
15
15
 
16
16
  # Initialize the DRead instance.
17
17
  def initialize(string=nil, options={})
18
18
  # Process option values, setting defaults for the ones that are not specified:
19
- @lib = options[:lib] || DLibrary.new
20
19
  @sys_endian = options[:sys_endian] || false
21
- @bin = options[:bin]
20
+ @bin_string = options[:bin]
22
21
  @transfer_syntax = options[:syntax]
23
22
  # Initiate the variables that are used during file reading:
24
23
  init_variables
25
24
 
26
25
  # Are we going to read from a file, or read from a binary string:
27
- if @bin
26
+ if @bin_string
28
27
  # Read from the provided binary string:
29
28
  @str = string
30
29
  else
@@ -43,7 +42,7 @@ module DICOM
43
42
  # Create a Stream instance to handle the decoding of content from this binary string:
44
43
  @stream = Stream.new(@str, @file_endian, @explicit)
45
44
  # Do not check for header information when supplied a (network) binary string:
46
- unless @bin
45
+ unless @bin_string
47
46
  # Read and verify the DICOM header:
48
47
  header = check_header
49
48
  # If the file didnt have the expected header, we will attempt to read
@@ -66,11 +65,11 @@ module DICOM
66
65
  # Post processing:
67
66
  # Assume file has been read successfully:
68
67
  @success = true
69
- # Check if the last element was read out correctly (that the length of its data (@raw.last.length)
68
+ # Check if the last element was read out correctly (that the length of its data (@bin.last.length)
70
69
  # corresponds to that expected by the length specified in the DICOM file (@lengths.last)).
71
70
  # We only run this test if the last element has a positive expectation value, obviously.
72
71
  if @lengths.last.to_i > 0
73
- if @raw.last.length != @lengths.last
72
+ if @bin.last.length != @lengths.last
74
73
  @msg << "Error! The data content read from file does not match the length specified for the tag #{@tags.last}. It seems this is either an invalid or corrupt DICOM file. Returning."
75
74
  @success = false
76
75
  return
@@ -176,17 +175,17 @@ module DICOM
176
175
  return false
177
176
  end
178
177
  # STEP 2: ------------------------------------------------------
179
- # Access library to retrieve the data element name and type (VR) from the tag we just read:
180
- lib_data = @lib.get_name_vr(tag)
178
+ # Access library to retrieve the data element name and VR from the tag we just read:
179
+ lib_data = LIBRARY.get_name_vr(tag)
181
180
  name = lib_data[0]
182
181
  vr = lib_data[1]
183
182
  # (Note: VR will be overwritten if the DICOM file contains VR)
184
183
 
185
184
  # STEP 3: ----------------------------------------------------
186
- # Read type (VR) (if it exists) and the length value:
187
- tag_info = read_type_length(vr,tag)
188
- type = tag_info[0]
189
- level_type = type
185
+ # Read VR (if it exists) and the length value:
186
+ tag_info = read_vr_length(vr,tag)
187
+ vr = tag_info[0]
188
+ level_vr = vr
190
189
  length = tag_info[1]
191
190
 
192
191
  # STEP 4: ----------------------------------------
@@ -195,37 +194,37 @@ module DICOM
195
194
  if @enc_image and tag == "FFFE,E000"
196
195
  # The first item appearing after the image element is a 'normal' item, the rest hold image data.
197
196
  # Note that the first item will contain data if there are multiple images, and so must be read.
198
- type = "OW" # how about alternatives like OB?
197
+ vr = "OW" # how about alternatives like OB?
199
198
  # Modify name of item if this is an item that holds pixel data:
200
199
  if @tags.last != "7FE0,0010"
201
200
  name = "Pixel Data Item"
202
201
  end
203
202
  end
204
203
  # Read the value of the element (if it contains data, and it is not a sequence or ordinary item):
205
- if length.to_i > 0 and type != "SQ" and type != "()"
204
+ if length.to_i > 0 and vr != "SQ" and vr != "()"
206
205
  # Read the element's value (data):
207
- data = read_value(type,length)
206
+ data = read_value(vr,length)
208
207
  value = data[0]
209
- raw = data[1]
208
+ bin = data[1]
210
209
  else
211
210
  # Data element has no value (data).
212
211
  # Special case: Check if pixel data element is sequenced:
213
212
  if tag == "7FE0,0010"
214
- # Change name and type of pixel data element if it does not contain data itself:
213
+ # Change name and vr of pixel data element if it does not contain data itself:
215
214
  name = "Encapsulated Pixel Data"
216
- level_type = "SQ"
215
+ level_vr = "SQ"
217
216
  @enc_image = true
218
217
  end
219
218
  end # of if length.to_i > 0
220
219
  # Set the hiearchy level of this data element:
221
- set_level(level_type, length, tag, name)
220
+ set_level(level_vr, length, tag, name)
222
221
  # Transfer the gathered data to arrays and return true:
223
222
  @names << name
224
223
  @tags << tag
225
- @types << type
224
+ @vr << vr
226
225
  @lengths << length
227
226
  @values << value
228
- @raw << raw
227
+ @bin << bin
229
228
  return true
230
229
  end # of process_data_element
231
230
 
@@ -252,8 +251,8 @@ module DICOM
252
251
  end
253
252
 
254
253
 
255
- # Reads and returns data element TYPE (VR) (2 bytes) and data element LENGTH (Varying length; 2-6 bytes).
256
- def read_type_length(type,tag)
254
+ # Reads and returns data element VR (2 bytes) and data element LENGTH (Varying length; 2-6 bytes).
255
+ def read_vr_length(vr,tag)
257
256
  # Structure will differ, dependent on whether we have explicit or implicit encoding:
258
257
  pre_skip = 0
259
258
  bytes = 0
@@ -261,14 +260,14 @@ module DICOM
261
260
  if @explicit == true
262
261
  # Step 1: Read VR (if it exists)
263
262
  unless tag == "FFFE,E000" or tag == "FFFE,E00D" or tag == "FFFE,E0DD"
264
- # Read the element's type (2 bytes - since we are not dealing with an item related element):
265
- type = @stream.decode(2, "STR")
263
+ # Read the element's vr (2 bytes - since we are not dealing with an item related element):
264
+ vr = @stream.decode(2, "STR")
266
265
  @integrated_lengths[@integrated_lengths.length-1] += 2
267
266
  end
268
267
  # Step 2: Read length
269
- # Three possible structures for value length here, dependent on element type:
270
- case type
271
- when "OB","OW","SQ","UN"
268
+ # Three possible structures for value length here, dependent on element vr:
269
+ case vr
270
+ when "OB","OW","SQ","UN","UT"
272
271
  # 6 bytes total:
273
272
  # Two empty bytes first:
274
273
  pre_skip = 2
@@ -276,11 +275,11 @@ module DICOM
276
275
  bytes = 4
277
276
  when "()"
278
277
  # 4 bytes:
279
- # For elements "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
278
+ # For elements "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD":
280
279
  bytes = 4
281
280
  else
282
281
  # 2 bytes:
283
- # For all the other element types, value length is 2 bytes:
282
+ # For all the other element vr, value length is 2 bytes:
284
283
  bytes = 2
285
284
  end
286
285
  else
@@ -306,23 +305,23 @@ module DICOM
306
305
  # If it is not, it may indicate a file that is not standards compliant or it might even not be a DICOM file.
307
306
  @msg += ["Warning: Odd number of bytes in data element's length occured. This is a violation of the DICOM standard, but program will attempt to read the rest of the file anyway."]
308
307
  end
309
- return [type, length]
310
- end # of read_type_length
308
+ return [vr, length]
309
+ end # of read_vr_length
311
310
 
312
311
 
313
312
  # Reads and returns data element VALUE (Of varying length - which is determined at an earlier stage).
314
- def read_value(type, length)
313
+ def read_value(vr, length)
315
314
  # Extract the binary data:
316
315
  bin = @stream.extract(length)
317
316
  @integrated_lengths[@integrated_lengths.size-1] += length
318
317
  # Decode data?
319
318
  # Some data elements (like those containing image data, compressed data or unknown data),
320
319
  # will not be decoded here.
321
- unless type == "OW" or type == "OB" or type == "OF" or type == "UN"
320
+ unless vr == "OW" or vr == "OB" or vr == "OF" or vr == "UN"
322
321
  # "Rewind" and extract the value from this binary data:
323
322
  @stream.skip(-length)
324
323
  # Decode data:
325
- value = @stream.decode(length, type)
324
+ value = @stream.decode(length, vr)
326
325
  if not value.is_a?(Array)
327
326
  data = value
328
327
  else
@@ -341,7 +340,7 @@ module DICOM
341
340
 
342
341
  # Sets the level of the current element in the hiearchy.
343
342
  # The default (top) level is zero.
344
- def set_level(type, length, tag, name)
343
+ def set_level(vr, length, tag, name)
345
344
  # Set the level of this element:
346
345
  @levels += [@current_level]
347
346
  # Determine if there is a level change for the following element:
@@ -350,7 +349,7 @@ module DICOM
350
349
  # Note the following exception:
351
350
  # If data element is an "Item", and it contains data (image fragment) directly, which is to say,
352
351
  # not in its sub-elements, we should not increase the level. (This is fixed in the process_data_element method.)
353
- if type == "SQ"
352
+ if vr == "SQ"
354
353
  increase = true
355
354
  elsif name == "Item"
356
355
  increase = true
@@ -363,7 +362,7 @@ module DICOM
363
362
  if length.to_i != 0
364
363
  @hierarchy << [length, @integrated_lengths.last]
365
364
  else
366
- @hierarchy << type
365
+ @hierarchy << vr
367
366
  end
368
367
  end
369
368
  # Need to check whether a previous sequence or item has ended, if so the level must be decreased by one:
@@ -448,7 +447,7 @@ module DICOM
448
447
  end
449
448
  end
450
449
  # Query the library with our particular transfer syntax string:
451
- result = @lib.process_transfer_syntax(@transfer_syntax)
450
+ result = LIBRARY.process_transfer_syntax(@transfer_syntax)
452
451
  # Result is a 3-element array: [Validity of ts, explicitness, endianness]
453
452
  unless result[0]
454
453
  @msg+=["Warning: Invalid/unknown transfer syntax! Will try reading the file, but errors may occur."]
@@ -487,10 +486,10 @@ module DICOM
487
486
  # Arrays that will hold information from the elements of the DICOM file:
488
487
  @names = Array.new
489
488
  @tags = Array.new
490
- @types = Array.new
489
+ @vr = Array.new
491
490
  @lengths = Array.new
492
491
  @values = Array.new
493
- @raw = Array.new
492
+ @bin = Array.new
494
493
  @levels = Array.new
495
494
  # Array that will holde any messages generated while reading the DICOM file:
496
495
  @msg = Array.new
@@ -1,4 +1,4 @@
1
- # Copyright 2009 Christoffer Lervag
1
+ # Copyright 2009-2010 Christoffer Lervag
2
2
 
3
3
  module DICOM
4
4
 
@@ -6,16 +6,25 @@ module DICOM
6
6
  # which will act as a simple storage node (a server that receives images).
7
7
  class DServer
8
8
 
9
- attr_accessor :host_ae, :max_package_size, :port, :timeout, :verbose
9
+
10
+ # Run the server and take a block for initializing.
11
+ def self.run(port=104, path='./received/', &block)
12
+ server = DServer.new(port)
13
+ server.instance_eval(&block)
14
+ server.start_scp(path)
15
+ end
16
+
17
+ # Accessible attributes:
18
+ attr_accessor :host_ae, :max_package_size, :port, :timeout, :verbose, :file_handler
10
19
  attr_reader :errors, :notices
11
20
 
12
21
  # Initialize the instance with a host adress and a port number.
13
- def initialize(port, options={})
22
+ def initialize(port=104, options={})
14
23
  require 'socket'
15
24
  # Required parameters:
16
25
  @port = port
17
26
  # Optional parameters (and default values):
18
- @lib = options[:lib] || DLibrary.new
27
+ @file_handler = options[:file_handler] || FileHandler
19
28
  @host_ae = options[:host_ae] || "RUBY_DICOM"
20
29
  @max_package_size = options[:max_package_size] || 32768 # 16384
21
30
  @timeout = options[:timeout] || 10 # seconds
@@ -70,10 +79,9 @@ module DICOM
70
79
  @valid_abstract_syntaxes = Array.new
71
80
  end
72
81
 
73
-
74
- # Start a Storage Content Provider (SCP).
82
+ # Start a Service Class Provider (SCP).
75
83
  # This service will receive and store DICOM files in a specified folder.
76
- def start_scp(path)
84
+ def start_scp(path='./received/')
77
85
  add_notice("Starting SCP server...")
78
86
  add_notice("*********************************")
79
87
  # Initiate server:
@@ -82,16 +90,17 @@ module DICOM
82
90
  loop do
83
91
  Thread.start(@scp.accept) do |session|
84
92
  # Initialize the network package handler for this session:
85
- link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose)
93
+ link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose, :file_handler => @file_handler)
86
94
  add_notice("Connection established (name: #{session.peeraddr[2]}, ip: #{session.peeraddr[3]})")
87
95
  # Receive an incoming message:
88
- segments = link.receive_single_transmission(session)
96
+ #segments = link.receive_single_transmission(session)
97
+ segments = link.receive_multiple_transmissions(session)
89
98
  info = segments.first
90
99
  # Interpret the received message:
91
100
  if info[:valid]
92
101
  association_error = check_association_request(info)
93
102
  unless association_error
94
- syntax_result = check_syntax_requests(info)
103
+ syntax_result = check_syntax_requests(link, info)
95
104
  link.handle_association_accept(session, info, syntax_result)
96
105
  if syntax_result == "00" # Normal (no error)
97
106
  add_notice("An incoming association request and its abstract syntax has been accepted.")
@@ -100,10 +109,15 @@ module DICOM
100
109
  link.handle_release(session)
101
110
  else
102
111
  # Process the incoming data:
103
- file_path = link.handle_incoming_data(session, path)
104
- add_notice("DICOM file saved to: " + file_path)
105
- # Send a receipt for received data:
106
- link.handle_response(session)
112
+ success, message = link.handle_incoming_data(session, path)
113
+ if success
114
+ add_notice(message)
115
+ # Send a receipt for received data:
116
+ link.handle_response(session)
117
+ else
118
+ # Something has gone wrong:
119
+ add_error(message)
120
+ end
107
121
  # Release the connection:
108
122
  link.handle_release(session)
109
123
  end
@@ -171,17 +185,17 @@ module DICOM
171
185
 
172
186
  # Check if the requested abstract syntax & transfer syntax are supported:
173
187
  # Error codes are given in the official dicom document, part 08_08, page 39
174
- def check_syntax_requests(info)
188
+ def check_syntax_requests(link, info)
175
189
  result = "00" # (no error)
176
190
  # We will accept any transfer syntax (as long as it is recognized in the library):
177
191
  # (Weakness: Only checking the first occuring transfer syntax for now)
178
- transfer_syntax = info[:ts].first[:transfer_syntax]
179
- unless @lib.check_ts_validity(transfer_syntax)
192
+ transfer_syntax = link.extract_transfer_syntax(info)
193
+ unless LIBRARY.check_ts_validity(transfer_syntax)
180
194
  result = "04" # transfer syntax not supported
181
195
  add_error("Warning: Unsupported transfer syntax received in incoming association request. (#{transfer_syntax})")
182
196
  end
183
197
  # Check that abstract syntax is among the ones that have been set as valid for this server instance:
184
- abstract_syntax = info[:abstract_syntax]
198
+ abstract_syntax = link.extract_abstract_syntax(info)
185
199
  unless @valid_abstract_syntaxes.include?(abstract_syntax)
186
200
  result = "03" # abstract syntax not supported
187
201
  end
@@ -286,5 +300,5 @@ module DICOM
286
300
  end
287
301
 
288
302
 
289
- end
290
- end
303
+ end # of class
304
+ end # of module
@@ -1,4 +1,4 @@
1
- # Copyright 2008-2009 Christoffer Lervag
1
+ # Copyright 2008-2010 Christoffer Lervag
2
2
 
3
3
  # Some notes about this DICOM file writing class:
4
4
  # In its current state, this class will always try to write the file such that it is compliant to the
@@ -14,22 +14,21 @@ module DICOM
14
14
  # Class for writing the data from a DObject to a valid DICOM file.
15
15
  class DWrite
16
16
 
17
- attr_writer :tags, :types, :lengths, :raw, :rest_endian, :rest_explicit
17
+ attr_writer :tags, :vr, :lengths, :bin, :rest_endian, :rest_explicit
18
18
  attr_reader :success, :msg
19
19
 
20
20
  # Initialize the DWrite instance.
21
21
  def initialize(file_name=nil, options={})
22
22
  # Process option values, setting defaults for the ones that are not specified:
23
- @lib = options[:lib] || DLibrary.new
24
23
  @sys_endian = options[:sys_endian] || false
25
24
  @file_name = file_name
26
25
  @transfer_syntax = options[:transfer_syntax] || "1.2.840.10008.1.2" # Implicit, little endian
27
26
 
28
27
  # Create arrays used for storing data element information:
29
28
  @tags = Array.new
30
- @types = Array.new
29
+ @vr = Array.new
31
30
  @lengths = Array.new
32
- @raw = Array.new
31
+ @bin = Array.new
33
32
  # Array for storing error/warning messages:
34
33
  @msg = Array.new
35
34
  # Default values that may be overwritten by the user:
@@ -99,12 +98,12 @@ module DICOM
99
98
  if value_length > size
100
99
  # Start writing content from this data element,
101
100
  # then continue writing its content in the next segments.
102
- # Write tag & type/length:
101
+ # Write tag & vr/length:
103
102
  write_tag(i)
104
- write_type_length(i)
103
+ write_vr_length(i)
105
104
  # Find out how much of this element's value we can write, then add it:
106
105
  available = size - @stream.length
107
- value_first_part = @raw[i].slice(0, available)
106
+ value_first_part = @bin[i].slice(0, available)
108
107
  @stream.add_last(value_first_part)
109
108
  # Add segment and reset:
110
109
  segments << @stream.string
@@ -114,7 +113,7 @@ module DICOM
114
113
  index = available
115
114
  # Iterate through the data element's value until we have added it entirely:
116
115
  remaining_segments.times do
117
- value = @raw[i].slice(index, size)
116
+ value = @bin[i].slice(index, size)
118
117
  index = index + size
119
118
  @stream.add_last(value)
120
119
  # Add segment and reset:
@@ -183,14 +182,14 @@ module DICOM
183
182
  tag = @stream.encode_tag("0002,0012")
184
183
  @stream.add_last(tag)
185
184
  @stream.encode_last("UI", "STR")
186
- value = @stream.encode_value(@implementation_uid, "STR")
185
+ value = @stream.encode_value(UID, "STR")
187
186
  @stream.encode_last(value.length, "US")
188
187
  @stream.add_last(value)
189
188
  # Implementation Version Name:
190
189
  tag = @stream.encode_tag("0002,0013")
191
190
  @stream.add_last(tag)
192
191
  @stream.encode_last("SH", "STR")
193
- value = @stream.encode_value(@implementation_name, "STR")
192
+ value = @stream.encode_value(NAME, "STR")
194
193
  @stream.encode_last(value.length, "US")
195
194
  @stream.add_last(value)
196
195
  # Group length:
@@ -201,9 +200,9 @@ module DICOM
201
200
  # Length:
202
201
  length = @stream.encode(4, "US")
203
202
  @stream.add_first(length)
204
- # Type:
205
- type = @stream.encode("UL", "STR")
206
- @stream.add_first(type)
203
+ # VR:
204
+ vr = @stream.encode("UL", "STR")
205
+ @stream.add_first(vr)
207
206
  # Tag:
208
207
  tag = @stream.encode_tag("0002,0000")
209
208
  @stream.add_first(tag)
@@ -216,8 +215,8 @@ module DICOM
216
215
  def write_data_element(i)
217
216
  # Step 1: Write tag:
218
217
  write_tag(i)
219
- # Step 2: Write [type] and value length:
220
- write_type_length(i)
218
+ # Step 2: Write [vr] and value length:
219
+ write_vr_length(i)
221
220
  # Step 3: Write value:
222
221
  write_value(i)
223
222
  # If DICOM object contains encapsulated pixel data, we need some special handling for its items:
@@ -242,9 +241,9 @@ module DICOM
242
241
  end
243
242
 
244
243
 
245
- # Writes the type (VR) (if it is to be written) and length value
244
+ # Writes the VR (if it is to be written) and length value
246
245
  # (these two are the middle part of the data element):
247
- def write_type_length(i)
246
+ def write_vr_length(i)
248
247
  # First some preprocessing:
249
248
  # Set length value:
250
249
  if @lengths[i] == nil
@@ -265,14 +264,14 @@ module DICOM
265
264
  if @explicit == true
266
265
  # Step 1: Write VR (if it is to be written)
267
266
  unless @tags[i] == "FFFE,E000" or @tags[i] == "FFFE,E00D" or @tags[i] == "FFFE,E0DD"
268
- # Write data element type (VR) (2 bytes - since we are not dealing with an item related element):
269
- vr = @stream.encode(@types[i], "STR")
267
+ # Write data element VR (2 bytes - since we are not dealing with an item related element):
268
+ vr = @stream.encode(@vr[i], "STR")
270
269
  add(vr)
271
270
  end
272
271
  # Step 2: Write length
273
- # Three possible structures for value length here, dependent on data element type:
274
- case @types[i]
275
- when "OB","OW","SQ","UN"
272
+ # Three possible structures for value length here, dependent on data element vr:
273
+ case @vr[i]
274
+ when "OB","OW","SQ","UN","UT"
276
275
  if @enc_image
277
276
  # Item under an encapsulated Pixel Data (7FE0,0010):
278
277
  # 4 bytes:
@@ -291,22 +290,22 @@ module DICOM
291
290
  add(length4)
292
291
  else
293
292
  # 2 bytes:
294
- # For all the other data element types, value length is 2 bytes:
293
+ # For all the other data element vr, value length is 2 bytes:
295
294
  add(length2)
296
- end # of case type
295
+ end
297
296
  else
298
297
  # *****IMPLICIT*****:
299
298
  # No VR written.
300
299
  # Writing value length (4 bytes):
301
300
  add(length4)
302
301
  end
303
- end # of write_type_length
302
+ end # of write_vr_length
304
303
 
305
304
 
306
305
  # Writes the value (last part of the data element):
307
306
  def write_value(i)
308
307
  # This is pretty straightforward, just dump the binary data to the file/string:
309
- add(@raw[i])
308
+ add(@bin[i])
310
309
  end
311
310
 
312
311
 
@@ -347,7 +346,7 @@ module DICOM
347
346
  # Changes encoding variables as the file writing proceeds past the initial 0002 group of the DICOM file.
348
347
  def switch_syntax
349
348
  # The information from the Transfer syntax element (if present), needs to be processed:
350
- result = @lib.process_transfer_syntax(@transfer_syntax.rstrip)
349
+ result = LIBRARY.process_transfer_syntax(@transfer_syntax.rstrip)
351
350
  # Result is a 3-element array: [Validity of ts, explicitness, endianness]
352
351
  unless result[0]
353
352
  @msg << "Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."
@@ -405,9 +404,6 @@ module DICOM
405
404
  end
406
405
  # Items contained under the Pixel Data element needs some special attention to write correctly:
407
406
  @enc_image = false
408
- # Version information:
409
- @implementation_uid = "1.2.826.0.1.3680043.8.641"
410
- @implementation_name = "RUBY_DICOM_0.6"
411
407
  end
412
408
 
413
409
  end # of class