writeexcel 0.1.0

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