WriteExcel 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +17 -0
  5. data/Rakefile +47 -0
  6. data/VERSION +1 -0
  7. data/examples/a_simple.rb +42 -0
  8. data/examples/autofilters.rb +266 -0
  9. data/examples/bigfile.rb +30 -0
  10. data/examples/copyformat.rb +51 -0
  11. data/examples/data_validate.rb +278 -0
  12. data/examples/date_time.rb +86 -0
  13. data/examples/demo.rb +118 -0
  14. data/examples/diag_border.rb +35 -0
  15. data/examples/formats.rb +489 -0
  16. data/examples/header.rb +136 -0
  17. data/examples/hidden.rb +28 -0
  18. data/examples/hyperlink.rb +42 -0
  19. data/examples/images.rb +52 -0
  20. data/examples/merge1.rb +39 -0
  21. data/examples/merge2.rb +44 -0
  22. data/examples/merge3.rb +65 -0
  23. data/examples/merge4.rb +82 -0
  24. data/examples/merge5.rb +79 -0
  25. data/examples/protection.rb +46 -0
  26. data/examples/regions.rb +52 -0
  27. data/examples/repeat.rb +42 -0
  28. data/examples/stats.rb +75 -0
  29. data/examples/stocks.rb +80 -0
  30. data/examples/tab_colors.rb +30 -0
  31. data/lib/WriteExcel.rb +30 -0
  32. data/lib/WriteExcel/biffwriter.rb +259 -0
  33. data/lib/WriteExcel/chart.rb +217 -0
  34. data/lib/WriteExcel/excelformula.y +138 -0
  35. data/lib/WriteExcel/excelformulaparser.rb +573 -0
  36. data/lib/WriteExcel/format.rb +1108 -0
  37. data/lib/WriteExcel/formula.rb +986 -0
  38. data/lib/WriteExcel/olewriter.rb +322 -0
  39. data/lib/WriteExcel/properties.rb +250 -0
  40. data/lib/WriteExcel/storage_lite.rb +590 -0
  41. data/lib/WriteExcel/workbook.rb +2602 -0
  42. data/lib/WriteExcel/worksheet.rb +6378 -0
  43. data/spec/WriteExcel_spec.rb +7 -0
  44. data/spec/spec.opts +1 -0
  45. data/spec/spec_helper.rb +9 -0
  46. data/test/tc_all.rb +31 -0
  47. data/test/tc_biff.rb +104 -0
  48. data/test/tc_chart.rb +22 -0
  49. data/test/tc_example_match.rb +1280 -0
  50. data/test/tc_format.rb +1264 -0
  51. data/test/tc_formula.rb +63 -0
  52. data/test/tc_ole.rb +110 -0
  53. data/test/tc_storage_lite.rb +102 -0
  54. data/test/tc_workbook.rb +115 -0
  55. data/test/tc_worksheet.rb +115 -0
  56. data/test/test_00_IEEE_double.rb +14 -0
  57. data/test/test_01_add_worksheet.rb +12 -0
  58. data/test/test_02_merge_formats.rb +58 -0
  59. data/test/test_04_dimensions.rb +397 -0
  60. data/test/test_05_rows.rb +182 -0
  61. data/test/test_06_extsst.rb +80 -0
  62. data/test/test_11_date_time.rb +484 -0
  63. data/test/test_12_date_only.rb +506 -0
  64. data/test/test_13_date_seconds.rb +486 -0
  65. data/test/test_21_escher.rb +629 -0
  66. data/test/test_22_mso_drawing_group.rb +739 -0
  67. data/test/test_23_note.rb +78 -0
  68. data/test/test_24_txo.rb +80 -0
  69. data/test/test_26_autofilter.rb +327 -0
  70. data/test/test_27_autofilter.rb +144 -0
  71. data/test/test_28_autofilter.rb +174 -0
  72. data/test/test_29_process_jpg.rb +131 -0
  73. data/test/test_30_validation_dval.rb +82 -0
  74. data/test/test_31_validation_dv_strings.rb +131 -0
  75. data/test/test_32_validation_dv_formula.rb +211 -0
  76. data/test/test_40_property_types.rb +191 -0
  77. data/test/test_41_properties.rb +238 -0
  78. data/test/test_42_set_properties.rb +430 -0
  79. data/test/ts_all.rb +34 -0
  80. metadata +154 -0
