keeguon-spreadsheet 0.9.3

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +619 -0
  3. data/Manifest.txt +85 -0
  4. data/bin/xlsopcodes +18 -0
  5. data/lib/parseexcel.rb +27 -0
  6. data/lib/parseexcel/parseexcel.rb +75 -0
  7. data/lib/parseexcel/parser.rb +11 -0
  8. data/lib/spreadsheet.rb +80 -0
  9. data/lib/spreadsheet/column.rb +71 -0
  10. data/lib/spreadsheet/compatibility.rb +23 -0
  11. data/lib/spreadsheet/datatypes.rb +161 -0
  12. data/lib/spreadsheet/encodings.rb +57 -0
  13. data/lib/spreadsheet/excel.rb +88 -0
  14. data/lib/spreadsheet/excel/error.rb +26 -0
  15. data/lib/spreadsheet/excel/internals.rb +458 -0
  16. data/lib/spreadsheet/excel/internals/biff5.rb +17 -0
  17. data/lib/spreadsheet/excel/internals/biff8.rb +19 -0
  18. data/lib/spreadsheet/excel/offset.rb +41 -0
  19. data/lib/spreadsheet/excel/password_hash.rb +24 -0
  20. data/lib/spreadsheet/excel/reader.rb +1302 -0
  21. data/lib/spreadsheet/excel/reader/biff5.rb +42 -0
  22. data/lib/spreadsheet/excel/reader/biff8.rb +231 -0
  23. data/lib/spreadsheet/excel/rgb.rb +122 -0
  24. data/lib/spreadsheet/excel/row.rb +98 -0
  25. data/lib/spreadsheet/excel/sst_entry.rb +46 -0
  26. data/lib/spreadsheet/excel/workbook.rb +80 -0
  27. data/lib/spreadsheet/excel/worksheet.rb +115 -0
  28. data/lib/spreadsheet/excel/writer.rb +1 -0
  29. data/lib/spreadsheet/excel/writer/biff8.rb +75 -0
  30. data/lib/spreadsheet/excel/writer/format.rb +264 -0
  31. data/lib/spreadsheet/excel/writer/n_worksheet.rb +888 -0
  32. data/lib/spreadsheet/excel/writer/workbook.rb +735 -0
  33. data/lib/spreadsheet/excel/writer/worksheet.rb +940 -0
  34. data/lib/spreadsheet/font.rb +115 -0
  35. data/lib/spreadsheet/format.rb +209 -0
  36. data/lib/spreadsheet/formula.rb +9 -0
  37. data/lib/spreadsheet/helpers.rb +11 -0
  38. data/lib/spreadsheet/link.rb +43 -0
  39. data/lib/spreadsheet/note.rb +23 -0
  40. data/lib/spreadsheet/noteObject.rb +17 -0
  41. data/lib/spreadsheet/row.rb +151 -0
  42. data/lib/spreadsheet/workbook.rb +143 -0
  43. data/lib/spreadsheet/worksheet.rb +326 -0
  44. data/lib/spreadsheet/writer.rb +30 -0
  45. data/test/data/test_adding_data_to_existing_file.xls +0 -0
  46. data/test/data/test_borders.xls +0 -0
  47. data/test/data/test_changes.xls +0 -0
  48. data/test/data/test_comment.xls +0 -0
  49. data/test/data/test_copy.xls +0 -0
  50. data/test/data/test_datetime.xls +0 -0
  51. data/test/data/test_empty.xls +0 -0
  52. data/test/data/test_formula.xls +0 -0
  53. data/test/data/test_long_sst_record.xls +0 -0
  54. data/test/data/test_margin.xls +0 -0
  55. data/test/data/test_merged_and_protected.xls +0 -0
  56. data/test/data/test_merged_cells.xls +0 -0
  57. data/test/data/test_missing_row.xls +0 -0
  58. data/test/data/test_pagesetup.xls +0 -0
  59. data/test/data/test_version_excel5.xls +0 -0
  60. data/test/data/test_version_excel95.xls +0 -0
  61. data/test/data/test_version_excel97.xls +0 -0
  62. data/test/data/test_version_excel97_2010.xls +0 -0
  63. data/test/data/test_worksheet_visibility.xls +0 -0
  64. data/test/excel/reader.rb +30 -0
  65. data/test/excel/row.rb +40 -0
  66. data/test/excel/writer/workbook.rb +95 -0
  67. data/test/excel/writer/worksheet.rb +81 -0
  68. data/test/font.rb +163 -0
  69. data/test/format.rb +95 -0
  70. data/test/integration.rb +1390 -0
  71. data/test/row.rb +33 -0
  72. data/test/suite.rb +18 -0
  73. data/test/workbook.rb +55 -0
  74. data/test/workbook_protection.rb +19 -0
  75. data/test/worksheet.rb +112 -0
  76. metadata +148 -0
