writeexcel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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