@@ -0,0 +1,2602 @@
1
+
2
+ #require "WriteExcel/biffwriter"
3
+ #require "WriteExcel/olewriter"
4
+ #require "WriteExcel/formula"
5
+ #require "WriteExcel/format"
6
+ #require "WriteExcel/worksheet"
7
+ #require "WriteExcel/properties"
8
+ #require "digest/md5"
9
+ #require "WriteExcel/storage_lite"
10
+
11
+ class Workbook < BIFFWriter
12
+ BOF = 11
13
+ EOF = 4
14
+ SheetName = "Sheet"
15
+ NonAscii = /[^!"#\$%&'\(\)\*\+,\-\.\/\:\;<=>\?@0-9A-Za-z_\[\\\]^` ~\0\n]/
16
+
17
+ attr_accessor :date_system, :str_unique, :biff_only
18
+ attr_reader :encoding, :url_format, :parser, :tempdir, :date_1904, :compatibility
19
+ attr_reader :summary
20
+ attr_accessor :activesheet, :firstsheet, :str_total, :str_unique, :str_table
21
+ attr_reader :formats, :xf_index, :worksheets, :extsst_buckets, :extsst_bucket_size
22
+ attr_reader :data
23
+ attr_writer :mso_size
24
+ attr_accessor :localtime
25
+
26
+ ###############################################################################
27
+ #
28
+ # new()
29
+ #
30
+ # Constructor. Creates a new Workbook object from a BIFFwriter object.
31
+ #
32
+ def initialize(filename, default_formats = {})
33
+ super()
34
+ @filename = filename
35
+ @default_formats = default_formats
36
+ @parser = Formula.new(@byte_order)
37
+ @tempdir = nil
38
+ @date_1904 = false
39
+ @sheet =
40
+
41
+ @activesheet = 0
42
+ @firstsheet = 0
43
+ @selected = 0
44
+ @xf_index = 0
45
+ @fileclosed = false
46
+ @biffsize = 0
47
+ @sheetname = "Sheet"
48
+ @url_format = ''
49
+ @codepage = 0x04E4
50
+ @worksheets = []
51
+ @sheetnames = []
52
+ @formats = []
53
+ @palette = []
54
+ @biff_only = 0
55
+
56
+ @internal_fh = 0
57
+ @fh_out = ""
58
+
59
+ @str_total = 0
60
+ @str_unique = 0
61
+ @str_table = {}
62
+ @str_array = []
63
+ @str_block_sizes = []
64
+ @extsst_offsets = []
65
+ @extsst_buckets = 0
66
+ @extsst_bucket_size = 0
67
+
68
+ @ext_ref_count = 0
69
+ @ext_refs = {}
70
+
71
+ @mso_clusters = []
72
+ @mso_size = 0
73
+
74
+ @hideobj = 0
75
+ @compatibility = 0
76
+
77
+ @add_doc_properties = 0
78
+ @summary = ''
79
+ @doc_summary = ''
80
+ @localtime = Time.now
81
+
82
+ # Add the in-built style formats and the default cell format.
83
+ add_format(:type => 1) # 0 Normal
84
+ add_format(:type => 1) # 1 RowLevel 1
85
+ add_format(:type => 1) # 2 RowLevel 2
86
+ add_format(:type => 1) # 3 RowLevel 3
87
+ add_format(:type => 1) # 4 RowLevel 4
88
+ add_format(:type => 1) # 5 RowLevel 5
89
+ add_format(:type => 1) # 6 RowLevel 6
90
+ add_format(:type => 1) # 7 RowLevel 7
91
+ add_format(:type => 1) # 8 ColLevel 1
92
+ add_format(:type => 1) # 9 ColLevel 2
93
+ add_format(:type => 1) # 10 ColLevel 3
94
+ add_format(:type => 1) # 11 ColLevel 4
95
+ add_format(:type => 1) # 12 ColLevel 5
96
+ add_format(:type => 1) # 13 ColLevel 6
97
+ add_format(:type => 1) # 14 ColLevel 7
98
+ add_format(default_formats) # 15 Cell XF
99
+ add_format(:type => 1, :num_format => 0x2B) # 16 Comma
100
+ add_format(:type => 1, :num_format => 0x29) # 17 Comma[0]
101
+ add_format(:type => 1, :num_format => 0x2C) # 18 Currency
102
+ add_format(:type => 1, :num_format => 0x2A) # 19 Currency[0]
103
+ add_format(:type => 1, :num_format => 0x09) # 20 Percent
104
+
105
+ # Add the default format for hyperlinks
106
+ @url_format = add_format(:color => 'blue', :underline => 1)
107
+
108
+ # Convert the filename to a filehandle to pass to the OLE writer when the
109
+ # file is closed. If the filename is a reference it is assumed that it is
110
+ # a valid filehandle.
111
+ #
112
+ if filename.kind_of?(String) && filename != ''
113
+ @fh_out = open(filename, "wb")
114
+ @internal_fh = 1
115
+ else
116
+ print "Workbook#new - filename required."
117
+ exit
118
+ end
119
+
120
+ # Set colour palette.
121
+ set_palette_xl97
122
+
123
+ get_checksum_method
124
+ end
125
+
126
+ ###############################################################################
127
+ #
128
+ # _get_checksum_method.
129
+ #
130
+ # Check for modules available to calculate image checksum. Excel uses MD4 but
131
+ # MD5 will also work.
132
+ #
133
+ # ------- cxn03651 add -------
134
+ # md5 can use in ruby. so, @checksum_method is always 3.
135
+
136
+ def get_checksum_method
137
+ @checksum_method = 3
138
+ end
139
+
140
+ ###############################################################################
141
+ #
142
+ # close()
143
+ #
144
+ # Calls finalization methods and explicitly close the OLEwriter file
145
+ # handle.
146
+ #
147
+ def close
148
+ return if @fileclosed # Prevent close() from being called twice.
149
+
150
+ @fileclosed = true
151
+ return store_workbook
152
+ end
153
+
154
+ ###############################################################################
155
+ #
156
+ # sheets(slice,...)
157
+ #
158
+ # An accessor for the _worksheets[] array
159
+ #
160
+ # Returns: an optionally sliced list of the worksheet objects in a workbook.
161
+ #
162
+ def sheets(*args)
163
+ if args.empty?
164
+ @worksheets
165
+ else
166
+ ary = []
167
+ args.each do |i|
168
+ ary << @worksheets[i]
169
+ end
170
+ ary
171
+ end
172
+ end
173
+
174
+ ###############################################################################
175
+ #
176
+ # add_worksheet($name, $encoding)
177
+ #
178
+ # Add a new worksheet to the Excel workbook.
179
+ #
180
+ # Returns: reference to a worksheet object
181
+ #
182
+ def add_worksheet(name = '', encoding = 0)
183
+ name, encoding = check_sheetname(name, encoding)
184
+
185
+ index = @worksheets.size
186
+
187
+ worksheet = Worksheet.new(
188
+ self,
189
+ name,
190
+ index,
191
+ encoding
192
+ )
193
+ @worksheets[index] = worksheet # Store ref for iterator
194
+ @sheetnames[index] = name # Store EXTERNSHEET names
195
+ # @parser->set_ext_sheets($name, $index) # Store names in Formula.pm
196
+ return worksheet
197
+ end
198
+
199
+ ###############################################################################
200
+ #
201
+ # add_chart_ext($filename, $name)
202
+ #
203
+ # Add an externally created chart.
204
+ #
205
+ #
206
+ def add_chart_ext(filename, name, encoding = nil)
207
+ index = @worksheets.size
208
+
209
+ name, encoding = check_sheetname(name, encoding)
210
+
211
+ init_data = [
212
+ filename,
213
+ name,
214
+ index,
215
+ encoding,
216
+ @activesheet,
217
+ @firstsheet
218
+ ]
219
+
220
+ worksheet = Chart.new(*init_data)
221
+ @worksheets[index] = worksheet # Store ref for iterator
222
+ @sheetnames[index] = name # Store EXTERNSHEET names
223
+ @parser.set_ext_sheets(name, index) # Store names in Formula.pm
224
+ return worksheet
225
+ end
226
+
227
+ ###############################################################################
228
+ #
229
+ # _check_sheetname($name, $encoding)
230
+ #
231
+ # Check for valid worksheet names. We check the length, if it contains any
232
+ # invalid characters and if the name is unique in the workbook.
233
+ #
234
+ def check_sheetname(name, encoding = 0)
235
+ name = '' if name.nil?
236
+ limit = encoding != 0 ? 62 : 31
237
+ invalid_char = %r![\[\]:*?/\\]!
238
+
239
+ # Supply default "Sheet" name if none has been defined.
240
+ index = @worksheets.size
241
+ sheetname = @sheetname
242
+
243
+ if name == ""
244
+ name = sheetname + (index+1).to_s
245
+ encoding = 0
246
+ end
247
+
248
+ # Check that sheetname is <= 31 (1 or 2 byte chars). Excel limit.
249
+ raise "Sheetname #{name} must be <= 31 chars" if name.length > limit
250
+
251
+ # Check that Unicode sheetname has an even number of bytes
252
+ if encoding == 1 and name.length % 2
253
+ raise 'Odd number of bytes in Unicode worksheet name:' + name
254
+ end
255
+
256
+ # Check that sheetname doesn't contain any invalid characters
257
+ if encoding != 1 and name =~ invalid_char
258
+ # Check ASCII names
259
+ raise 'Invalid character []:*?/\\ in worksheet name: ' + name
260
+ else
261
+ # Extract any 8bit clean chars from the UTF16 name and validate them.
262
+ str = name.dup
263
+ while str =~ /../m
264
+ hi, lo = $~[0].unpack('aa')
265
+ if hi == "\0" and lo =~ invalid_char
266
+ raise 'Invalid character []:*?/\\ in worksheet name: ' + name
267
+ end
268
+ str = $~.post_match
269
+ end
270
+ end
271
+
272
+ # Handle utf8 strings
273
+ if name =~ NonAscii
274
+ name = NKF.nkf('-w16B0 -m0 -W', name)
275
+ encoding = 1
276
+ end
277
+
278
+ # Check that the worksheet name doesn't already exist since this is a fatal
279
+ # error in Excel 97. The check must also exclude case insensitive matches
280
+ # since the names 'Sheet1' and 'sheet1' are equivalent. The tests also have
281
+ # to take the encoding into account.
282
+ #
283
+ @worksheets.each do |worksheet|
284
+ name_a = name
285
+ encd_a = encoding
286
+ name_b = worksheet.name
287
+ encd_b = worksheet.encoding
288
+ error = 0;
289
+
290
+ if encd_a == 0 and encd_b == 0
291
+ error = 1 if name_a.downcase == name_b.downcase
292
+ elsif encd_a == 0 and encd_b == 1
293
+ name_a = name_a.unpack("C*").pack("n*")
294
+ error = 1 if name_a.downcase == name_b.downcase
295
+ elsif encd_a == 1 and encd_b == 0
296
+ name_b = name_b.unpack("C*").pack("n*")
297
+ error = 1 if name_a.downcase == name_b.downcase
298
+ elsif encd_a == 1 and encd_b == 1
299
+ # # We can do a true case insensitive test with Perl 5.8 and utf8.
300
+ # if ($] >= 5.008) {
301
+ # $name_a = Encode::decode("UTF-16BE", $name_a);
302
+ # $name_b = Encode::decode("UTF-16BE", $name_b);
303
+ # $error = 1 if lc($name_a) eq lc($name_b);
304
+ # }
305
+ # else {
306
+ # # We can't easily do a case insensitive test of the UTF16 names.
307
+ # # As a special case we check if all of the high bytes are nulls and
308
+ # # then do an ASCII style case insensitive test.
309
+ #
310
+ # # Strip out the high bytes (funkily).
311
+ # my $hi_a = grep {ord} $name_a =~ /(.)./sg;
312
+ # my $hi_b = grep {ord} $name_b =~ /(.)./sg;
313
+ #
314
+ # if ($hi_a or $hi_b) {
315
+ # $error = 1 if $name_a eq $name_b;
316
+ # }
317
+ # else {
318
+ # $error = 1 if lc($name_a) eq lc($name_b);
319
+ # }
320
+ # }
321
+ # }
322
+ # # If any of the cases failed we throw the error here.
323
+ end
324
+ if error != 0
325
+ raise "Worksheet name '#{name}', with case ignored, " +
326
+ "is already in use"
327
+ end
328
+ end
329
+ return [name, encoding]
330
+ end
331
+
332
+ ###############################################################################
333
+ #
334
+ # add_format(%properties)
335
+ #
336
+ # Add a new format to the Excel workbook. This adds an XF record and
337
+ # a FONT record. Also, pass any properties to the Format::new().
338
+ #
339
+ def add_format(formats = {})
340
+ format = Format.new(@xf_index, @default_formats.merge(formats))
341
+ @xf_index += 1
342
+ @formats.push format # Store format reference
343
+ return format
344
+ end
345
+
346
+ ###############################################################################
347
+ #
348
+ # compatibility_mode()
349
+ #
350
+ # Set the compatibility mode.
351
+ #
352
+ # Excel doesn't require every possible Biff record to be present in a file.
353
+ # In particular if the indexing records INDEX, ROW and DBCELL aren't present
354
+ # it just ignores the fact and reads the cells anyway. This is also true of
355
+ # the EXTSST record. Gnumeric and OOo also take this approach. This allows
356
+ # WriteExcel to ignore these records in order to minimise the amount of data
357
+ # stored in memory. However, other third party applications that read Excel
358
+ # files often expect these records to be present. In "compatibility mode"
359
+ # WriteExcel writes these records and tries to be as close to an Excel
360
+ # generated file as possible.
361
+ #
362
+ # This requires additional data to be stored in memory until the file is
363
+ # about to be written. This incurs a memory and speed penalty and may not be
364
+ # suitable for very large files.
365
+ #
366
+ def compatibility_mode(mode = 1)
367
+ unless sheets.empty?
368
+ raise "compatibility_mode() must be called before add_worksheet()"
369
+ end
370
+ @compatibility = mode
371
+ end
372
+
373
+ ###############################################################################
374
+ #
375
+ # set_1904()
376
+ #
377
+ # Set the date system: false = 1900 (the default), true = 1904
378
+ #
379
+ def set_1904(mode = true)
380
+ unless sheets.empty?
381
+ raise "set_1904() must be called before add_worksheet()"
382
+ end
383
+ @date_1904 = mode
384
+ end
385
+
386
+ ###############################################################################
387
+ #
388
+ # set_custom_color()
389
+ #
390
+ # Change the RGB components of the elements in the colour palette.
391
+ #
392
+ def set_custom_color(index = nil, red = nil, green = nil, blue = nil)
393
+ # Match a HTML #xxyyzz style parameter
394
+ if !red.nil? && red =~ /^#(\w\w)(\w\w)(\w\w)/
395
+ red = $1.hex
396
+ green = $2.hex
397
+ blue = $3.hex
398
+ end
399
+
400
+ # Check that the colour index is the right range
401
+ if index < 8 || index > 64
402
+ raise "Color index #{index} outside range: 8 <= index <= 64";
403
+ end
404
+
405
+ # Check that the colour components are in the right range
406
+ if (red < 0 || red > 255) ||
407
+ (green < 0 || green > 255) ||
408
+ (blue < 0 || blue > 255)
409
+ raise "Color component outside range: 0 <= color <= 255";
410
+ end
411
+
412
+ index -=8 # Adjust colour index (wingless dragonfly)
413
+
414
+ # Set the RGB value
415
+ @palette[index] = [red, green, blue, 0]
416
+
417
+ return index +8
418
+ end
419
+
420
+ ###############################################################################
421
+ #
422
+ # set_palette_xl97()
423
+ #
424
+ # Sets the colour palette to the Excel 97+ default.
425
+ #
426
+ def set_palette_xl97
427
+ @palette = [
428
+ [0x00, 0x00, 0x00, 0x00], # 8
429
+ [0xff, 0xff, 0xff, 0x00], # 9
430
+ [0xff, 0x00, 0x00, 0x00], # 10
431
+ [0x00, 0xff, 0x00, 0x00], # 11
432
+ [0x00, 0x00, 0xff, 0x00], # 12
433
+ [0xff, 0xff, 0x00, 0x00], # 13
434
+ [0xff, 0x00, 0xff, 0x00], # 14
435
+ [0x00, 0xff, 0xff, 0x00], # 15
436
+ [0x80, 0x00, 0x00, 0x00], # 16
437
+ [0x00, 0x80, 0x00, 0x00], # 17
438
+ [0x00, 0x00, 0x80, 0x00], # 18
439
+ [0x80, 0x80, 0x00, 0x00], # 19
440
+ [0x80, 0x00, 0x80, 0x00], # 20
441
+ [0x00, 0x80, 0x80, 0x00], # 21
442
+ [0xc0, 0xc0, 0xc0, 0x00], # 22
443
+ [0x80, 0x80, 0x80, 0x00], # 23
444
+ [0x99, 0x99, 0xff, 0x00], # 24
445
+ [0x99, 0x33, 0x66, 0x00], # 25
446
+ [0xff, 0xff, 0xcc, 0x00], # 26
447
+ [0xcc, 0xff, 0xff, 0x00], # 27
448
+ [0x66, 0x00, 0x66, 0x00], # 28
449
+ [0xff, 0x80, 0x80, 0x00], # 29
450
+ [0x00, 0x66, 0xcc, 0x00], # 30
451
+ [0xcc, 0xcc, 0xff, 0x00], # 31
452
+ [0x00, 0x00, 0x80, 0x00], # 32
453
+ [0xff, 0x00, 0xff, 0x00], # 33
454
+ [0xff, 0xff, 0x00, 0x00], # 34
455
+ [0x00, 0xff, 0xff, 0x00], # 35
456
+ [0x80, 0x00, 0x80, 0x00], # 36
457
+ [0x80, 0x00, 0x00, 0x00], # 37
458
+ [0x00, 0x80, 0x80, 0x00], # 38
459
+ [0x00, 0x00, 0xff, 0x00], # 39
460
+ [0x00, 0xcc, 0xff, 0x00], # 40
461
+ [0xcc, 0xff, 0xff, 0x00], # 41
462
+ [0xcc, 0xff, 0xcc, 0x00], # 42
463
+ [0xff, 0xff, 0x99, 0x00], # 43
464
+ [0x99, 0xcc, 0xff, 0x00], # 44
465
+ [0xff, 0x99, 0xcc, 0x00], # 45
466
+ [0xcc, 0x99, 0xff, 0x00], # 46
467
+ [0xff, 0xcc, 0x99, 0x00], # 47
468
+ [0x33, 0x66, 0xff, 0x00], # 48
469
+ [0x33, 0xcc, 0xcc, 0x00], # 49
470
+ [0x99, 0xcc, 0x00, 0x00], # 50
471
+ [0xff, 0xcc, 0x00, 0x00], # 51
472
+ [0xff, 0x99, 0x00, 0x00], # 52
473
+ [0xff, 0x66, 0x00, 0x00], # 53
474
+ [0x66, 0x66, 0x99, 0x00], # 54
475
+ [0x96, 0x96, 0x96, 0x00], # 55
476
+ [0x00, 0x33, 0x66, 0x00], # 56
477
+ [0x33, 0x99, 0x66, 0x00], # 57
478
+ [0x00, 0x33, 0x00, 0x00], # 58
479
+ [0x33, 0x33, 0x00, 0x00], # 59
480
+ [0x99, 0x33, 0x00, 0x00], # 60
481
+ [0x99, 0x33, 0x66, 0x00], # 61
482
+ [0x33, 0x33, 0x99, 0x00], # 62
483
+ [0x33, 0x33, 0x33, 0x00] # 63
484
+ ]
485
+ return 0
486
+ end
487
+
488
+ ###############################################################################
489
+ #
490
+ # set_tempdir()
491
+ #
492
+ # Change the default temp directory used by _initialize() in Worksheet.pm.
493
+ #
494
+ def set_tempdir(dir = '')
495
+ raise "#{dir} is not a valid directory" if dir != '' && !FileTest.directory?(dir)
496
+ raise "set_tempdir must be called before add_worksheet" unless sheets.empty?
497
+
498
+ @tempdir = dir
499
+ end
500
+
501
+ ###############################################################################
502
+ #
503
+ # set_codepage()
504
+ #
505
+ # See also the _store_codepage method. This is used to store the code page, i.e.
506
+ # the character set used in the workbook.
507
+ #
508
+ def set_codepage(type = 1)
509
+ if type == 2
510
+ @codepage = 0x8000
511
+ else
512
+ @codepage = 0x04E4
513
+ end
514
+ end
515
+
516
+ ###############################################################################
517
+ #
518
+ # set_properties()
519
+ #
520
+ # Set the document properties such as Title, Author etc. These are written to
521
+ # property sets in the OLE container.
522
+ #
523
+ def set_properties(params)
524
+ # Ignore if no args were passed.
525
+ return -1 if !params.kind_of?(Hash) || params.empty?
526
+
527
+ # List of valid input parameters.
528
+ properties = {
529
+ :codepage => [0x0001, 'VT_I2' ],
530
+ :title => [0x0002, 'VT_LPSTR' ],
531
+ :subject => [0x0003, 'VT_LPSTR' ],
532
+ :author => [0x0004, 'VT_LPSTR' ],
533
+ :keywords => [0x0005, 'VT_LPSTR' ],
534
+ :comments => [0x0006, 'VT_LPSTR' ],
535
+ :last_author => [0x0008, 'VT_LPSTR' ],
536
+ :created => [0x000C, 'VT_FILETIME'],
537
+ :category => [0x0002, 'VT_LPSTR' ],
538
+ :manager => [0x000E, 'VT_LPSTR' ],
539
+ :company => [0x000F, 'VT_LPSTR' ],
540
+ :utf8 => 1
541
+ }
542
+
543
+ # Check for valid input parameters.
544
+ params.each_key do |k|
545
+ unless properties.has_key?(k)
546
+ raise "Unknown parameter '#{k}' in set_properties()";
547
+ end
548
+ end
549
+
550
+ # Set the creation time unless specified by the user.
551
+ unless params.has_key?(:created)
552
+ params[:created] = @localtime
553
+ end
554
+
555
+ #
556
+ # Create the SummaryInformation property set.
557
+ #
558
+
559
+ # Get the codepage of the strings in the property set.
560
+ strings = ["title", "subject", "author", "keywords", "comments", "last_author"]
561
+ params[:codepage] = get_property_set_codepage(params, strings)
562
+
563
+ # Create an array of property set values.
564
+ property_sets = []
565
+ strings.unshift("codepage")
566
+ strings.push("created")
567
+ strings.each do |property|
568
+ if params.has_key?(property.to_sym) && !params[property.to_sym].nil?
569
+ property_sets.push(
570
+ [ properties[property.to_sym][0],
571
+ properties[property.to_sym][1],
572
+ params[property.to_sym] ]
573
+ )
574
+ end
575
+ end
576
+
577
+ # Pack the property sets.
578
+ @summary = create_summary_property_set(property_sets)
579
+
580
+ #
581
+ # Create the DocSummaryInformation property set.
582
+ #
583
+
584
+ # Get the codepage of the strings in the property set.
585
+ strings = ["category", "manager", "company"]
586
+ params[:codepage] = get_property_set_codepage(params, strings)
587
+
588
+ # Create an array of property set values.
589
+ property_sets = []
590
+
591
+ ["codepage", "category", "manager", "company"].each do |property|
592
+ if params.has_key?(property.to_sym) && !params[property.to_sym].nil?
593
+ property_sets.push(
594
+ [ properties[property.to_sym][0],
595
+ properties[property.to_sym][1],
596
+ params[property.to_sym] ]
597
+ )
598
+ end
599
+ end
600
+
601
+ # Pack the property sets.
602
+ @doc_summary = create_doc_summary_property_set(property_sets)
603
+
604
+ # Set a flag for when the files is written.
605
+ @add_doc_properties = 1
606
+ end
607
+
608
+ ###############################################################################
609
+ #
610
+ # _get_property_set_codepage()
611
+ #
612
+ # Get the character codepage used by the strings in a property set. If one of
613
+ # the strings used is utf8 then the codepage is marked as utf8. Otherwise
614
+ # Latin 1 is used (although in our case this is limited to 7bit ASCII).
615
+ #
616
+ def get_property_set_codepage(params, strings)
617
+ # Allow for manually marked utf8 strings.
618
+ unless params[:utf8].nil?
619
+ return 0xFDE9
620
+ else
621
+ strings.each do |string|
622
+ next unless params.has_key?(string.to_sym)
623
+ return 0xFDE9 if params[string.to_sym] =~ NonAscii
624
+ end
625
+ return 0x04E4; # Default codepage, Latin 1.
626
+ end
627
+ end
628
+
629
+ ###############################################################################
630
+ #
631
+ # _store_workbook()
632
+ #
633
+ # Assemble worksheets into a workbook and send the BIFF data to an OLE
634
+ # storage.
635
+ #
636
+ def store_workbook
637
+ # Add a default worksheet if non have been added.
638
+ add_worksheet if @worksheets.empty?
639
+
640
+ # Calculate size required for MSO records and update worksheets.
641
+ calc_mso_sizes
642
+
643
+ # Ensure that at least one worksheet has been selected.
644
+ @worksheets[0].select if @activesheet == 0
645
+
646
+ # Calculate the number of selected worksheet tabs and call the finalization
647
+ # methods for each worksheet
648
+ @worksheets.each do |sheet|
649
+ @selected += 1 if sheet.selected != 0
650
+ sheet.active = 1 if sheet.index == @activesheet
651
+ end
652
+
653
+ # Add Workbook globals
654
+ store_bof(0x0005)
655
+ store_codepage
656
+ store_window1
657
+ store_hideobj
658
+ store_1904
659
+ store_all_fonts
660
+ store_all_num_formats
661
+ store_all_xfs
662
+ store_all_styles
663
+ store_palette
664
+
665
+ # Calculate the offsets required by the BOUNDSHEET records
666
+ calc_sheet_offsets
667
+
668
+ # Add BOUNDSHEET records.
669
+ @worksheets.each do |sheet|
670
+ store_boundsheet(
671
+ sheet.name,
672
+ sheet.offset,
673
+ sheet.type,
674
+ sheet.hidden,
675
+ sheet.encoding
676
+ )
677
+ end
678
+
679
+ # NOTE: If any records are added between here and EOF the
680
+ # _calc_sheet_offsets() should be updated to include the new length.
681
+ store_country
682
+ if @ext_ref_count != 0
683
+ store_supbook
684
+ store_externsheet
685
+ store_names
686
+ end
687
+ add_mso_drawing_group
688
+ store_shared_strings
689
+ store_extsst
690
+
691
+ # End Workbook globals
692
+ store_eof
693
+
694
+ # Store the workbook in an OLE container
695
+ return store_OLE_file
696
+ end
697
+
698
+ ###############################################################################
699
+ #
700
+ # _store_OLE_file()
701
+ #
702
+ # Store the workbook in an OLE container using the default handler or using
703
+ # OLE::Storage_Lite if the workbook data is > ~ 7MB.
704
+ #
705
+ def store_OLE_file
706
+ maxsize = 7_087_104
707
+ # maxsize = 1
708
+
709
+ if @add_doc_properties == 0 && @biffsize <= maxsize
710
+ # Write the OLE file using OLEwriter if data <= 7MB
711
+ ole = OLEWriter.new(@fh_out)
712
+
713
+ # Write the BIFF data without the OLE container for testing.
714
+ ole.biff_only = @biff_only
715
+
716
+ # Indicate that we created the filehandle and want to close it.
717
+ ole.internal_fh = @internal_fh
718
+
719
+ ole.set_size(@biffsize)
720
+ ole.write_header
721
+
722
+ while tmp = get_data
723
+ ole.write(tmp)
724
+ end
725
+
726
+ @worksheets.each do |worksheet|
727
+ while tmp = worksheet.get_data
728
+ ole.write(tmp)
729
+ end
730
+ end
731
+
732
+ return ole.close
733
+ else
734
+ # Write the OLE file using ruby-ole if data > 7MB
735
+
736
+ # Create the Workbook stream.
737
+ stream = 'Workbook'.unpack('C*').pack('v*')
738
+ workbook = OLEStorageLitePPSFile.new(stream)
739
+ workbook.set_file # use tempfile
740
+
741
+ while tmp = get_data
742
+ workbook.append(tmp)
743
+ end
744
+
745
+ @worksheets.each do |worksheet|
746
+ while tmp = worksheet.get_data
747
+ workbook.append(tmp)
748
+ end
749
+ end
750
+
751
+ streams = []
752
+ streams << workbook
753
+
754
+ # Create the properties streams, if any.
755
+ if @add_doc_properties != 0
756
+ stream = "\5SummaryInformation".unpack('C*').pack('v*')
757
+ summary = OLEStorageLitePPSFile.new(stream, @summary)
758
+ streams << summary
759
+
760
+ stream = "\5DocumentSummaryInformation".unpack('C*').pack('v*')
761
+ summary = OLEStorageLitePPSFile.new(stream, @doc_summary)
762
+ streams << summary
763
+ end
764
+
765
+ # Create the OLE root document and add the substreams.
766
+ localtime = @localtime.to_a[0..5]
767
+ localtime[4] -= 1 # month
768
+ localtime[5] -= 1900
769
+
770
+ ole_root = OLEStorageLitePPSRoot.new(
771
+ localtime,
772
+ localtime,
773
+ streams
774
+ )
775
+ ole_root.save(@filename)
776
+
777
+ # Close the filehandle if it was created internally.
778
+ return @fh_out.close if @internal_fh != 0
779
+ end
780
+ end
781
+
782
+ ###############################################################################
783
+ #
784
+ # _calc_sheet_offsets()
785
+ #
786
+ # Calculate Worksheet BOF offsets records for use in the BOUNDSHEET records.
787
+ #
788
+ def calc_sheet_offsets
789
+ _bof = 12
790
+ _eof = 4
791
+
792
+ offset = @datasize
793
+
794
+ # Add the length of the COUNTRY record
795
+ offset += 8
796
+
797
+ # Add the length of the SST and associated CONTINUEs
798
+ offset += calculate_shared_string_sizes
799
+
800
+ # Add the length of the EXTSST record.
801
+ offset += calculate_extsst_size
802
+
803
+ # Add the length of the SUPBOOK, EXTERNSHEET and NAME records
804
+ offset += calculate_extern_sizes
805
+
806
+ # Add the length of the MSODRAWINGGROUP records including an extra 4 bytes
807
+ # for any CONTINUE headers. See _add_mso_drawing_group_continue().
808
+ mso_size = @mso_size
809
+ mso_size += 4 * Integer((mso_size -1) / Float(@limit))
810
+ offset += mso_size
811
+
812
+ @worksheets.each do |sheet|
813
+ offset += _bof + sheet.name.length
814
+ end
815
+
816
+ offset += _eof
817
+ @worksheets.each do |sheet|
818
+ sheet.offset = offset
819
+ sheet.close(*@sheetnames)
820
+ offset += sheet.datasize
821
+ end
822
+
823
+ @biffsize = offset
824
+ end
825
+
826
+ ###############################################################################
827
+ #
828
+ # _calc_mso_sizes()
829
+ #
830
+ # Calculate the MSODRAWINGGROUP sizes and the indexes of the Worksheet
831
+ # MSODRAWING records.
832
+ #
833
+ # In the following SPID is shape id, according to Escher nomenclature.
834
+ #
835
+ def calc_mso_sizes
836
+ mso_size = 0 # Size of the MSODRAWINGGROUP record
837
+ start_spid = 1024 # Initial spid for each sheet
838
+ max_spid = 1024 # spidMax
839
+ num_clusters = 1 # cidcl
840
+ shapes_saved = 0 # cspSaved
841
+ drawings_saved = 0 # cdgSaved
842
+ clusters = []
843
+
844
+ process_images
845
+
846
+ # Add Bstore container size if there are images.
847
+ mso_size += 8 unless @images_data.empty?
848
+
849
+ # Iterate through the worksheets, calculate the MSODRAWINGGROUP parameters
850
+ # and space required to store the record and the MSODRAWING parameters
851
+ # required by each worksheet.
852
+ #
853
+ @worksheets.each do |sheet|
854
+ num_images = sheet.num_images
855
+ image_mso_size = sheet.image_mso_size
856
+ num_comments = sheet.prepare_comments
857
+ num_charts = sheet.prepare_charts
858
+ num_filters = sheet.filter_count
859
+
860
+ next if num_images + num_comments + num_charts + num_filters == 0
861
+
862
+ # Include 1 parent MSODRAWING shape, per sheet, in the shape count.
863
+ num_shapes = 1 + num_images + num_comments +
864
+ num_charts + num_filters
865
+ shapes_saved += num_shapes
866
+ mso_size += image_mso_size
867
+
868
+ # Add a drawing object for each sheet with comments.
869
+ drawings_saved += 1
870
+
871
+ # For each sheet start the spids at the next 1024 interval.
872
+ max_spid = 1024 * (1 + Integer((max_spid -1)/1024.0))
873
+ start_spid = max_spid
874
+
875
+ # Max spid for each sheet and eventually for the workbook.
876
+ max_spid += num_shapes
877
+
878
+ # Store the cluster ids
879
+ i = num_shapes
880
+ while i > 0
881
+ num_clusters += 1
882
+ mso_size += 8
883
+ size = i > 1024 ? 1024 : i
884
+
885
+ clusters.push([drawings_saved, size])
886
+ i -= 1024
887
+ end
888
+
889
+ # Pass calculated values back to the worksheet
890
+ sheet.object_ids = [start_spid, drawings_saved,
891
+ num_shapes, max_spid -1]
892
+ end
893
+
894
+
895
+ # Calculate the MSODRAWINGGROUP size if we have stored some shapes.
896
+ mso_size += 86 if mso_size != 0 # Smallest size is 86+8=94
897
+
898
+ @mso_size = mso_size
899
+ @mso_clusters = [
900
+ max_spid, num_clusters, shapes_saved,
901
+ drawings_saved, clusters
902
+ ]
903
+ end
904
+
905
+ ###############################################################################
906
+ #
907
+ # _process_images()
908
+ #
909
+ # We need to process each image in each worksheet and extract information.
910
+ # Some of this information is stored and used in the Workbook and some is
911
+ # passed back into each Worksheet. The overall size for the image related
912
+ # BIFF structures in the Workbook is calculated here.
913
+ #
914
+ # MSO size = 8 bytes for bstore_container +
915
+ # 44 bytes for blip_store_entry +
916
+ # 25 bytes for blip
917
+ # = 77 + image size.
918
+ #
919
+ def process_images
920
+ images_seen = {}
921
+ image_data = []
922
+ previous_images = []
923
+ image_id = 1;
924
+ images_size = 0;
925
+
926
+ @worksheets.each do |sheet|
927
+ next if sheet.prepare_images == 0
928
+
929
+ num_images = 0
930
+ image_mso_size = 0
931
+
932
+ sheet.images_array.each do |image|
933
+ filename = image[2]
934
+ num_images += 1
935
+
936
+ #
937
+ # For each Worksheet image we get a structure like this
938
+ # [
939
+ # $row,
940
+ # $col,
941
+ # $name,
942
+ # $x_offset,
943
+ # $y_offset,
944
+ # $scale_x,
945
+ # $scale_y,
946
+ # ]
947
+ #
948
+ # And we add additional information:
949
+ #
950
+ # $image_id,
951
+ # $type,
952
+ # $width,
953
+ # $height;
954
+
955
+ if images_seen[filename].nil?
956
+ # TODO should also match seen images based on checksum.
957
+
958
+ # Open the image file and import the data.
959
+ fh = open(filename, "rb")
960
+ raise "Couldn't import #{filename}: #{$!}" unless fh
961
+
962
+ # Slurp the file into a string and do some size calcs.
963
+ # my $data = do {local $/; <$fh>};
964
+ data = fh.read
965
+ size = data.length
966
+ checksum1 = image_checksum(data, image_id)
967
+ checksum2 = checksum1
968
+ ref_count = 1
969
+
970
+ # Process the image and extract dimensions.
971
+ # Test for PNGs...
972
+ if data.unpack('x A3')[0] == 'PNG'
973
+ type, width, height = process_png(data)
974
+ # Test for JFIF and Exif JPEGs...
975
+ elsif ( data.unpack('n')[0] == 0xFFD8 &&
976
+ (data.unpack('x6 A4')[0] == 'JFIF' ||
977
+ data.unpack('x6 A4')[0] == 'Exif')
978
+ )
979
+ type, width, height = process_jpg(data, filename)
980
+ # Test for BMPs...
981
+ elsif data.unpack('A2')[0] == 'BM'
982
+ type, width, height = process_bmp(data, filename)
983
+ # The 14 byte header of the BMP is stripped off.
984
+ data[0, 13] = ''
985
+
986
+ # A checksum of the new image data is also required.
987
+ checksum2 = image_checksum(data, image_id, image_id)
988
+
989
+ # Adjust size -14 (header) + 16 (extra checksum).
990
+ size += 2
991
+ else
992
+ raise "Unsupported image format for file: #{filename}\n"
993
+ end
994
+
995
+ # Push the new data back into the Worksheet array;
996
+ image.push(image_id, type, width, height)
997
+
998
+ # Also store new data for use in duplicate images.
999
+ previous_images.push([image_id, type, width, height])
1000
+
1001
+ # Store information required by the Workbook.
1002
+ image_data.push([ref_count, type, data, size,
1003
+ checksum1, checksum2])
1004
+
1005
+ # Keep track of overall data size.
1006
+ images_size += size +61; # Size for bstore container.
1007
+ image_mso_size += size +69; # Size for dgg container.
1008
+
1009
+ images_seen[filename] = image_id
1010
+ image_id += 1
1011
+ fh.close
1012
+ else
1013
+ # We've processed this file already.
1014
+ index = images_seen[filename] -1
1015
+
1016
+ # Increase image reference count.
1017
+ image_data[index][0] += 1
1018
+
1019
+ # Add previously calculated data back onto the Worksheet array.
1020
+ # $image_id, $type, $width, $height
1021
+ a_ref = sheet.images_array[index]
1022
+ image.concat(previous_images[index])
1023
+ end
1024
+ end
1025
+
1026
+ # Store information required by the Worksheet.
1027
+ sheet.num_images = num_images
1028
+ sheet.image_mso_size = image_mso_size
1029
+
1030
+ end
1031
+
1032
+
1033
+ # Store information required by the Workbook.
1034
+ @images_size = images_size
1035
+ @images_data = image_data # Store the data for MSODRAWINGGROUP.
1036
+
1037
+ end
1038
+
1039
+ ###############################################################################
1040
+ #
1041
+ # _image_checksum()
1042
+ #
1043
+ # Generate a checksum for the image using whichever module is available..The
1044
+ # available modules are checked in _get_checksum_method(). Excel uses an MD4
1045
+ # checksum but any other will do. In the event of no checksum module being
1046
+ # available we simulate a checksum using the image index.
1047
+ #
1048
+ def image_checksum(data, index1, index2 = 0)
1049
+ if @checksum_method == 1
1050
+ # Digest::MD4
1051
+ # return Digest::MD4::md4_hex($data);
1052
+ elsif @checksum_method == 2
1053
+ # Digest::Perl::MD4
1054
+ # return Digest::Perl::MD4::md4_hex($data);
1055
+ elsif @checksum_method == 3
1056
+ # Digest::MD5
1057
+ return Digest::MD5.hexdigest(data)
1058
+ else
1059
+ # Default
1060
+ return sprintf('%016X%016X', index2, index1)
1061
+ end
1062
+ end
1063
+
1064
+ ###############################################################################
1065
+ #
1066
+ # _process_png()
1067
+ #
1068
+ # Extract width and height information from a PNG file.
1069
+ #
1070
+ def process_png(data)
1071
+ type = 6 # Excel Blip type (MSOBLIPTYPE).
1072
+ width = data[16, 4].unpack("N")[0]
1073
+ height = data[20, 4].unpack("N")[0]
1074
+
1075
+ return [type, width, height]
1076
+ end
1077
+
1078
+ ###############################################################################
1079
+ #
1080
+ # _process_bmp()
1081
+ #
1082
+ # Extract width and height information from a BMP file.
1083
+ #
1084
+ # Most of these checks came from the old Worksheet::_process_bitmap() method.
1085
+ #
1086
+ def process_bmp(data, filename)
1087
+ type = 7 # Excel Blip type (MSOBLIPTYPE).
1088
+
1089
+ # Check that the file is big enough to be a bitmap.
1090
+ if data.length <= 0x36
1091
+ raise "#{filename} doesn't contain enough data."
1092
+ end
1093
+
1094
+ # Read the bitmap width and height. Verify the sizes.
1095
+ width, height = data.unpack("x18 V2")
1096
+
1097
+ if width > 0xFFFF
1098
+ raise "#{filename}: largest image width #{width} supported is 65k."
1099
+ end
1100
+
1101
+ if height > 0xFFFF
1102
+ raise "#{filename}: largest image height supported is 65k."
1103
+ end
1104
+
1105
+ # Read the bitmap planes and bpp data. Verify them.
1106
+ planes, bitcount = data.unpack("x26 v2")
1107
+
1108
+ if bitcount != 24
1109
+ raise "#{filename} isn't a 24bit true color bitmap."
1110
+ end
1111
+
1112
+ if planes != 1
1113
+ raise "#{filename}: only 1 plane supported in bitmap image."
1114
+ end
1115
+
1116
+ # Read the bitmap compression. Verify compression.
1117
+ compression = data.unpack("x30 V")
1118
+
1119
+ if compression != 0
1120
+ raise "#{filename}: compression not supported in bitmap image."
1121
+ end
1122
+
1123
+ return [type, width, height]
1124
+ end
1125
+
1126
+ ###############################################################################
1127
+ #
1128
+ # _process_jpg()
1129
+ #
1130
+ # Extract width and height information from a JPEG file.
1131
+ #
1132
+ def process_jpg(data, filename)
1133
+ type = 5 # Excel Blip type (MSOBLIPTYPE).
1134
+
1135
+ offset = 2;
1136
+ data_length = data.length
1137
+
1138
+ # Search through the image data to find the 0xFFC0 marker. The height and
1139
+ # width are contained in the data for that sub element.
1140
+ while offset < data_length
1141
+ marker = data[offset, 2].unpack("n")
1142
+ marker = marker[0]
1143
+ length = data[offset+2, 2].unpack("n")
1144
+ length = length[0]
1145
+
1146
+ if marker == 0xFFC0
1147
+ height = data[offset+5, 2].unpack("n")
1148
+ height = height[0]
1149
+ width = data[offset+7, 2].unpack("n")
1150
+ width = width[0]
1151
+ break
1152
+ end
1153
+
1154
+ offset = offset + length + 2
1155
+ break if marker == 0xFFDA
1156
+ end
1157
+
1158
+ if height.nil?
1159
+ raise "#{filename}: no size data found in image.\n"
1160
+ end
1161
+
1162
+ return [type, width, height]
1163
+ end
1164
+
1165
+ ###############################################################################
1166
+ #
1167
+ # _store_all_fonts()
1168
+ #
1169
+ # Store the Excel FONT records.
1170
+ #
1171
+ def store_all_fonts
1172
+ format = @formats[15] # The default cell format.
1173
+ font = format.get_font
1174
+
1175
+ # Fonts are 0-indexed. According to the SDK there is no index 4,
1176
+ (0..3).each do
1177
+ append(font)
1178
+ end
1179
+
1180
+ # Add the font for comments. This isn't connected to any XF format.
1181
+ tmp = Format.new(nil, :font => 'Tahoma', :size => 8)
1182
+ font = tmp.get_font
1183
+ append(font)
1184
+
1185
+ # Iterate through the XF objects and write a FONT record if it isn't the
1186
+ # same as the default FONT and if it hasn't already been used.
1187
+ #
1188
+ fonts = {}
1189
+ index = 6 # The first user defined FONT
1190
+
1191
+ key = format.get_font_key # The default font for cell formats.
1192
+ fonts[key] = 0 # Index of the default font
1193
+
1194
+ # Fonts that are marked as '_font_only' are always stored. These are used
1195
+ # mainly for charts and may not have an associated XF record.
1196
+
1197
+ @formats.each do |format|
1198
+ key = format.get_font_key
1199
+ if format.font_only == 0 and !fonts[key].nil?
1200
+ # FONT has already been used
1201
+ format.font_index = fonts[key]
1202
+ else
1203
+ # Add a new FONT record
1204
+
1205
+ if format.font_only == 0
1206
+ fonts[key] = index
1207
+ end
1208
+
1209
+ format.font_index = index
1210
+ index += 1
1211
+ font = format.get_font
1212
+ append(font)
1213
+ end
1214
+ end
1215
+ end
1216
+
1217
+ ###############################################################################
1218
+ #
1219
+ # _store_all_num_formats()
1220
+ #
1221
+ # Store user defined numerical formats i.e. FORMAT records
1222
+ #
1223
+ def store_all_num_formats
1224
+ num_formats = {}
1225
+ index = 164 # User defined FORMAT records start from 0xA4
1226
+
1227
+ # Iterate through the XF objects and write a FORMAT record if it isn't a
1228
+ # built-in format type and if the FORMAT string hasn't already been used.
1229
+ #
1230
+ @formats.each do |format|
1231
+ num_format = format.num_format
1232
+ encoding = format.num_format_enc
1233
+
1234
+ # Check if $num_format is an index to a built-in format.
1235
+ # Also check for a string of zeros, which is a valid format string
1236
+ # but would evaluate to zero.
1237
+ #
1238
+ unless num_format.to_s =~ /^0+\d/
1239
+ next if num_format.to_s =~ /^\d+$/ # built-in
1240
+ end
1241
+
1242
+ if num_formats[num_format]
1243
+ # FORMAT has already been used
1244
+ format.num_format = num_formats[num_format]
1245
+ else
1246
+ # Add a new FORMAT
1247
+ num_formats[num_format] = index
1248
+ format.num_format = index
1249
+ store_num_format(num_format, index, encoding)
1250
+ index += 1
1251
+ end
1252
+ end
1253
+ end
1254
+
1255
+ ###############################################################################
1256
+ #
1257
+ # _store_all_xfs()
1258
+ #
1259
+ # Write all XF records.
1260
+ #
1261
+ def store_all_xfs
1262
+ @formats.each do |format|
1263
+ xf = format.get_xf
1264
+ append(xf)
1265
+ end
1266
+ end
1267
+
1268
+ ###############################################################################
1269
+ #
1270
+ # _store_all_styles()
1271
+ #
1272
+ # Write all STYLE records.
1273
+ #
1274
+ def store_all_styles
1275
+ # Excel adds the built-in styles in alphabetical order.
1276
+ built_ins = [
1277
+ [0x03, 16], # Comma
1278
+ [0x06, 17], # Comma[0]
1279
+ [0x04, 18], # Currency
1280
+ [0x07, 19], # Currency[0]
1281
+ [0x00, 0], # Normal
1282
+ [0x05, 20] # Percent
1283
+
1284
+ # We don't deal with these styles yet.
1285
+ #[0x08, 21], # Hyperlink
1286
+ #[0x02, 8], # ColLevel_n
1287
+ #[0x01, 1], # RowLevel_n
1288
+ ]
1289
+
1290
+ built_ins.each do |aref|
1291
+ type = aref[0]
1292
+ xf_index = aref[1]
1293
+
1294
+ store_style(type, xf_index)
1295
+ end
1296
+ end
1297
+
1298
+ ###############################################################################
1299
+ #
1300
+ # _store_names()
1301
+ #
1302
+ # Write the NAME record to define the print area and the repeat rows and cols.
1303
+ #
1304
+ def store_names
1305
+ index = 0
1306
+
1307
+ # Create the print area NAME records
1308
+ @worksheets.each do |worksheet|
1309
+
1310
+ key = "#{index}:#{index}"
1311
+ ref = @ext_refs[key]
1312
+ index += 1
1313
+
1314
+ # Write a Name record if Autofilter has been defined
1315
+ if worksheet.filter_count != 0
1316
+ store_name_short(
1317
+ worksheet.index,
1318
+ 0x0D, # NAME type = Filter Database
1319
+ ref,
1320
+ worksheet.filter_area[0],
1321
+ worksheet.filter_area[1],
1322
+ worksheet.filter_area[2],
1323
+ worksheet.filter_area[3],
1324
+ 1 # Hidden
1325
+ )
1326
+ end
1327
+
1328
+ # Write a Name record if the print area has been defined
1329
+ if !worksheet.print_rowmin.nil? && worksheet.print.rowmin != 0
1330
+ store_name_short(
1331
+ worksheet.index,
1332
+ 0x06, # NAME type = Print_Area
1333
+ ref,
1334
+ worksheet.print_rowmin,
1335
+ worksheet.print_rowmax,
1336
+ worksheet.print_colmin,
1337
+ worksheet.print_colmax
1338
+ )
1339
+ end
1340
+
1341
+ end
1342
+
1343
+ index = 0
1344
+
1345
+ # Create the print title NAME records
1346
+ @worksheets.each do |worksheet|
1347
+
1348
+ rowmin = worksheet.title_rowmin
1349
+ rowmax = worksheet.title_rowmax
1350
+ colmin = worksheet.title_colmin
1351
+ colmax = worksheet.title_colmax
1352
+ key = "#{index}:#{index}"
1353
+ ref = @ext_refs[key]
1354
+ index += 1
1355
+
1356
+ # Determine if row + col, row, col or nothing has been defined
1357
+ # and write the appropriate record
1358
+ #
1359
+ if rowmin && colmin
1360
+ # Row and column titles have been defined.
1361
+ # Row title has been defined.
1362
+ store_name_long(
1363
+ worksheet.index,
1364
+ 0x07, # NAME type = Print_Titles
1365
+ ref,
1366
+ rowmin,
1367
+ rowmax,
1368
+ colmin,
1369
+ colmax
1370
+ )
1371
+ elsif rowmin
1372
+ # Row title has been defined.
1373
+ store_name_short(
1374
+ worksheet.index,
1375
+ 0x07, # NAME type = Print_Titles
1376
+ ref,
1377
+ rowmin,
1378
+ rowmax,
1379
+ 0x00,
1380
+ 0xff
1381
+ )
1382
+ elsif colmin
1383
+ # Column title has been defined.
1384
+ store_name_short(
1385
+ worksheet.index,
1386
+ 0x07, # NAME type = Print_Titles
1387
+ ref,
1388
+ 0x0000,
1389
+ 0xffff,
1390
+ colmin,
1391
+ colmax
1392
+ )
1393
+ else
1394
+ # Nothing left to do
1395
+ end
1396
+ end
1397
+ end
1398
+
1399
+ ###############################################################################
1400
+ ###############################################################################
1401
+ #
1402
+ # BIFF RECORDS
1403
+ #
1404
+
1405
+
1406
+ ###############################################################################
1407
+ #
1408
+ # _store_window1()
1409
+ #
1410
+ # Write Excel BIFF WINDOW1 record.
1411
+ #
1412
+ def store_window1
1413
+ record = 0x003D # Record identifier
1414
+ length = 0x0012 # Number of bytes to follow
1415
+
1416
+ xWn = 0x0000 # Horizontal position of window
1417
+ yWn = 0x0000 # Vertical position of window
1418
+ dxWn = 0x355C # Width of window
1419
+ dyWn = 0x30ED # Height of window
1420
+
1421
+ grbit = 0x0038 # Option flags
1422
+ ctabsel = @selected # Number of workbook tabs selected
1423
+ wTabRatio = 0x0258 # Tab to scrollbar ratio
1424
+
1425
+ itabFirst = @firstsheet # 1st displayed worksheet
1426
+ itabCur = @activesheet # Active worksheet
1427
+
1428
+ header = [record, length].pack("vv")
1429
+ data = [
1430
+ xWn, yWn, dxWn, dyWn,
1431
+ grbit,
1432
+ itabCur, itabFirst,
1433
+ ctabsel, wTabRatio
1434
+ ].pack("vvvvvvvvv")
1435
+
1436
+ append(header, data)
1437
+ end
1438
+
1439
+ ###############################################################################
1440
+ #
1441
+ # _store_boundsheet()
1442
+ # my $sheetname = $_[0]; # Worksheet name
1443
+ # my $offset = $_[1]; # Location of worksheet BOF
1444
+ # my $type = $_[2]; # Worksheet type
1445
+ # my $hidden = $_[3]; # Worksheet hidden flag
1446
+ # my $encoding = $_[4]; # Sheet name encoding
1447
+ #
1448
+ # Writes Excel BIFF BOUNDSHEET record.
1449
+ #
1450
+ def store_boundsheet(sheetname, offset, type, hidden, encoding)
1451
+ record = 0x0085 # Record identifier
1452
+ length = 0x08 + sheetname.length # Number of bytes to follow
1453
+
1454
+ cch = sheetname.length # Length of sheet name
1455
+
1456
+ grbit = type | hidden
1457
+
1458
+ # Character length is num of chars not num of bytes
1459
+ cch /= 2 if encoding != 0
1460
+
1461
+ # Change the UTF-16 name from BE to LE
1462
+ sheetname = sheetname.unpack('v*').pack('n*') if encoding != 0
1463
+
1464
+ header = [record, length].pack("vv")
1465
+ data = [offset, grbit, cch, encoding].pack("VvCC")
1466
+
1467
+ append(header, data, sheetname)
1468
+ end
1469
+
1470
+ ###############################################################################
1471
+ #
1472
+ # _store_style()
1473
+ # type = $_[0] # Built-in style
1474
+ # xf_index = $_[1] # Index to style XF
1475
+ #
1476
+ # Write Excel BIFF STYLE records.
1477
+ #
1478
+ def store_style(type, xf_index)
1479
+ record = 0x0293 # Record identifier
1480
+ length = 0x0004 # Bytes to follow
1481
+
1482
+ level = 0xff # Outline style level
1483
+
1484
+ xf_index |= 0x8000 # Add flag to indicate built-in style.
1485
+
1486
+ header = [record, length].pack("vv")
1487
+ data = [xf_index, type, level].pack("vCC")
1488
+
1489
+ append(header, data)
1490
+ end
1491
+
1492
+ ###############################################################################
1493
+ #
1494
+ # _store_num_format()
1495
+ # my $format = $_[0]; # Custom format string
1496
+ # my $ifmt = $_[1]; # Format index code
1497
+ # my $encoding = $_[2]; # Char encoding for format string
1498
+ #
1499
+ # Writes Excel FORMAT record for non "built-in" numerical formats.
1500
+ #
1501
+ def store_num_format(format, ifmt, encoding)
1502
+ format = format.to_s unless format.kind_of?(String)
1503
+ record = 0x041E # Record identifier
1504
+ # length # Number of bytes to follow
1505
+ # Char length of format string
1506
+ cch = format.length
1507
+
1508
+ # Handle utf8 strings
1509
+ if format =~ NonAscii
1510
+ format = NKF.nkf('-w16B0 -m0 -W', format)
1511
+ encoding = 1
1512
+ end
1513
+
1514
+ # Handle Unicode format strings.
1515
+ if encoding == 1
1516
+ raise "Uneven number of bytes in Unicode font name" if cch % 2 != 0
1517
+ cch /= 2 if encoding != 0
1518
+ format = format.unpack('n*').pack('v*')
1519
+ end
1520
+
1521
+ # Special case to handle Euro symbol, 0x80, in non-Unicode strings.
1522
+ if encoding == 0 and format =~ /\x80/
1523
+ format = format.unpack('C*').pack('v*')
1524
+ format.gsub!(/\x80\x00/, "\xAC\x20")
1525
+ encoding = 1
1526
+ end
1527
+
1528
+ length = 0x05 + format.length
1529
+
1530
+ header = [record, length].pack("vv")
1531
+ data = [ifmt, cch, encoding].pack("vvC")
1532
+
1533
+ append(header, data, format)
1534
+ end
1535
+
1536
+ ###############################################################################
1537
+ #
1538
+ # _store_1904()
1539
+ #
1540
+ # Write Excel 1904 record to indicate the date system in use.
1541
+ #
1542
+ def store_1904
1543
+ record = 0x0022 # Record identifier
1544
+ length = 0x0002 # Bytes to follow
1545
+
1546
+ f1904 = @date_1904 ? 1 : 0 # Flag for 1904 date system
1547
+
1548
+ header = [record, length].pack("vv")
1549
+ data = [f1904].pack("v")
1550
+
1551
+ append(header, data)
1552
+ end
1553
+
1554
+ ###############################################################################
1555
+ #
1556
+ # _store_supbook()
1557
+ #
1558
+ # Write BIFF record SUPBOOK to indicate that the workbook contains external
1559
+ # references, in our case, formula, print area and print title refs.
1560
+ #
1561
+ def store_supbook
1562
+ record = 0x01AE # Record identifier
1563
+ length = 0x0004 # Number of bytes to follow
1564
+
1565
+ ctabs = @worksheets.size # Number of worksheets
1566
+ stVirtPath = 0x0401 # Encoded workbook filename
1567
+
1568
+ header = [record, length].pack("vv")
1569
+ data = [ctabs, stVirtPath].pack("vv")
1570
+
1571
+ append(header, data)
1572
+ end
1573
+
1574
+ ###############################################################################
1575
+ #
1576
+ # _store_externsheet()
1577
+ #
1578
+ # Writes the Excel BIFF EXTERNSHEET record. These references are used by
1579
+ # formulas. TODO NAME record is required to define the print area and the
1580
+ # repeat rows and columns.
1581
+ #
1582
+ def store_externsheet
1583
+ record = 0x0017 # Record identifier
1584
+
1585
+ # Get the external refs
1586
+ ext_refs = @ext_refs
1587
+ ext = ext_refs.keys.sort
1588
+
1589
+ # Change the external refs from stringified "1:1" to [1, 1]
1590
+ ext.map! {|e| e.split(/:/).map! {|e| e.to_i} }
1591
+
1592
+ cxti = ext.size # Number of Excel XTI structures
1593
+ rgxti = '' # Array of XTI structures
1594
+
1595
+ # Write the XTI structs
1596
+ ext.each do |e|
1597
+ rgxti = rgxti + [0, e[0], e[1]].pack("vvv")
1598
+ end
1599
+
1600
+ data = [cxti].pack("v") + rgxti
1601
+ header = [record, data.length].pack("vv")
1602
+
1603
+ append(header, data)
1604
+ end
1605
+
1606
+ ###############################################################################
1607
+ #
1608
+ # _store_name_short()
1609
+ # index = shift # Sheet index
1610
+ # type = shift
1611
+ # ext_ref = shift # TODO
1612
+ # rowmin = $_[0] # Start row
1613
+ # rowmax = $_[1] # End row
1614
+ # colmin = $_[2] # Start column
1615
+ # colmax = $_[3] # end column
1616
+ # hidden = $_[4] # Name is hidden
1617
+ #
1618
+ #
1619
+ # Store the NAME record in the short format that is used for storing the print
1620
+ # area, repeat rows only and repeat columns only.
1621
+ #
1622
+ def store_name_short(index, type, ext_ref, rowmin, rowmax, colmin, colmax, hidden)
1623
+ record = 0x0018 # Record identifier
1624
+ length = 0x001b # Number of bytes to follow
1625
+
1626
+ grbit = 0x0020 # Option flags
1627
+ chKey = 0x00 # Keyboard shortcut
1628
+ cch = 0x01 # Length of text name
1629
+ cce = 0x000b # Length of text definition
1630
+ unknown01 = 0x0000 #
1631
+ ixals = index +1 # Sheet index
1632
+ unknown02 = 0x00 #
1633
+ cchCustMenu = 0x00 # Length of cust menu text
1634
+ cchDescription = 0x00 # Length of description text
1635
+ cchHelptopic = 0x00 # Length of help topic text
1636
+ cchStatustext = 0x00 # Length of status bar text
1637
+ rgch = type # Built-in name type
1638
+ unknown03 = 0x3b #
1639
+
1640
+ grbit = 0x0021 if hidden
1641
+
1642
+ header = [record, length].pack("vv")
1643
+ data = [grbit].pack("v")
1644
+ data = data + [chKey].pack("C")
1645
+ data = data + [cch].pack("C")
1646
+ data = data + [cce].pack("v")
1647
+ data = data + [unknown01].pack("v")
1648
+ data = data + [ixals].pack("v")
1649
+ data = data + [unknown02].pack("C")
1650
+ data = data + [cchCustMenu].pack("C")
1651
+ data = data + [cchDescription].pack("C")
1652
+ data = data + [cchHelptopic].pack("C")
1653
+ data = data + [cchStatustext].pack("C")
1654
+ data = data + [rgch].pack("C")
1655
+ data = data + [unknown03].pack("C")
1656
+ data = data + [ext_ref].pack("v")
1657
+
1658
+ data = data + [rowmin].pack("v")
1659
+ data = data + [rowmax].pack("v")
1660
+ data = data + [colmin].pack("v")
1661
+ data = data + [colmax].pack("v")
1662
+
1663
+ append(header, data)
1664
+ end
1665
+
1666
+ ###############################################################################
1667
+ #
1668
+ # _store_name_long()
1669
+ # my $index = shift; # Sheet index
1670
+ # my $type = shift;
1671
+ # my $ext_ref = shift; # TODO
1672
+ # my $rowmin = $_[0]; # Start row
1673
+ # my $rowmax = $_[1]; # End row
1674
+ # my $colmin = $_[2]; # Start column
1675
+ # my $colmax = $_[3]; # end column
1676
+ #
1677
+ #
1678
+ # Store the NAME record in the long format that is used for storing the repeat
1679
+ # rows and columns when both are specified. This share a lot of code with
1680
+ # _store_name_short() but we use a separate method to keep the code clean.
1681
+ # Code abstraction for reuse can be carried too far, and I should know. ;-)
1682
+ #
1683
+ def store_name_long(index, type, ext_ref, rowmin, rowmax, colmin, colmax)
1684
+ record = 0x0018 # Record identifier
1685
+ length = 0x002a # Number of bytes to follow
1686
+
1687
+ index = shift # Sheet index
1688
+ type = shift
1689
+ ext_ref = shift # TODO
1690
+
1691
+ grbit = 0x0020 # Option flags
1692
+ chKey = 0x00 # Keyboard shortcut
1693
+ cch = 0x01 # Length of text name
1694
+ cce = 0x001a # Length of text definition
1695
+ unknown01 = 0x0000 #
1696
+ ixals = index +1 # Sheet index
1697
+ unknown02 = 0x00 #
1698
+ cchCustMenu = 0x00 # Length of cust menu text
1699
+ cchDescription = 0x00 # Length of description text
1700
+ cchHelptopic = 0x00 # Length of help topic text
1701
+ cchStatustext = 0x00 # Length of status bar text
1702
+ rgch = type # Built-in name type
1703
+
1704
+ unknown03 = 0x29
1705
+ unknown04 = 0x0017
1706
+ unknown05 = 0x3b
1707
+
1708
+ header = [record, length].pack("vv")
1709
+ data = [grbit].pack("v")
1710
+ data = data + [chKey].pack("C")
1711
+ data = data + [cch].pack("C")
1712
+ data = data + [cce].pack("v")
1713
+ data = data + [unknown01].pack("v")
1714
+ data = data + [ixals].pack("v")
1715
+ data = data + [unknown02].pack("C")
1716
+ data = data + [cchCustMenu].pack("C")
1717
+ data = data + [cchDescription].pack("C")
1718
+ data = data + [cchHelptopic].pack("C")
1719
+ data = data + [cchStatustext].pack("C")
1720
+ data = data + [rgch].pack("C")
1721
+
1722
+ # Column definition
1723
+ data = data + [unknown03].pack("C")
1724
+ data = data + [unknown04].pack("v")
1725
+ data = data + [unknown05].pack("C")
1726
+ data = data + [ext_ref].pack("v")
1727
+ data = data + [0x0000].pack("v")
1728
+ data = data + [0xffff].pack("v")
1729
+ data = data + [colmin].pack("v")
1730
+ data = data + [colmax].pack("v")
1731
+
1732
+ # Row definition
1733
+ data = data + [unknown05].pack("C")
1734
+ data = data + [ext_ref].pack("v")
1735
+ data = data + [rowmin].pack("v")
1736
+ data = data + [rowmax].pack("v")
1737
+ data = data + [0x00].pack("v")
1738
+ data = data + [0xff].pack("v")
1739
+ # End of data
1740
+ data = data + [0x10].pack("C")
1741
+
1742
+ append(header, data)
1743
+ end
1744
+
1745
+ ###############################################################################
1746
+ #
1747
+ # _store_palette()
1748
+ #
1749
+ # Stores the PALETTE biff record.
1750
+ #
1751
+ def store_palette
1752
+ record = 0x0092 # Record identifier
1753
+ length = 2 + 4 * @palette.size # Number of bytes to follow
1754
+ ccv = @palette.size # Number of RGB values to follow
1755
+ data = '' # The RGB data
1756
+
1757
+ # Pack the RGB data
1758
+ @palette.each do |p|
1759
+ data = data + p.pack('CCCC')
1760
+ end
1761
+
1762
+ header = [record, length, ccv].pack("vvv")
1763
+
1764
+ append(header, data)
1765
+ end
1766
+
1767
+ ###############################################################################
1768
+ #
1769
+ # _store_codepage()
1770
+ #
1771
+ # Stores the CODEPAGE biff record.
1772
+ #
1773
+ def store_codepage
1774
+ record = 0x0042 # Record identifier
1775
+ length = 0x0002 # Number of bytes to follow
1776
+ cv = @codepage # The code page
1777
+
1778
+ header = [record, length].pack("vv")
1779
+ data = [cv].pack("v")
1780
+
1781
+ append(header, data)
1782
+ end
1783
+
1784
+ ###############################################################################
1785
+ #
1786
+ # _store_country()
1787
+ #
1788
+ # Stores the COUNTRY biff record.
1789
+ #
1790
+ # Will add setter method for the country codes when/if required.
1791
+ #
1792
+ def store_country
1793
+ record = 0x008C # Record identifier
1794
+ length = 0x0004 # Number of bytes to follow
1795
+ country_default = 1
1796
+ country_win_ini = 1
1797
+
1798
+ header = [record, length].pack("vv")
1799
+ data = [country_default, country_win_ini].pack("vv")
1800
+ append(header, data)
1801
+ end
1802
+
1803
+ ###############################################################################
1804
+ #
1805
+ # _store_hideobj()
1806
+ #
1807
+ # Stores the HIDEOBJ biff record.
1808
+ #
1809
+ def store_hideobj
1810
+ record = 0x008D # Record identifier
1811
+ length = 0x0002 # Number of bytes to follow
1812
+ hide = @hideobj # Option to hide objects
1813
+
1814
+ header = [record, length].pack("vv")
1815
+ data = [hide].pack("v")
1816
+
1817
+ append(header, data)
1818
+ end
1819
+
1820
+ ###############################################################################
1821
+ ###############################################################################
1822
+ ###############################################################################
1823
+
1824
+
1825
+
1826
+ ###############################################################################
1827
+ #
1828
+ # _calculate_extern_sizes()
1829
+ #
1830
+ # We need to calculate the space required by the SUPBOOK, EXTERNSHEET and NAME
1831
+ # records so that it can be added to the BOUNDSHEET offsets.
1832
+ #
1833
+ def calculate_extern_sizes
1834
+ ext_refs = @parser.get_ext_sheets
1835
+ ext_ref_count = ext_refs.keys.size
1836
+ length = 0
1837
+ index = 0
1838
+
1839
+ @worksheets.each do |worksheet|
1840
+
1841
+ rowmin = worksheet.title_rowmin
1842
+ colmin = worksheet.title_colmin
1843
+ filter = worksheet.filter_count
1844
+ key = "#{index}:#{index}"
1845
+ index += 1
1846
+
1847
+ # Add area NAME records
1848
+ #
1849
+ if !worksheet.print_rowmin.nil? && worksheet.print_rowmin != 0
1850
+ if ext_ref[key].nil?
1851
+ ext_refs[key] = ext_ref_count
1852
+ ext_ref_count += 1
1853
+ end
1854
+ length += 31
1855
+ end
1856
+
1857
+ # Add title NAME records
1858
+ #
1859
+ if rowmin and colmin
1860
+ if ext_ref[key].nil?
1861
+ ext_refs[key] = ext_ref_count
1862
+ ext_ref_count += 1
1863
+ end
1864
+
1865
+ length += 46
1866
+ elsif rowmin or colmin
1867
+ if ext_ref[key].nil?
1868
+ ext_refs[key] = ext_ref_count
1869
+ ext_ref_count += 1
1870
+ end
1871
+ length += 31
1872
+ else
1873
+ # TODO, may need this later.
1874
+ end
1875
+
1876
+ # Add Autofilter NAME records
1877
+ #
1878
+ if filter != 0
1879
+ if ext_refs[key].nil?
1880
+ ext_refs[key] = ext_ref_count
1881
+ ext_ref_count += 1
1882
+ end
1883
+ length += 31
1884
+ end
1885
+ end
1886
+
1887
+ # Update the ref counts.
1888
+ @ext_ref_count = ext_ref_count
1889
+ @ext_refs = ext_refs
1890
+
1891
+ # If there are no external refs then we don't write, SUPBOOK, EXTERNSHEET
1892
+ # and NAME. Therefore the length is 0.
1893
+
1894
+ return length = 0 if ext_ref_count == 0
1895
+
1896
+ # The SUPBOOK record is 8 bytes
1897
+ length += 8
1898
+
1899
+ # The EXTERNSHEET record is 6 bytes + 6 bytes for each external ref
1900
+ length += 6 * (1 + ext_ref_count)
1901
+
1902
+ return length
1903
+ end
1904
+
1905
+ ###############################################################################
1906
+ #
1907
+ # _calculate_shared_string_sizes()
1908
+ #
1909
+ # Handling of the SST continue blocks is complicated by the need to include an
1910
+ # additional continuation byte depending on whether the string is split between
1911
+ # blocks or whether it starts at the beginning of the block. (There are also
1912
+ # additional complications that will arise later when/if Rich Strings are
1913
+ # supported). As such we cannot use the simple CONTINUE mechanism provided by
1914
+ # the _add_continue() method in BIFFwriter.pm. Thus we have to make two passes
1915
+ # through the strings data. The first is to calculate the required block sizes
1916
+ # and the second, in _store_shared_strings(), is to write the actual strings.
1917
+ # The first pass through the data is also used to calculate the size of the SST
1918
+ # and CONTINUE records for use in setting the BOUNDSHEET record offsets. The
1919
+ # downside of this is that the same algorithm repeated in _store_shared_strings.
1920
+ #
1921
+ def calculate_shared_string_sizes
1922
+ strings = Array.new(@str_unique)
1923
+
1924
+ @str_table.each_key do |key|
1925
+ strings[@str_table[key]] = key
1926
+ end
1927
+ # The SST data could be very large, free some memory (maybe).
1928
+ @str_table = nil
1929
+ @str_array = strings
1930
+
1931
+ # Iterate through the strings to calculate the CONTINUE block sizes.
1932
+ #
1933
+ # The SST blocks requires a specialised CONTINUE block, so we have to
1934
+ # ensure that the maximum data block size is less than the limit used by
1935
+ # _add_continue() in BIFFwriter.pm. For simplicity we use the same size
1936
+ # for the SST and CONTINUE records:
1937
+ # 8228 : Maximum Excel97 block size
1938
+ # -4 : Length of block header
1939
+ # -8 : Length of additional SST header information
1940
+ # -8 : Arbitrary number to keep within _add_continue() limit
1941
+ # = 8208
1942
+ #
1943
+ continue_limit = 8208
1944
+ block_length = 0
1945
+ written = 0
1946
+ block_sizes = []
1947
+ continue = 0
1948
+
1949
+ strings.each do |string|
1950
+
1951
+ string_length = string.length
1952
+ encoding = string.unpack("xx C")[0]
1953
+ split_string = 0
1954
+
1955
+ # Block length is the total length of the strings that will be
1956
+ # written out in a single SST or CONTINUE block.
1957
+ #
1958
+ block_length += string_length
1959
+
1960
+ # We can write the string if it doesn't cross a CONTINUE boundary
1961
+ if block_length < continue_limit
1962
+ written += string_length
1963
+ next
1964
+ end
1965
+
1966
+
1967
+ # Deal with the cases where the next string to be written will exceed
1968
+ # the CONTINUE boundary. If the string is very long it may need to be
1969
+ # written in more than one CONTINUE record.
1970
+ #
1971
+ while block_length >= continue_limit
1972
+
1973
+ # We need to avoid the case where a string is continued in the first
1974
+ # n bytes that contain the string header information.
1975
+ #
1976
+ header_length = 3 # Min string + header size -1
1977
+ space_remaining = continue_limit -written -continue
1978
+
1979
+
1980
+ # Unicode data should only be split on char (2 byte) boundaries.
1981
+ # Therefore, in some cases we need to reduce the amount of available
1982
+ # space by 1 byte to ensure the correct alignment.
1983
+ align = 0
1984
+
1985
+ # Only applies to Unicode strings
1986
+ if encoding == 1
1987
+ # Min string + header size -1
1988
+ header_length = 4
1989
+
1990
+ if space_remaining > header_length
1991
+ # String contains 3 byte header => split on odd boundary
1992
+ if split_string == 0 and space_remaining % 2 != 1
1993
+ space_remaining -= 1
1994
+ align = 1
1995
+ # Split section without header => split on even boundary
1996
+ elsif split_string != 0 and space_remaining % 2 == 1
1997
+ space_remaining -= 1
1998
+ align = 1
1999
+ end
2000
+
2001
+ split_string = 1
2002
+ end
2003
+ end
2004
+
2005
+ if space_remaining > header_length
2006
+ # Write as much as possible of the string in the current block
2007
+ written += space_remaining
2008
+
2009
+ # Reduce the current block length by the amount written
2010
+ block_length -= continue_limit -continue -align
2011
+
2012
+ # Store the max size for this block
2013
+ block_sizes.push(continue_limit -align)
2014
+
2015
+ # If the current string was split then the next CONTINUE block
2016
+ # should have the string continue flag (grbit) set unless the
2017
+ # split string fits exactly into the remaining space.
2018
+ #
2019
+ if block_length > 0
2020
+ continue = 1
2021
+ else
2022
+ continue = 0
2023
+ end
2024
+ else
2025
+ # Store the max size for this block
2026
+ block_sizes.push(written +continue)
2027
+
2028
+ # Not enough space to start the string in the current block
2029
+ block_length -= continue_limit -space_remaining -continue
2030
+ continue = 0
2031
+ end
2032
+
2033
+ # If the string (or substr) is small enough we can write it in the
2034
+ # new CONTINUE block. Else, go through the loop again to write it in
2035
+ # one or more CONTINUE blocks
2036
+ #
2037
+ if block_length < continue_limit
2038
+ written = block_length
2039
+ else
2040
+ written = 0
2041
+ end
2042
+ end
2043
+ end
2044
+
2045
+ # Store the max size for the last block unless it is empty
2046
+ block_sizes.push(written +continue) if written +continue != 0
2047
+
2048
+ @str_block_sizes = block_sizes.dup
2049
+
2050
+ # Calculate the total length of the SST and associated CONTINUEs (if any).
2051
+ # The SST record will have a length even if it contains no strings.
2052
+ # This length is required to set the offsets in the BOUNDSHEET records since
2053
+ # they must be written before the SST records
2054
+ #
2055
+ length = 12
2056
+ length += block_sizes.shift unless block_sizes.empty? # SST
2057
+ while !block_sizes.empty? do
2058
+ length += 4 + block_sizes.shift # CONTINUEs
2059
+ end
2060
+
2061
+ return length
2062
+ end
2063
+
2064
+ ###############################################################################
2065
+ #
2066
+ # _store_shared_strings()
2067
+ #
2068
+ # Write all of the workbooks strings into an indexed array.
2069
+ #
2070
+ # See the comments in _calculate_shared_string_sizes() for more information.
2071
+ #
2072
+ # We also use this routine to record the offsets required by the EXTSST table.
2073
+ # In order to do this we first identify the first string in an EXTSST bucket
2074
+ # and then store its global and local offset within the SST table. The offset
2075
+ # occurs wherever the start of the bucket string is written out via append().
2076
+ #
2077
+ def store_shared_strings
2078
+ strings = @str_array
2079
+
2080
+ record = 0x00FC # Record identifier
2081
+ length = 0x0008 # Number of bytes to follow
2082
+ total = 0x0000
2083
+
2084
+ # Iterate through the strings to calculate the CONTINUE block sizes
2085
+ continue_limit = 8208
2086
+ block_length = 0
2087
+ written = 0
2088
+ continue = 0
2089
+
2090
+ # The SST and CONTINUE block sizes have been pre-calculated by
2091
+ # _calculate_shared_string_sizes()
2092
+ block_sizes = @str_block_sizes
2093
+
2094
+ # The SST record is required even if it contains no strings. Thus we will
2095
+ # always have a length
2096
+ #
2097
+ if block_sizes.size != 0
2098
+ length = 8 + block_sizes.shift
2099
+ else
2100
+ # No strings
2101
+ length = 8
2102
+ end
2103
+
2104
+ # Initialise variables used to track EXTSST bucket offsets.
2105
+ extsst_str_num = -1
2106
+ sst_block_start = @datasize
2107
+
2108
+ # Write the SST block header information
2109
+ header = [record, length].pack("vv")
2110
+ data = [@str_total, @str_unique].pack("VV")
2111
+ append(header, data)
2112
+
2113
+ # Iterate through the strings and write them out
2114
+ return if strings.empty?
2115
+ strings.each do |string|
2116
+
2117
+ string_length = string.length
2118
+ encoding = string.unpack("xx C")[0]
2119
+ split_string = 0
2120
+ bucket_string = 0 # Used to track EXTSST bucket offsets.
2121
+
2122
+ # Check if the string is at the start of a EXTSST bucket.
2123
+ extsst_str_num += 1
2124
+ if extsst_str_num % @extsst_bucket_size == 0
2125
+ bucket_string = 1
2126
+ end
2127
+
2128
+ # Block length is the total length of the strings that will be
2129
+ # written out in a single SST or CONTINUE block.
2130
+ #
2131
+ block_length += string_length
2132
+
2133
+ # We can write the string if it doesn't cross a CONTINUE boundary
2134
+ if block_length < continue_limit
2135
+
2136
+ # Store location of EXTSST bucket string.
2137
+ if bucket_string != 0
2138
+ global_offset = @datasize
2139
+ local_offset = @datasize - sst_block_start
2140
+
2141
+ @extsst_offsets.push([global_offset, local_offset])
2142
+ bucket_string = 0
2143
+ end
2144
+
2145
+ append(string)
2146
+ written += string_length
2147
+ next
2148
+ end
2149
+
2150
+ # Deal with the cases where the next string to be written will exceed
2151
+ # the CONTINUE boundary. If the string is very long it may need to be
2152
+ # written in more than one CONTINUE record.
2153
+ #
2154
+ while block_length >= continue_limit
2155
+
2156
+ # We need to avoid the case where a string is continued in the first
2157
+ # n bytes that contain the string header information.
2158
+ #
2159
+ header_length = 3 # Min string + header size -1
2160
+ space_remaining = continue_limit -written -continue
2161
+
2162
+
2163
+ # Unicode data should only be split on char (2 byte) boundaries.
2164
+ # Therefore, in some cases we need to reduce the amount of available
2165
+ # space by 1 byte to ensure the correct alignment.
2166
+ align = 0
2167
+
2168
+ # Only applies to Unicode strings
2169
+ if encoding == 1
2170
+ # Min string + header size -1
2171
+ header_length = 4
2172
+
2173
+ if space_remaining > header_length
2174
+ # String contains 3 byte header => split on odd boundary
2175
+ if split_string == 0 and space_remaining % 2 != 1
2176
+ space_remaining -= 1
2177
+ align = 1
2178
+ # Split section without header => split on even boundary
2179
+ elsif split_string != 0 and space_remaining % 2 == 1
2180
+ space_remaining -= 1
2181
+ align = 1
2182
+ end
2183
+
2184
+ split_string = 1
2185
+ end
2186
+ end
2187
+
2188
+ if space_remaining > header_length
2189
+ # Write as much as possible of the string in the current block
2190
+ tmp = string[0, space_remaining]
2191
+
2192
+ # Store location of EXTSST bucket string.
2193
+ if bucket_string != 0
2194
+ global_offset = @datasize
2195
+ local_offset = @datasize - sst_block_start
2196
+
2197
+ @extsst_offsets.push([global_offset, local_offset])
2198
+ bucket_string = 0
2199
+ end
2200
+
2201
+ append(tmp)
2202
+
2203
+
2204
+ # The remainder will be written in the next block(s)
2205
+ string = string[space_remaining .. string.length-1]
2206
+
2207
+ # Reduce the current block length by the amount written
2208
+ block_length -= continue_limit -continue -align
2209
+
2210
+ # If the current string was split then the next CONTINUE block
2211
+ # should have the string continue flag (grbit) set unless the
2212
+ # split string fits exactly into the remaining space.
2213
+ #
2214
+ if block_length > 0
2215
+ continue = 1
2216
+ else
2217
+ continue = 0
2218
+ end
2219
+ else
2220
+ # Not enough space to start the string in the current block
2221
+ block_length -= continue_limit -space_remaining -continue
2222
+ continue = 0
2223
+ end
2224
+
2225
+ # Write the CONTINUE block header
2226
+ if block_sizes.size != 0
2227
+ sst_block_start= @datasize # Reset EXTSST offset.
2228
+
2229
+ record = 0x003C
2230
+ length = block_sizes.shift
2231
+
2232
+ header = [record, length].pack("vv")
2233
+ header = header + [encoding].pack("C") if continue != 0
2234
+
2235
+ append(header)
2236
+ end
2237
+
2238
+ # If the string (or substr) is small enough we can write it in the
2239
+ # new CONTINUE block. Else, go through the loop again to write it in
2240
+ # one or more CONTINUE blocks
2241
+ #
2242
+ if block_length < continue_limit
2243
+
2244
+ # Store location of EXTSST bucket string.
2245
+ if bucket_string != 0
2246
+ global_offset = @datasize
2247
+ local_offset = @datasize - sst_block_start
2248
+
2249
+ @extsst_offsets.push([global_offset, local_offset])
2250
+
2251
+ bucket_string = 0
2252
+ end
2253
+ append(string)
2254
+
2255
+ written = block_length
2256
+ else
2257
+ written = 0
2258
+ end
2259
+ end
2260
+ end
2261
+ end
2262
+
2263
+ ###############################################################################
2264
+ #
2265
+ # _calculate_extsst_size
2266
+ #
2267
+ # The number of buckets used in the EXTSST is between 0 and 128. The number of
2268
+ # strings per bucket (bucket size) has a minimum value of 8 and a theoretical
2269
+ # maximum of 2^16. For "number of strings" < 1024 there is a constant bucket
2270
+ # size of 8. The following algorithm generates the same size/bucket ratio
2271
+ # as Excel.
2272
+ #
2273
+ def calculate_extsst_size
2274
+ unique_strings = @str_unique
2275
+
2276
+ if unique_strings < 1024
2277
+ bucket_size = 8
2278
+ else
2279
+ bucket_size = 1 + Integer(unique_strings / 128.0)
2280
+ end
2281
+
2282
+ buckets = Integer((unique_strings + bucket_size -1) / Float(bucket_size))
2283
+
2284
+ @extsst_buckets = buckets
2285
+ @extsst_bucket_size = bucket_size
2286
+
2287
+ return 6 + 8 * buckets
2288
+ end
2289
+
2290
+ ###############################################################################
2291
+ #
2292
+ # _store_extsst
2293
+ #
2294
+ # Write EXTSST table using the offsets calculated in _store_shared_strings().
2295
+ #
2296
+ def store_extsst
2297
+ offsets = @extsst_offsets
2298
+ bucket_size = @extsst_bucket_size
2299
+
2300
+ record = 0x00FF # Record identifier
2301
+ length = 2 + 8 * offsets.size # Bytes to follow
2302
+
2303
+ header = [record, length].pack('vv')
2304
+ data = [bucket_size].pack('v')
2305
+
2306
+ offsets.each do |offset|
2307
+ data = data + [offset[0], offset[1], 0].pack('Vvv')
2308
+ end
2309
+
2310
+ append(header, data)
2311
+
2312
+ end
2313
+
2314
+ #
2315
+ # Methods related to comments and MSO objects.
2316
+ #
2317
+
2318
+ ###############################################################################
2319
+ #
2320
+ # _add_mso_drawing_group()
2321
+ #
2322
+ # Write the MSODRAWINGGROUP record that keeps track of the Escher drawing
2323
+ # objects in the file such as images, comments and filters.
2324
+ #
2325
+ def add_mso_drawing_group #:nodoc:
2326
+ return unless @mso_size != 0
2327
+
2328
+ record = 0x00EB # Record identifier
2329
+ length = 0x0000 # Number of bytes to follow
2330
+
2331
+ data = store_mso_dgg_container
2332
+ data = data + store_mso_dgg(*@mso_clusters)
2333
+ data = data + store_mso_bstore_container
2334
+ @images_data.each do |image|
2335
+ data = data + store_mso_images(*image)
2336
+ end
2337
+ data = data + store_mso_opt
2338
+ data = data + store_mso_split_menu_colors
2339
+
2340
+ length = data.length
2341
+ header = [record, length].pack("vv")
2342
+
2343
+ add_mso_drawing_group_continue(header + data)
2344
+
2345
+ return header + data # For testing only.
2346
+ end
2347
+
2348
+ ###############################################################################
2349
+ #
2350
+ # _add_mso_drawing_group_continue()
2351
+ #
2352
+ # See first the Spreadsheet::WriteExcel::BIFFwriter::_add_continue() method.
2353
+ #
2354
+ # Add specialised CONTINUE headers to large MSODRAWINGGROUP data block.
2355
+ # We use the Excel 97 max block size of 8228 - 4 bytes for the header = 8224.
2356
+ #
2357
+ # The structure depends on the size of the data block:
2358
+ #
2359
+ # Case 1: <= 8224 bytes 1 MSODRAWINGGROUP
2360
+ # Case 2: <= 2*8224 bytes 1 MSODRAWINGGROUP + 1 CONTINUE
2361
+ # Case 3: > 2*8224 bytes 2 MSODRAWINGGROUP + n CONTINUE
2362
+ #
2363
+ def add_mso_drawing_group_continue(data)
2364
+ limit = 8228 -4
2365
+ mso_group = 0x00EB # Record identifier
2366
+ continue = 0x003C # Record identifier
2367
+ block_count = 1
2368
+
2369
+ # Ignore the base class _add_continue() method.
2370
+ @ignore_continue = 1
2371
+
2372
+ # Case 1 above. Just return the data as it is.
2373
+ if data.length <= limit
2374
+ append(data)
2375
+ return
2376
+ end
2377
+
2378
+ # Change length field of the first MSODRAWINGGROUP block. Case 2 and 3.
2379
+ tmp = data.dup
2380
+ tmp[0, limit + 4] = ""
2381
+ tmp[2, 2] = [limit].pack('v')
2382
+ append(tmp)
2383
+
2384
+ # Add MSODRAWINGGROUP and CONTINUE blocks for Case 3 above.
2385
+ while data.length > limit
2386
+ if block_count == 1
2387
+ # Add extra MSODRAWINGGROUP block header.
2388
+ header = [mso_group, limit].pack("vv")
2389
+ block_count += 1
2390
+ else
2391
+ # Add normal CONTINUE header.
2392
+ header = [continue, limit].pack("vv")
2393
+ end
2394
+
2395
+ tmp = data.dup
2396
+ tmp[0, limit] = ''
2397
+ append(header, tmp)
2398
+ end
2399
+
2400
+ # Last CONTINUE block for remaining data. Case 2 and 3 above.
2401
+ header = [continue, data.length].pack("vv")
2402
+ append(header, data)
2403
+
2404
+ # Turn the base class _add_continue() method back on.
2405
+ @ignore_continue = 0
2406
+ end
2407
+
2408
+ ###############################################################################
2409
+ #
2410
+ # _store_mso_dgg_container()
2411
+ #
2412
+ # Write the Escher DggContainer record that is part of MSODRAWINGGROUP.
2413
+ #
2414
+ def store_mso_dgg_container
2415
+ type = 0xF000
2416
+ version = 15
2417
+ instance = 0
2418
+ data = ''
2419
+ length = @mso_size -12 # -4 (biff header) -8 (for this).
2420
+
2421
+ return add_mso_generic(type, version, instance, data, length)
2422
+ end
2423
+
2424
+
2425
+ ###############################################################################
2426
+ #
2427
+ # _store_mso_dgg()
2428
+ # my $max_spid = $_[0];
2429
+ # my $num_clusters = $_[1];
2430
+ # my $shapes_saved = $_[2];
2431
+ # my $drawings_saved = $_[3];
2432
+ # my $clusters = $_[4];
2433
+ #
2434
+ # Write the Escher Dgg record that is part of MSODRAWINGGROUP.
2435
+ #
2436
+ def store_mso_dgg(max_spid, num_clusters, shapes_saved, drawings_saved, clusters)
2437
+ type = 0xF006
2438
+ version = 0
2439
+ instance = 0
2440
+ data = ''
2441
+ length = nil # Calculate automatically.
2442
+
2443
+ data = [max_spid, num_clusters,
2444
+ shapes_saved, drawings_saved].pack("VVVV")
2445
+
2446
+ clusters.each do |aref|
2447
+ drawing_id = aref[0]
2448
+ shape_ids_used = aref[1]
2449
+
2450
+ data = data + [drawing_id, shape_ids_used].pack("VV")
2451
+ end
2452
+
2453
+ return add_mso_generic(type, version, instance, data, length)
2454
+ end
2455
+
2456
+ ###############################################################################
2457
+ #
2458
+ # _store_mso_bstore_container()
2459
+ #
2460
+ # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP.
2461
+ #
2462
+ def store_mso_bstore_container
2463
+ return '' if @images_size == 0
2464
+
2465
+ type = 0xF001
2466
+ version = 15
2467
+ instance = @images_data.size # Number of images.
2468
+ data = ''
2469
+ length = @images_size +8 *instance
2470
+
2471
+ return add_mso_generic(type, version, instance, data, length)
2472
+ end
2473
+
2474
+ ###############################################################################
2475
+ #
2476
+ # _store_mso_images()
2477
+ # ref_count = $_[0]
2478
+ # image_type = $_[1]
2479
+ # image = $_[2]
2480
+ # size = $_[3]
2481
+ # checksum1 = $_[4]
2482
+ # checksum2 = $_[5]
2483
+ #
2484
+ # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP.
2485
+ #
2486
+ def store_mso_images(ref_count, image_type, image, size, checksum1, checksum2)
2487
+ blip_store_entry = store_mso_blip_store_entry(
2488
+ ref_count,
2489
+ image_type,
2490
+ size,
2491
+ checksum1
2492
+ )
2493
+
2494
+ blip = store_mso_blip(
2495
+ image_type,
2496
+ image,
2497
+ size,
2498
+ checksum1,
2499
+ checksum2
2500
+ )
2501
+
2502
+ return blip_store_entry + blip
2503
+ end
2504
+
2505
+ ###############################################################################
2506
+ #
2507
+ # _store_mso_blip_store_entry()
2508
+ # ref_count = $_[0]
2509
+ # image_type = $_[1]
2510
+ # size = $_[2]
2511
+ # checksum1 = $_[3]
2512
+ #
2513
+ # Write the Escher BlipStoreEntry record that is part of MSODRAWINGGROUP.
2514
+ #
2515
+ def store_mso_blip_store_entry(ref_count, image_type, size, checksum1)
2516
+ type = 0xF007
2517
+ version = 2
2518
+ instance = image_type
2519
+ length = size +61
2520
+ data = [image_type].pack('C') + # Win32
2521
+ [image_type].pack('C') + # Mac
2522
+ [checksum1].pack('H*') + # Uid checksum
2523
+ [0xFF].pack('v') + # Tag
2524
+ [size +25].pack('V') + # Next Blip size
2525
+ [ref_count].pack('V') + # Image ref count
2526
+ [0x00000000].pack('V') + # File offset
2527
+ [0x00].pack('C') + # Usage
2528
+ [0x00].pack('C') + # Name length
2529
+ [0x00].pack('C') + # Unused
2530
+ [0x00].pack('C') # Unused
2531
+
2532
+ return add_mso_generic(type, version, instance, data, length)
2533
+ end
2534
+
2535
+ ###############################################################################
2536
+ #
2537
+ # _store_mso_blip()
2538
+ # image_type = $_[0]
2539
+ # image_data = $_[1]
2540
+ # size = $_[2]
2541
+ # checksum1 = $_[3]
2542
+ # checksum2 = $_[4]
2543
+ #
2544
+ # Write the Escher Blip record that is part of MSODRAWINGGROUP.
2545
+ #
2546
+ def store_mso_blip(image_type, image_data, size, checksum1, checksum2)
2547
+ instance = 0x046A if image_type == 5 # JPG
2548
+ instance = 0x06E0 if image_type == 6 # PNG
2549
+ instance = 0x07A9 if image_type == 7 # BMP
2550
+
2551
+ # BMPs contain an extra checksum for the stripped data.
2552
+ if image_type == 7
2553
+ checksum1 = checksum2 + checksum1
2554
+ end
2555
+
2556
+ type = 0xF018 + image_type
2557
+ version = 0x0000
2558
+ length = size +17
2559
+ data = [checksum1].pack('H*') + # Uid checksum
2560
+ [0xFF].pack('C') + # Tag
2561
+ image_data # Image
2562
+
2563
+ return add_mso_generic(type, version, instance, data, length)
2564
+ end
2565
+
2566
+ ###############################################################################
2567
+ #
2568
+ # _store_mso_opt()
2569
+ #
2570
+ # Write the Escher Opt record that is part of MSODRAWINGGROUP.
2571
+ #
2572
+ def store_mso_opt
2573
+ type = 0xF00B
2574
+ version = 3
2575
+ instance = 3
2576
+ data = ''
2577
+ length = 18
2578
+
2579
+ data = ['BF0008000800810109000008C0014000'+'0008'].pack("H*")
2580
+
2581
+ return add_mso_generic(type, version, instance, data, length)
2582
+ end
2583
+
2584
+ ###############################################################################
2585
+ #
2586
+ # _store_mso_split_menu_colors()
2587
+ #
2588
+ # Write the Escher SplitMenuColors record that is part of MSODRAWINGGROUP.
2589
+ #
2590
+ def store_mso_split_menu_colors
2591
+ type = 0xF11E
2592
+ version = 0
2593
+ instance = 4
2594
+ data = ''
2595
+ length = 16
2596
+
2597
+ data = ['0D0000080C00000817000008F7000010'].pack("H*")
2598
+
2599
+ return add_mso_generic(type, version, instance, data, length)
2600
+ end
2601
+
2602
+ end