@@ -0,0 +1,735 @@
1
+ require 'spreadsheet/excel/internals'
2
+ require 'spreadsheet/writer'
3
+ require 'spreadsheet/excel/writer/biff8'
4
+ require 'spreadsheet/excel/writer/format'
5
+ require 'spreadsheet/excel/writer/worksheet'
6
+ require 'ole/storage'
7
+
8
+ module Spreadsheet
9
+ module Excel
10
+ module Writer
11
+ ##
12
+ # Writer class for Excel Workbooks. Most write_* method correspond to an
13
+ # Excel-Record/Opcode. Designed to be able to write several Workbooks in
14
+ # parallel (just because I can't imagine why you would want to do that
15
+ # doesn't mean it shouldn't be possible ;). You should not need to call any of
16
+ # its methods directly. If you think you do, look at #write_workbook
17
+ class Workbook < Spreadsheet::Writer
18
+ include Spreadsheet::Excel::Writer::Biff8
19
+ include Spreadsheet::Excel::Internals
20
+ attr_reader :fonts, :date_base
21
+ def initialize *args
22
+ super
23
+ @biff_version = 0x0600
24
+ @bof = 0x0809
25
+ @build_id = 3515
26
+ @build_year = 1996
27
+ @bof_types = {
28
+ :globals => 0x0005,
29
+ :visual_basic => 0x0006,
30
+ :worksheet => 0x0010,
31
+ :chart => 0x0020,
32
+ :macro_sheet => 0x0040,
33
+ :workspace => 0x0100,
34
+ }
35
+ @worksheets = {}
36
+ @sst = {}
37
+ @recordsize_limit = 8224
38
+ @fonts = {}
39
+ @formats = {}
40
+ @number_formats = {}
41
+ end
42
+ def cleanup workbook
43
+ worksheets(workbook).each do |worksheet|
44
+ @sst.delete worksheet
45
+ end
46
+ @fonts.delete workbook
47
+ @formats.delete workbook
48
+ @number_formats.delete workbook
49
+ @worksheets.delete workbook
50
+ end
51
+ def collect_formats workbook, opts={}
52
+ # The default cell format is always present in an Excel file, described by
53
+ # the XF record with the fixed index 15 (0-based). By default, it uses the
54
+ # worksheet/workbook default cell style, described by the very first XF
55
+ # record (index 0).
56
+ formats = []
57
+ unless opts[:existing_document]
58
+ 15.times do
59
+ formats.push Format.new(self, workbook, workbook.default_format,
60
+ :type => :style)
61
+ end
62
+ formats.push Format.new(self, workbook)
63
+ end
64
+ workbook.formats.each do |fmt|
65
+ formats.push Format.new(self, workbook, fmt)
66
+ end
67
+ @formats[workbook] = {
68
+ :writers => [],
69
+ :xf_indexes => {}
70
+ }
71
+ formats.each_with_index do |fmt, idx|
72
+ @formats[workbook][:writers] << fmt
73
+ @formats[workbook][:xf_indexes][fmt.format] ||= idx
74
+ end
75
+ end
76
+ def complete_sst_update? workbook
77
+ stored = workbook.sst.collect do |entry| entry.content end
78
+ num_total = 0
79
+ current = worksheets(workbook).inject(Hash.new(0)) do |memo, worksheet|
80
+ worksheet.strings.each do |k,v|
81
+ memo[k] += v
82
+ num_total += v
83
+ end
84
+ memo
85
+ end
86
+ current.delete ''
87
+ if !stored.empty? && stored.all?{|x| current.include?(x) }
88
+ ## if all previously stored strings are still needed, we don't have to
89
+ # rewrite all cells because the sst-index of such string does not change.
90
+ additions = current.keys - stored
91
+ [:partial_update, num_total, stored + additions]
92
+ else
93
+ [:complete_update, num_total, current.keys]
94
+ end
95
+ end
96
+ def font_index workbook, font_key
97
+ idx = @fonts[workbook][font_key] || 0
98
+ ## this appears to be undocumented: the first 4 fonts seem to be accessed
99
+ # with a 0-based index, but all subsequent font indices are 1-based.
100
+ idx > 3 ? idx.next : idx
101
+ end
102
+ def number_format_index workbook, format
103
+ @number_formats[workbook][format] || 0
104
+ end
105
+ def sanitize_worksheets sheets
106
+ return sheets if sheets.empty?
107
+ found_selected = false
108
+ sheets.each do |sheet|
109
+ found_selected ||= sheet.selected
110
+ sheet.format_dates!
111
+ end
112
+ unless found_selected
113
+ sheets.first.selected = true
114
+ end
115
+ sheets
116
+ end
117
+ def worksheets workbook
118
+ @worksheets[workbook] ||= workbook.worksheets.collect do |worksheet|
119
+ Excel::Writer::Worksheet.new self, worksheet
120
+ end
121
+ end
122
+ def write_bof workbook, writer, type
123
+ data = [
124
+ @biff_version, # BIFF version (always 0x0600 for BIFF8)
125
+ @bof_types[type], # Type of the following data:
126
+ # 0x0005 = Workbook globals
127
+ # 0x0006 = Visual Basic module
128
+ # 0x0010 = Worksheet
129
+ # 0x0020 = Chart
130
+ # 0x0040 = Macro sheet
131
+ # 0x0100 = Workspace file
132
+ @build_id, # Build identifier
133
+ @build_year, # Build year
134
+ 0x000, # File history flags
135
+ 0x006, # Lowest Excel version that can read
136
+ # all records in this file
137
+ ]
138
+ write_op writer, @bof, data.pack("v4V2")
139
+ end
140
+ def write_bookbool workbook, writer
141
+ write_placeholder writer, 0x00da
142
+ end
143
+ def write_boundsheets workbook, writer, offset
144
+ worksheets = worksheets(workbook)
145
+ worksheets.each do |worksheet|
146
+ # account for boundsheet-entry
147
+ offset += worksheet.boundsheet_size
148
+ end
149
+ worksheets.each do |worksheet|
150
+ visibility = SEITILIBISIV_TEEHSKROW[worksheet.worksheet.visibility]
151
+ data = [
152
+ offset, # Absolute stream position of the BOF record of the sheet
153
+ # represented by this record. This field is never encrypted
154
+ # in protected files.
155
+ visibility, # Visibility: 0x00 = Visible
156
+ # 0x01 = Hidden
157
+ # 0x02 = Strong hidden (see below)
158
+ 0x00, # Sheet type: 0x00 = Worksheet
159
+ # 0x02 = Chart
160
+ # 0x06 = Visual Basic module
161
+ ]
162
+ write_op writer, 0x0085, data.pack("VC2"), worksheet.name
163
+ offset += worksheet.size
164
+ end
165
+ end
166
+ ##
167
+ # Copy unchanged data verbatim, adjust offsets and write new records for
168
+ # changed data.
169
+ def write_changes workbook, io
170
+ sanitize_worksheets workbook.worksheets
171
+ collect_formats workbook, :existing_document => true
172
+ reader = workbook.ole
173
+ sheet_data = {}
174
+ sst_status, sst_total, sst_strings = complete_sst_update? workbook
175
+ sst = {}
176
+ sst_strings.each_with_index do |str, idx| sst.store str, idx end
177
+ sheets = worksheets(workbook)
178
+ positions = []
179
+ newsheets = []
180
+ sheets.each do |sheet|
181
+ @sst[sheet] = sst
182
+ pos, len = workbook.offsets[sheet.worksheet]
183
+ if pos
184
+ positions.push pos
185
+ sheet.write_changes reader, pos + len, sst_status
186
+ else
187
+ newsheets.push sheet
188
+ sheet.write_from_scratch
189
+ end
190
+ sheet_data[sheet.worksheet] = sheet.data
191
+ end
192
+ Ole::Storage.open io do |ole|
193
+ ole.file.open 'Workbook', 'w' do |writer|
194
+ reader.seek lastpos = 0
195
+ workbook.offsets.select do |key, pair|
196
+ workbook.changes.include? key
197
+ end.sort_by do |key, (pos, _)|
198
+ pos
199
+ end.each do |key, (pos, len)|
200
+ data = reader.read(pos - lastpos)
201
+ writer.write data
202
+ case key
203
+ when Spreadsheet::Worksheet
204
+ writer.write sheet_data[key]
205
+ when :boundsheets
206
+ ## boundsheets are hard to calculate. The offset below is only
207
+ # correct if there are no more changes in the workbook globals
208
+ # string after this.
209
+ oldoffset = positions.min - len
210
+ lastpos = pos + len
211
+ bytechange = 0
212
+ buffer = StringIO.new ''
213
+ if tuple = workbook.offsets[:sst]
214
+ write_sst_changes workbook, buffer, writer.pos,
215
+ sst_total, sst_strings
216
+ pos, len = tuple
217
+ if offset = workbook.offsets[:extsst]
218
+ len += offset[1].to_i
219
+ end
220
+ bytechange = buffer.size - len
221
+ write_boundsheets workbook, writer, oldoffset + bytechange
222
+ reader.seek lastpos
223
+ writer.write reader.read(pos - lastpos)
224
+ buffer.rewind
225
+ writer.write buffer.read
226
+ elsif sst.empty? || workbook.biff_version < 8
227
+ write_boundsheets workbook, writer, oldoffset + bytechange
228
+ else
229
+ write_sst workbook, buffer, writer.pos
230
+ write_boundsheets workbook, writer, oldoffset + buffer.size
231
+ pos = lastpos
232
+ len = positions.min - lastpos
233
+ if len > OPCODE_SIZE
234
+ reader.seek pos
235
+ writer.write reader.read(len - OPCODE_SIZE)
236
+ end
237
+ buffer.rewind
238
+ writer.write buffer.read
239
+ write_eof workbook, writer
240
+ end
241
+ else
242
+ send "write_#{key}", workbook, writer
243
+ end
244
+ lastpos = [pos + len, reader.size - 1].min
245
+ reader.seek lastpos
246
+ end
247
+ writer.write reader.read
248
+ newsheets.each do |sheet|
249
+ writer.write sheet.data
250
+ end
251
+ end
252
+ end
253
+ end
254
+ def write_datemode workbook, writer
255
+ mode = @date_base.year == 1899 ? 0x00 : 0x01
256
+ data = [
257
+ mode, # 0 = Base date is 1899-Dec-31
258
+ # (the cell value 1 represents 1900-Jan-01)
259
+ # 1 = Base date is 1904-Jan-01
260
+ # (the cell value 1 represents 1904-Jan-02)
261
+ ]
262
+ write_op writer, 0x0022, data.pack('v')
263
+ end
264
+ def write_dsf workbook, writer
265
+ data = [
266
+ 0x00, # 0 = Only the BIFF8 “Workbook” stream is present
267
+ # 1 = Additional BIFF5/BIFF7 “Book” stream is in the file
268
+ ]
269
+ write_op writer, 0x0161, data.pack('v')
270
+ end
271
+ def write_encoding workbook, writer
272
+ enc = workbook.encoding || 'UTF-16LE'
273
+ if RUBY_VERSION >= '1.9' && enc.is_a?(Encoding)
274
+ enc = enc.name.upcase
275
+ end
276
+ cp = SEGAPEDOC[enc] or raise "Invalid or Unknown Codepage '#{enc}'"
277
+ write_op writer, 0x0042, [cp].pack('v')
278
+ end
279
+ def write_eof workbook, writer
280
+ write_op writer, 0x000a
281
+ end
282
+ def write_extsst workbook, offsets, writer
283
+ header = [SST_CHUNKSIZE].pack('v')
284
+ data = offsets.collect do |pair| pair.push(0).pack('Vv2') end
285
+ write_op writer, 0x00ff, header, data
286
+ end
287
+ def write_font workbook, writer, font
288
+ # TODO: Colors/Palette index
289
+ size = font.size * TWIPS
290
+ color = SEDOC_ROLOC[font.color] || SEDOC_ROLOC[:text]
291
+ weight = FONT_WEIGHTS.fetch(font.weight, font.weight)
292
+ weight = [[weight, 1000].min, 100].max
293
+ esc = SEPYT_TNEMEPACSE.fetch(font.escapement, 0)
294
+ underline = SEPYT_ENILREDNU.fetch(font.underline, 0)
295
+ family = SEILIMAF_TNOF.fetch(font.family, 0)
296
+ encoding = SGNIDOCNE_TNOF.fetch(font.encoding, 0)
297
+ options = 0
298
+ options |= 0x0001 if weight > 600
299
+ options |= 0x0002 if font.italic?
300
+ options |= 0x0004 if underline > 0
301
+ options |= 0x0008 if font.strikeout?
302
+ options |= 0x0010 if font.outline?
303
+ options |= 0x0020 if font.shadow?
304
+ data = [
305
+ size, # Height of the font (in twips = 1/20 of a point)
306
+ options, # Option flags:
307
+ # Bit Mask Contents
308
+ # 0 0x0001 1 = Characters are bold (redundant, see below)
309
+ # 1 0x0002 1 = Characters are italic
310
+ # 2 0x0004 1 = Characters are underlined (redundant)
311
+ # 3 0x0008 1 = Characters are struck out
312
+ # 4 0x0010 1 = Characters are outlined (djberger)
313
+ # 5 0x0020 1 = Characters are shadowed (djberger)
314
+ color, # Palette index (➜ 6.70)
315
+ weight, # Font weight (100-1000). Standard values are
316
+ # 0x0190 (400) for normal text and
317
+ # 0x02bc (700) for bold text.
318
+ esc, # Escapement type: 0x0000 = None
319
+ # 0x0001 = Superscript
320
+ # 0x0002 = Subscript
321
+ underline,# Underline type: 0x00 = None
322
+ # 0x01 = Single
323
+ # 0x02 = Double
324
+ # 0x21 = Single accounting
325
+ # 0x22 = Double accounting
326
+ family, # Font family: 0x00 = None (unknown or don't care)
327
+ # 0x01 = Roman (variable width, serifed)
328
+ # 0x02 = Swiss (variable width, sans-serifed)
329
+ # 0x03 = Modern (fixed width,
330
+ # serifed or sans-serifed)
331
+ # 0x04 = Script (cursive)
332
+ # 0x05 = Decorative (specialised,
333
+ # e.g. Old English, Fraktur)
334
+ encoding, # Character set: 0x00 = 0 = ANSI Latin
335
+ # 0x01 = 1 = System default
336
+ # 0x02 = 2 = Symbol
337
+ # 0x4d = 77 = Apple Roman
338
+ # 0x80 = 128 = ANSI Japanese Shift-JIS
339
+ # 0x81 = 129 = ANSI Korean (Hangul)
340
+ # 0x82 = 130 = ANSI Korean (Johab)
341
+ # 0x86 = 134 = ANSI Chinese Simplified GBK
342
+ # 0x88 = 136 = ANSI Chinese Traditional BIG5
343
+ # 0xa1 = 161 = ANSI Greek
344
+ # 0xa2 = 162 = ANSI Turkish
345
+ # 0xa3 = 163 = ANSI Vietnamese
346
+ # 0xb1 = 177 = ANSI Hebrew
347
+ # 0xb2 = 178 = ANSI Arabic
348
+ # 0xba = 186 = ANSI Baltic
349
+ # 0xcc = 204 = ANSI Cyrillic
350
+ # 0xde = 222 = ANSI Thai
351
+ # 0xee = 238 = ANSI Latin II (Central European)
352
+ # 0xff = 255 = OEM Latin I
353
+ ]
354
+ name = unicode_string font.name # Font name: Unicode string,
355
+ # 8-bit string length (➜ 3.4)
356
+ write_op writer, opcode(:font), data.pack(binfmt(:font)), name
357
+ end
358
+ def write_fonts workbook, writer
359
+ fonts = @fonts[workbook] = {}
360
+ @formats[workbook][:writers].map{|format| format.font }.compact.uniq.each do |font|
361
+ unless fonts.include?(font.key)
362
+ fonts.store font.key, fonts.size
363
+ write_font workbook, writer, font
364
+ end
365
+ end
366
+ end
367
+ def write_formats workbook, writer
368
+ # From BIFF5 on, the built-in number formats will be omitted. The built-in
369
+ # formats are dependent on the current regional settings of the operating
370
+ # system. BUILTIN_FORMATS shows which number formats are used by
371
+ # default in a US-English environment. All indexes from 0 to 163 are
372
+ # reserved for built-in formats.
373
+ # The first user-defined format starts at 164 (0xa4).
374
+ formats = @number_formats[workbook] = {}
375
+ BUILTIN_FORMATS.each do |idx, str|
376
+ formats.store client(str, 'UTF-8'), idx
377
+ end
378
+ ## Ensure at least a 'GENERAL' format is written
379
+ formats.delete client('GENERAL', 'UTF-8')
380
+ idx = 0xa4
381
+ workbook.formats.each do |fmt|
382
+ str = fmt.number_format
383
+ unless formats[str]
384
+ formats.store str, idx
385
+ # Number format string (Unicode string, 16-bit string length, ➜ 3.4)
386
+ write_op writer, opcode(:format), [idx].pack('v'), unicode_string(str, 2)
387
+ idx += 1
388
+ end
389
+ end
390
+ end
391
+ ##
392
+ # Write a new Excel file.
393
+ def write_from_scratch workbook, io
394
+ sanitize_worksheets workbook.worksheets
395
+ collect_formats workbook
396
+ sheets = worksheets workbook
397
+ buffer1 = StringIO.new ''
398
+ # ● BOF Type = workbook globals (➜ 6.8)
399
+ write_bof workbook, buffer1, :globals
400
+ # ○ File Protection Block ➜ 4.19
401
+ # ○ WRITEACCESS User name (BIFF3-BIFF8, ➜ 5.112)
402
+ # ○ FILESHARING File sharing options (BIFF3-BIFF8, ➜ 5.44)
403
+ # ○ CODEPAGE ➜ 6.17
404
+ write_encoding workbook, buffer1
405
+ # ○ DSF ➜ 6.32
406
+ write_dsf workbook, buffer1
407
+ # ○ TABID
408
+ write_tabid workbook, buffer1
409
+ # ○ FNGROUPCOUNT
410
+ # ○ Workbook Protection Block ➜ 4.18
411
+ # ○ WINDOWPROTECT Window settings: 1 = protected (➜ 5.111)
412
+ # ○ PROTECT Cell contents: 1 = protected (➜ 5.82)
413
+ write_protect workbook, buffer1
414
+ # ○ OBJECTPROTECT Embedded objects: 1 = protected (➜ 5.72)
415
+ # ○ PASSWORD Hash value of the password; 0 = No password (➜ 5.76)
416
+ write_password workbook, buffer1
417
+ # ○ BACKUP ➜ 5.5
418
+ # ○ HIDEOBJ ➜ 5.56
419
+ # ● WINDOW1 ➜ 5.109
420
+ write_window1 workbook, buffer1
421
+ # ○ DATEMODE ➜ 5.28
422
+ write_datemode workbook, buffer1
423
+ # ○ PRECISION ➜ 5.79
424
+ write_precision workbook, buffer1
425
+ # ○ REFRESHALL
426
+ write_refreshall workbook, buffer1
427
+ # ○ BOOKBOOL ➜ 5.9
428
+ write_bookbool workbook, buffer1
429
+ # ●● FONT ➜ 5.45
430
+ write_fonts workbook, buffer1
431
+ # ○○ FORMAT ➜ 5.49
432
+ write_formats workbook, buffer1
433
+ # ●● XF ➜ 5.115
434
+ write_xfs workbook, buffer1
435
+ # ●● STYLE ➜ 5.103
436
+ write_styles workbook, buffer1
437
+ # ○ PALETTE ➜ 5.74
438
+ write_palette workbook, buffer1
439
+ # ○ USESELFS ➜ 5.106
440
+ buffer1.rewind
441
+ # ●● BOUNDSHEET ➜ 5.95
442
+ buffer2 = StringIO.new ''
443
+ # ○ COUNTRY ➜ 5.22
444
+ # ○ Link Table ➜ 4.10.3
445
+ # ○○ NAME ➜ 6.66
446
+ # ○ Shared String Table ➜ 4.11
447
+ # ● SST ➜ 5.100
448
+ # ● EXTSST ➜ 5.42
449
+ write_sst workbook, buffer2, buffer1.size
450
+ # ● EOF ➜ 5.37
451
+ write_eof workbook, buffer2
452
+ buffer2.rewind
453
+ # worksheet data can only be assembled after write_sst
454
+ sheets.each do |worksheet| worksheet.write_from_scratch end
455
+ Ole::Storage.open io do |ole|
456
+ ole.file.open 'Workbook', 'w' do |writer|
457
+ writer.write buffer1.read
458
+ write_boundsheets workbook, writer, buffer1.size + buffer2.size
459
+ writer.write buffer2.read
460
+ sheets.each do |worksheet|
461
+ writer.write worksheet.data
462
+ end
463
+ end
464
+ end
465
+ end
466
+ def write_op writer, op, *args
467
+ data = args.join
468
+ limited = data.slice!(0...@recordsize_limit)
469
+ writer.write [op,limited.size].pack("v2")
470
+ writer.write limited
471
+ data
472
+ end
473
+ def write_password workbook, writer
474
+ write_placeholder writer, 0x0013
475
+ end
476
+ def write_placeholder writer, op, value=0x0000, fmt='v'
477
+ write_op writer, op, [value].pack(fmt)
478
+ end
479
+ def write_precision workbook, writer
480
+ # 0 = Use displayed values; 1 = Use real cell values
481
+ write_placeholder writer, 0x000e, 0x0001
482
+ end
483
+ def write_protect workbook, writer
484
+ write_placeholder writer, 0x0012
485
+ end
486
+ def write_refreshall workbook, writer
487
+ write_placeholder writer, 0x01b7
488
+ end
489
+ def write_sst workbook, writer, offset
490
+ # Offset Size Contents
491
+ # 0 4 Total number of strings in the workbook (see below)
492
+ # 4 4 Number of following strings (nm)
493
+ # 8 var. List of nm Unicode strings, 16-bit string length (➜ 3.4)
494
+ num_total = 0
495
+ strings = worksheets(workbook).inject(Hash.new(0)) do |memo, worksheet|
496
+ worksheet.strings.each do |k,v|
497
+ memo[k] += v
498
+ num_total += v
499
+ end
500
+ memo
501
+ end
502
+ _write_sst workbook, writer, offset, num_total, strings.keys
503
+ end
504
+ def _write_sst workbook, writer, offset, total, strings
505
+ sst = {}
506
+ worksheets(workbook).each do |worksheet|
507
+ offset += worksheet.boundsheet_size
508
+ @sst[worksheet] = sst
509
+ end
510
+ sst_size = strings.size
511
+ data = [total, sst_size].pack 'V2'
512
+ op = 0x00fc
513
+ wide = 0
514
+ offsets = []
515
+ strings.each_with_index do |string, idx|
516
+ sst.store string, idx
517
+ op_offset = data.size + 4
518
+ if idx % SST_CHUNKSIZE == 0
519
+ offsets.push [offset + writer.pos + op_offset, op_offset]
520
+ end
521
+ header, packed, next_wide = _unicode_string string, 2
522
+ # the first few bytes (header + first character) must not be split
523
+ must_fit = header.size + wide + 1
524
+ while data.size + must_fit > @recordsize_limit
525
+ op, data, wide = write_string_part writer, op, data, wide
526
+ end
527
+ wide = next_wide
528
+ data << header << packed
529
+ end
530
+ until data.empty?
531
+ op, data, wide = write_string_part writer, op, data, wide
532
+ end
533
+ write_extsst workbook, offsets, writer
534
+ end
535
+ def write_sst_changes workbook, writer, offset, total, strings
536
+ _write_sst workbook, writer, offset, total, strings
537
+ end
538
+ def write_string_part writer, op, data, wide
539
+ bef = data.size
540
+ ## if we're writing wide characters, we need to make sure we don't cut
541
+ # characters in half
542
+ if wide > 0 && data.size > @recordsize_limit
543
+ remove = @recordsize_limit - bef
544
+ remove -= remove % 2
545
+ rest = data.slice!(remove..-1)
546
+ write_op writer, op, data
547
+ data = rest
548
+ else
549
+ data = write_op writer, op, data
550
+ end
551
+ op = 0x003c
552
+ # Unicode strings are split in a special way. At the beginning of each
553
+ # CONTINUE record the option flags byte is repeated. Only the
554
+ # character size flag will be set in this flags byte, the Rich-Text
555
+ # flag and the Far-East flag are set to zero.
556
+ unless data.empty?
557
+ if wide == 1
558
+ # check if we can compress the rest of the string
559
+ data, wide = compress_unicode_string data
560
+ end
561
+ data = [wide].pack('C') << data
562
+ end
563
+ [op, data, wide]
564
+ end
565
+ def write_styles workbook, writer
566
+ # TODO: Style implementation. The following is simply a standard builtin
567
+ # style.
568
+ # TODO: User defined styles
569
+ data = [
570
+ 0x8000, # Bit Mask Contents
571
+ # 11- 0 0x0fff Index to style XF record (➜ 6.115)
572
+ # 15 0x8000 Always 1 for built-in styles
573
+ 0x00, # Identifier of the built-in cell style:
574
+ # 0x00 = Normal
575
+ # 0x01 = RowLevel_lv (see next field)
576
+ # 0x02 = ColLevel_lv (see next field)
577
+ # 0x03 = Comma
578
+ # 0x04 = Currency
579
+ # 0x05 = Percent
580
+ # 0x06 = Comma [0] (BIFF4-BIFF8)
581
+ # 0x07 = Currency [0] (BIFF4-BIFF8)
582
+ # 0x08 = Hyperlink (BIFF8)
583
+ # 0x09 = Followed Hyperlink (BIFF8)
584
+ 0xff, # Level for RowLevel or ColLevel style (zero-based, lv),
585
+ # 0xff otherwise
586
+ # The RowLevel and ColLevel styles specify the formatting of
587
+ # subtotal cells in a specific outline level. The level is
588
+ # specified by the last field in the STYLE record. Valid values
589
+ # are 0…6 for the outline levels 1…7.
590
+ ]
591
+ write_op writer, 0x0293, data.pack('vC2')
592
+ end
593
+ def write_palette workbook, writer
594
+ data = default_palette
595
+
596
+ workbook.palette.each do |idx, color|
597
+ idx = SEDOC_ROLOC[idx] - 8 if idx.kind_of? Symbol
598
+ raise "Undefined color index: #{idx}" unless data[idx]
599
+ data[idx] = color
600
+ end
601
+
602
+ writer.write [opcode(:palette), 2 + 4 * data.size, data.size].pack('v3')
603
+ writer.write data.collect { |c| c.push(0).pack('C4') }.join
604
+ end
605
+ def write_tabid workbook, writer
606
+ write_op writer, 0x013d, [1].pack('v')
607
+ end
608
+ def write_window1 workbook, writer
609
+ selected = workbook.worksheets.find do |sheet| sheet.selected end
610
+ actidx = workbook.worksheets.index selected
611
+ data = [
612
+ 0x0000, # Horizontal position of the document window
613
+ # (in twips = 1/20 of a point)
614
+ 0x0000, # Vertical position of the document window
615
+ # (in twips = 1/20 of a point)
616
+ 0x4000, # Width of the document window (in twips = 1/20 of a point)
617
+ 0x2000, # Height of the document window (in twips = 1/20 of a point)
618
+ 0x0038, # Option flags:
619
+ # Bit Mask Contents
620
+ # 0 0x0001 0 = Window is visible
621
+ # 1 = Window is hidden
622
+ # 1 0x0002 0 = Window is open
623
+ # 1 = Window is minimised
624
+ # 3 0x0008 0 = Horizontal scroll bar hidden
625
+ # 1 = Horizontal scroll bar visible
626
+ # 4 0x0010 0 = Vertical scroll bar hidden
627
+ # 1 = Vertical scroll bar visible
628
+ # 5 0x0020 0 = Worksheet tab bar hidden
629
+ # 1 = Worksheet tab bar visible
630
+ actidx, # Index to active (displayed) worksheet
631
+ 0x0000, # Index of first visible tab in the worksheet tab bar
632
+ 0x0001, # Number of selected worksheets
633
+ # (highlighted in the worksheet tab bar)
634
+ 0x00e5, # Width of worksheet tab bar (in 1/1000 of window width).
635
+ # The remaining space is used by the horizontal scrollbar.
636
+ ]
637
+ write_op writer, 0x003d, data.pack('v*')
638
+ end
639
+ ##
640
+ # The main writer method. Calls #write_from_scratch or #write_changes
641
+ # depending on the class and state of _workbook_.
642
+ def write_workbook workbook, io
643
+ unless workbook.is_a?(Excel::Workbook) && workbook.io
644
+ @date_base = Date.new 1899, 12, 31
645
+ write_from_scratch workbook, io
646
+ else
647
+ @date_base = workbook.date_base
648
+ if workbook.changes.empty?
649
+ super
650
+ else
651
+ write_changes workbook, io
652
+ end
653
+ end
654
+ ensure
655
+ cleanup workbook
656
+ end
657
+ def write_xfs workbook, writer
658
+ # The default cell format is always present in an Excel file, described by
659
+ # the XF record with the fixed index 15 (0-based). By default, it uses the
660
+ # worksheet/workbook default cell style, described by the very first XF
661
+ # record (index 0).
662
+ @formats[workbook][:writers].each do |fmt| fmt.write_xf writer end
663
+ end
664
+ def sst_index worksheet, str
665
+ @sst[worksheet][str]
666
+ end
667
+ def xf_index workbook, format
668
+ @formats[workbook][:xf_indexes][format] || 0
669
+ end
670
+ ##
671
+ # Returns Excel 97+ default colour palette.
672
+ def default_palette
673
+ [
674
+ [0x00, 0x00, 0x00],
675
+ [0xff, 0xff, 0xff],
676
+ [0xff, 0x00, 0x00],
677
+ [0x00, 0xff, 0x00],
678
+ [0x00, 0x00, 0xff],
679
+ [0xff, 0xff, 0x00],
680
+ [0xff, 0x00, 0xff],
681
+ [0x00, 0xff, 0xff],
682
+ [0x80, 0x00, 0x00],
683
+ [0x00, 0x80, 0x00],
684
+ [0x00, 0x00, 0x80],
685
+ [0x80, 0x80, 0x00],
686
+ [0x80, 0x00, 0x80],
687
+ [0x00, 0x80, 0x80],
688
+ [0xc0, 0xc0, 0xc0],
689
+ [0x80, 0x80, 0x80],
690
+ [0x99, 0x99, 0xff],
691
+ [0x99, 0x33, 0x66],
692
+ [0xff, 0xff, 0xcc],
693
+ [0xcc, 0xff, 0xff],
694
+ [0x66, 0x00, 0x66],
695
+ [0xff, 0x80, 0x80],
696
+ [0x00, 0x66, 0xcc],
697
+ [0xcc, 0xcc, 0xff],
698
+ [0x00, 0x00, 0x80],
699
+ [0xff, 0x00, 0xff],
700
+ [0xff, 0xff, 0x00],
701
+ [0x00, 0xff, 0xff],
702
+ [0x80, 0x00, 0x80],
703
+ [0x80, 0x00, 0x00],
704
+ [0x00, 0x80, 0x80],
705
+ [0x00, 0x00, 0xff],
706
+ [0x00, 0xcc, 0xff],
707
+ [0xcc, 0xff, 0xff],
708
+ [0xcc, 0xff, 0xcc],
709
+ [0xff, 0xff, 0x99],
710
+ [0x99, 0xcc, 0xff],
711
+ [0xff, 0x99, 0xcc],
712
+ [0xcc, 0x99, 0xff],
713
+ [0xff, 0xcc, 0x99],
714
+ [0x33, 0x66, 0xff],
715
+ [0x33, 0xcc, 0xcc],
716
+ [0x99, 0xcc, 0x00],
717
+ [0xff, 0xcc, 0x00],
718
+ [0xff, 0x99, 0x00],
719
+ [0xff, 0x66, 0x00],
720
+ [0x66, 0x66, 0x99],
721
+ [0x96, 0x96, 0x96],
722
+ [0x00, 0x33, 0x66],
723
+ [0x33, 0x99, 0x66],
724
+ [0x00, 0x33, 0x00],
725
+ [0x33, 0x33, 0x00],
726
+ [0x99, 0x33, 0x00],
727
+ [0x99, 0x33, 0x66],
728
+ [0x33, 0x33, 0x99],
729
+ [0x33, 0x33, 0x33]
730
+ ]
731
+ end
732
+ end
733
+ end
734
+ end
735
+ end