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,940 @@
1
+ require 'stringio'
2
+ require 'spreadsheet/excel/writer/biff8'
3
+ require 'spreadsheet/excel/internals'
4
+ require 'spreadsheet/excel/internals/biff8'
5
+ require 'bigdecimal'
6
+
7
+ module Spreadsheet
8
+ module Excel
9
+ module Writer
10
+ ##
11
+ # Writer class for Excel Worksheets. Most write_* method correspond to an
12
+ # Excel-Record/Opcode. You should not need to call any of its methods directly.
13
+ # If you think you do, look at #write_worksheet
14
+ class Worksheet
15
+ include Spreadsheet::Excel::Writer::Biff8
16
+ include Spreadsheet::Excel::Internals
17
+ include Spreadsheet::Excel::Internals::Biff8
18
+ attr_reader :worksheet
19
+ def initialize workbook, worksheet
20
+ @workbook = workbook
21
+ @worksheet = worksheet
22
+ @io = StringIO.new ''
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
+ end
36
+ ##
37
+ # The number of bytes needed to write a Boundsheet record for this Worksheet
38
+ # Used by Writer::Worksheet to calculate various offsets.
39
+ def boundsheet_size
40
+ name.size + 10
41
+ end
42
+ def data
43
+ @io.rewind
44
+ @io.read
45
+ end
46
+ def encode_date date
47
+ return date if date.is_a? Numeric
48
+ if date.is_a? Time
49
+ date = DateTime.new date.year, date.month, date.day,
50
+ date.hour, date.min, date.sec
51
+ end
52
+ base = @workbook.date_base
53
+ value = date - base
54
+ if LEAP_ERROR > base
55
+ value += 1
56
+ end
57
+ value
58
+ end
59
+ def encode_rk value
60
+ # Bit Mask Contents
61
+ # 0 0x00000001 0 = Value not changed 1 = Value is multiplied by 100
62
+ # 1 0x00000002 0 = Floating-point value 1 = Signed integer value
63
+ # 31-2 0xFFFFFFFC Encoded value
64
+ cent = 0
65
+ int = 2
66
+ higher = value * 100
67
+ if (higher.is_a?(BigDecimal) or higher.is_a?(Float)) && higher < 0xfffffffc
68
+ cent = 1
69
+ if higher == higher.to_i
70
+ value = higher.to_i
71
+ else
72
+ value = higher
73
+ end
74
+ end
75
+ if value.is_a?(Integer)
76
+ ## although not documented as signed, 'V' appears to correctly pack
77
+ # negative numbers.
78
+ value <<= 2
79
+ else
80
+ # FIXME: precision of small numbers
81
+ int = 0
82
+ value, = [value].pack(EIGHT_BYTE_DOUBLE).unpack('x4V')
83
+ value &= 0xfffffffc
84
+ end
85
+ value | cent | int
86
+ end
87
+ def name
88
+ unicode_string @worksheet.name
89
+ end
90
+ def need_number? cell
91
+ if cell.is_a?(Numeric) && cell.abs > 0x1fffffff
92
+ true
93
+ elsif (cell.is_a?(BigDecimal) or cell.is_a?(Float)) and not cell.nan?
94
+ higher = cell * 100
95
+ if higher == higher.to_i
96
+ need_number? higher.to_i
97
+ else
98
+ test1, test2 = [cell * 100].pack(EIGHT_BYTE_DOUBLE).unpack('V2')
99
+ test1 > 0 || need_number?(test2)
100
+ end
101
+ else
102
+ false
103
+ end
104
+ end
105
+ def row_blocks
106
+ # All cells in an Excel document are divided into blocks of 32 consecutive
107
+ # rows, called Row Blocks. The first Row Block starts with the first used
108
+ # row in that sheet. Inside each Row Block there will occur ROW records
109
+ # describing the properties of the rows, and cell records with all the cell
110
+ # contents in this Row Block.
111
+ blocks = []
112
+ @worksheet.reject do |row| row.empty? end.each_with_index do |row, idx|
113
+ blocks << [] if idx % 32 == 0
114
+ blocks.last << row
115
+ end
116
+ blocks
117
+ end
118
+ def size
119
+ @io.size
120
+ end
121
+ def strings
122
+ @worksheet.inject(Hash.new(0)) do |memo, row|
123
+ row.each do |cell|
124
+ memo[cell] += 1 if (cell.is_a?(String) && !cell.empty?)
125
+ end
126
+ memo
127
+ end
128
+ end
129
+ ##
130
+ # Write a blank cell
131
+ def write_blank row, idx
132
+ write_cell :blank, row, idx
133
+ end
134
+ def write_bof
135
+ data = [
136
+ @biff_version, # BIFF version (always 0x0600 for BIFF8)
137
+ 0x0010, # Type of the following data:
138
+ # 0x0005 = Workbook globals
139
+ # 0x0006 = Visual Basic module
140
+ # 0x0010 = Worksheet
141
+ # 0x0020 = Chart
142
+ # 0x0040 = Macro sheet
143
+ # 0x0100 = Workspace file
144
+ @build_id, # Build identifier
145
+ @build_year, # Build year
146
+ 0x000, # File history flags
147
+ 0x006, # Lowest Excel version that can read
148
+ # all records in this file
149
+ ]
150
+ write_op @bof, data.pack("v4V2")
151
+ end
152
+ ##
153
+ # Write a cell with a Boolean or Error value
154
+ def write_boolerr row, idx
155
+ value = row[idx]
156
+ type = 0
157
+ numval = 0
158
+ if value.is_a? Error
159
+ type = 1
160
+ numval = value.code
161
+ elsif value
162
+ numval = 1
163
+ end
164
+ data = [
165
+ numval, # Boolean or error value (type depends on the following byte)
166
+ type # 0 = Boolean value; 1 = Error code
167
+ ]
168
+ write_cell :boolerr, row, idx, *data
169
+ end
170
+ def write_calccount
171
+ count = 100 # Maximum number of iterations allowed in circular references
172
+ write_op 0x000c, [count].pack('v')
173
+ end
174
+ def write_cell type, row, idx, *args
175
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx)
176
+ data = [
177
+ row.idx, # Index to row
178
+ idx, # Index to column
179
+ xf_idx, # Index to XF record (➜ 6.115)
180
+ ].concat args
181
+ write_op opcode(type), data.pack(binfmt(type))
182
+ end
183
+ def write_cellblocks row
184
+ # BLANK ➜ 6.7
185
+ # BOOLERR ➜ 6.10
186
+ # INTEGER ➜ 6.56 (BIFF2 only)
187
+ # LABEL ➜ 6.59 (BIFF2-BIFF7)
188
+ # LABELSST ➜ 6.61 (BIFF8 only)
189
+ # MULBLANK ➜ 6.64 (BIFF5-BIFF8)
190
+ # MULRK ➜ 6.65 (BIFF5-BIFF8)
191
+ # NUMBER ➜ 6.68
192
+ # RK ➜ 6.82 (BIFF3-BIFF8)
193
+ # RSTRING ➜ 6.84 (BIFF5/BIFF7)
194
+ multiples, first_idx = nil
195
+ row = row.formatted
196
+ row.each_with_index do |cell, idx|
197
+ cell = nil if cell == ''
198
+ ## it appears that there are limitations to RK precision, both for
199
+ # Integers and Floats, that lie well below 2^30 significant bits, or
200
+ # Ruby's Bignum threshold. In that case we'll just write a Number
201
+ # record
202
+ need_number = need_number? cell
203
+ if multiples && (!multiples.last.is_a?(cell.class) || need_number)
204
+ write_multiples row, first_idx, multiples
205
+ multiples, first_idx = nil
206
+ end
207
+ nxt = idx + 1
208
+ case cell
209
+ when NilClass
210
+ if multiples
211
+ multiples.push cell
212
+ elsif nxt < row.size && row[nxt].nil?
213
+ multiples = [cell]
214
+ first_idx = idx
215
+ else
216
+ write_blank row, idx
217
+ end
218
+ when TrueClass, FalseClass, Error
219
+ write_boolerr row, idx
220
+ when String
221
+ write_labelsst row, idx
222
+ when Numeric
223
+ ## RK encodes Floats with 30 significant bits, which is a bit more than
224
+ # 10^9. Not sure what is a good rule of thumb here, but it seems that
225
+ # Decimal Numbers with more than 4 significant digits are not represented
226
+ # with sufficient precision by RK
227
+ if need_number
228
+ write_number row, idx
229
+ elsif multiples
230
+ multiples.push cell
231
+ elsif nxt < row.size && row[nxt].is_a?(Numeric)
232
+ multiples = [cell]
233
+ first_idx = idx
234
+ else
235
+ write_rk row, idx
236
+ end
237
+ when Formula
238
+ write_formula row, idx
239
+ when Date, Time
240
+ write_number row, idx
241
+ end
242
+ end
243
+ write_multiples row, first_idx, multiples if multiples
244
+ end
245
+ def write_changes reader, endpos, sst_status
246
+
247
+ ## FIXME this is not smart solution to update outline_level.
248
+ # without this process, outlines in row disappear in MS Excel.
249
+ @worksheet.row_count.times do |i|
250
+ if @worksheet.row(i).outline_level > 0
251
+ @worksheet.row(i).outline_level = @worksheet.row(i).outline_level
252
+ end
253
+ end
254
+
255
+ reader.seek @worksheet.offset
256
+ blocks = row_blocks
257
+ lastpos = reader.pos
258
+ offsets = {}
259
+ row_offsets = []
260
+ changes = @worksheet.changes
261
+ @worksheet.offsets.each do |key, pair|
262
+ if changes.include?(key) \
263
+ || (sst_status == :complete_update && key.is_a?(Integer))
264
+ offsets.store pair, key
265
+ end
266
+ end
267
+ ## FIXME it may be smarter to simply write all rowblocks, instead of doing a
268
+ # song-and-dance routine for every row...
269
+ work = offsets.invert
270
+ work.each do |key, (pos, len)|
271
+ case key
272
+ when Integer
273
+ row_offsets.push [key, [pos, len]]
274
+ when :dimensions
275
+ row_offsets.push [-1, [pos, len]]
276
+ end
277
+ end
278
+ row_offsets.sort!
279
+ row_offsets.reverse!
280
+ control = changes.size
281
+ @worksheet.each do |row|
282
+ key = row.idx
283
+ if changes.include?(key) && !work.include?(key)
284
+ row, pair = row_offsets.find do |idx, _| idx <= key end
285
+ work.store key, pair
286
+ end
287
+ end
288
+ if changes.size > control
289
+ warn <<-EOS
290
+ Your Worksheet was modified while it was being written. This should not happen.
291
+ Please contact the author (hannes dot wyss at gmail dot com) with a sample file
292
+ and minimal code that generates this warning. Thanks!
293
+ EOS
294
+ end
295
+ work = work.sort_by do |key, (pos, _)|
296
+ [pos, key.is_a?(Integer) ? key : -1]
297
+ end
298
+ work.each do |key, (pos, len)|
299
+ @io.write reader.read(pos - lastpos) if pos > lastpos
300
+ if key.is_a?(Integer)
301
+ if block = blocks.find do |rows| rows.any? do |row| row.idx == key end end
302
+ write_rowblock block
303
+ blocks.delete block
304
+ end
305
+ else
306
+ send "write_#{key}"
307
+ end
308
+ lastpos = pos + len
309
+ reader.seek lastpos
310
+ end
311
+
312
+ # Necessary for outline (grouping) and hiding functions
313
+ # but these below are not necessary to run
314
+ # if [Row|Column]#hidden? = false and [Row|Column]#outline_level == 0
315
+ write_merged_cells
316
+ write_pagesetup
317
+ write_margins
318
+ write_colinfos
319
+ write_guts
320
+
321
+ @io.write reader.read(endpos - lastpos)
322
+ end
323
+ def write_colinfo bunch
324
+ col = bunch.first
325
+ width = col.width.to_f * 256
326
+ xf_idx = @workbook.xf_index @worksheet.workbook, col.default_format
327
+ opts = 0
328
+ opts |= 0x0001 if col.hidden?
329
+ opts |= col.outline_level.to_i << 8
330
+ opts |= 0x1000 if col.collapsed?
331
+ data = [
332
+ col.idx, # Index to first column in the range
333
+ bunch.last.idx, # Index to last column in the range
334
+ width.to_i, # Width of the columns in 1/256 of the width of the zero
335
+ # character, using default font (first FONT record in the
336
+ # file)
337
+ xf_idx.to_i, # Index to XF record (➜ 6.115) for default column formatting
338
+ opts, # Option flags:
339
+ # Bits Mask Contents
340
+ # 0 0x0001 1 = Columns are hidden
341
+ # 10-8 0x0700 Outline level of the columns
342
+ # (0 = no outline)
343
+ # 12 0x1000 1 = Columns are collapsed
344
+ ]
345
+ write_op opcode(:colinfo), data.pack(binfmt(:colinfo))
346
+ end
347
+ def write_colinfos
348
+ cols = @worksheet.columns
349
+ bunch = []
350
+ cols.each_with_index do |column, idx|
351
+ if column
352
+ bunch << column
353
+ if cols[idx.next] != column
354
+ write_colinfo bunch
355
+ bunch.clear
356
+ end
357
+ end
358
+ end
359
+ end
360
+ def write_defaultrowheight
361
+ data = [
362
+ 0x00, # Option flags:
363
+ # Bit Mask Contents
364
+ # 0 0x01 1 = Row height and default font height do not match
365
+ # 1 0x02 1 = Row is hidden
366
+ # 2 0x04 1 = Additional space above the row
367
+ # 3 0x08 1 = Additional space below the row
368
+ 0xf2, # Default height for unused rows, in twips = 1/20 of a point
369
+ ]
370
+ write_op 0x0225, data.pack('v2')
371
+ end
372
+ def write_defcolwidth
373
+ # Offset Size Contents
374
+ # 0 2 Column width in characters, using the width of the zero
375
+ # character from default font (first FONT record in the
376
+ # file). Excel adds some extra space to the default width,
377
+ # depending on the default font and default font size. The
378
+ # algorithm how to exactly calculate the resulting column
379
+ # width is not known.
380
+ #
381
+ # Example: The default width of 8 set in this record results
382
+ # in a column width of 8.43 using Arial font with a size of
383
+ # 10 points.
384
+ write_op 0x0055, [8].pack('v')
385
+ end
386
+ def write_dimensions
387
+ # Offset Size Contents
388
+ # 0 4 Index to first used row
389
+ # 4 4 Index to last used row, increased by 1
390
+ # 8 2 Index to first used column
391
+ # 10 2 Index to last used column, increased by 1
392
+ # 12 2 Not used
393
+ write_op 0x0200, @worksheet.dimensions.pack(binfmt(:dimensions))
394
+ end
395
+ def write_eof
396
+ write_op 0x000a
397
+ end
398
+ ##
399
+ # Write a cell with a Formula. May write an additional String record depending
400
+ # on the stored result of the Formula.
401
+ def write_formula row, idx
402
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx)
403
+ cell = row[idx]
404
+ data1 = [
405
+ row.idx, # Index to row
406
+ idx, # Index to column
407
+ xf_idx, # Index to XF record (➜ 6.115)
408
+ ].pack 'v3'
409
+ data2 = nil
410
+ case value = cell.value
411
+ when Numeric # IEEE 754 floating-point value (64-bit double precision)
412
+ data2 = [value].pack EIGHT_BYTE_DOUBLE
413
+ when String
414
+ data2 = [
415
+ 0x00, # (identifier for a string value)
416
+ 0xffff, #
417
+ ].pack 'Cx5v'
418
+ when true, false
419
+ value = value ? 1 : 0
420
+ data2 = [
421
+ 0x01, # (identifier for a Boolean value)
422
+ value, # 0 = FALSE, 1 = TRUE
423
+ 0xffff, #
424
+ ].pack 'CxCx3v'
425
+ when Error
426
+ data2 = [
427
+ 0x02, # (identifier for an error value)
428
+ value.code, # Error code
429
+ 0xffff, #
430
+ ].pack 'CxCx3v'
431
+ when nil
432
+ data2 = [
433
+ 0x03, # (identifier for an empty cell)
434
+ 0xffff, #
435
+ ].pack 'Cx5v'
436
+ else
437
+ data2 = [
438
+ 0x02, # (identifier for an error value)
439
+ 0x2a, # Error code: #N/A! Argument or function not available
440
+ 0xffff, #
441
+ ].pack 'CxCx3v'
442
+ end
443
+ opts = 0x03
444
+ opts |= 0x08 if cell.shared
445
+ data3 = [
446
+ opts # Option flags:
447
+ # Bit Mask Contents
448
+ # 0 0x0001 1 = Recalculate always
449
+ # 1 0x0002 1 = Calculate on open
450
+ # 3 0x0008 1 = Part of a shared formula
451
+ ].pack 'vx4'
452
+ write_op opcode(:formula), data1, data2, data3, cell.data
453
+ if cell.value.is_a?(String)
454
+ write_op opcode(:string), unicode_string(cell.value, 2)
455
+ end
456
+ end
457
+ ##
458
+ # Write a new Worksheet.
459
+ def write_from_scratch
460
+ # ● BOF Type = worksheet (➜ 5.8)
461
+ write_bof
462
+ # ○ UNCALCED ➜ 5.105
463
+ # ○ INDEX ➜ 4.7 (Row Blocks), ➜ 5.59
464
+ # ○ Calculation Settings Block ➜ 4.3
465
+ write_calccount
466
+ write_refmode
467
+ write_iteration
468
+ write_saverecalc
469
+ # ○ PRINTHEADERS ➜ 5.81
470
+ # ○ PRINTGRIDLINES ➜ 5.80
471
+ # ○ GRIDSET ➜ 5.52
472
+ # ○ GUTS ➜ 5.53
473
+ write_guts
474
+ # ○ DEFAULTROWHEIGHT ➜ 5.31
475
+ write_defaultrowheight
476
+ # ○ WSBOOL ➜ 5.113
477
+ write_wsbool
478
+ # ○ Page Settings Block ➜ 4.4
479
+ # ○ Worksheet Protection Block ➜ 4.18
480
+ write_proctection
481
+ # ○ DEFCOLWIDTH ➜ 5.32
482
+ write_defcolwidth
483
+ # ○○ COLINFO ➜ 5.18
484
+ write_colinfos
485
+ # ○ SORT ➜ 5.99
486
+ # ● DIMENSIONS ➜ 5.35
487
+ write_dimensions
488
+ # ○○ Row Blocks ➜ 4.7
489
+ write_rows
490
+ # ● Worksheet View Settings Block ➜ 4.5
491
+ # ● WINDOW2 ➜ 5.110
492
+ write_window2
493
+ # ○ SCL ➜ 5.92 (BIFF4-BIFF8 only)
494
+ # ○ PANE ➜ 5.75
495
+ # ○○ SELECTION ➜ 5.93
496
+ # ○ STANDARDWIDTH ➜ 5.101
497
+ # ○○ MERGEDCELLS ➜ 5.67
498
+ write_merged_cells
499
+ # ○ LABELRANGES ➜ 5.64
500
+ # ○ PHONETIC ➜ 5.77
501
+ # ○ Conditional Formatting Table ➜ 4.12
502
+ # ○ Hyperlink Table ➜ 4.13
503
+ write_pagesetup
504
+ write_margins
505
+ write_hyperlink_table
506
+ # ○ Data Validity Table ➜ 4.14
507
+ # ○ SHEETLAYOUT ➜ 5.96 (BIFF8X only)
508
+ # ○ SHEETPROTECTION Additional protection, ➜ 5.98 (BIFF8X only)
509
+ # ○ RANGEPROTECTION Additional protection, ➜ 5.84 (BIFF8X only)
510
+ # ● EOF ➜ 5.36
511
+ write_eof
512
+ end
513
+ ##
514
+ # Write record that contains information about the layout of outline symbols.
515
+ def write_guts
516
+ # find the maximum outline_level in rows and columns
517
+ row_outline_level = 0
518
+ col_outline_level = 0
519
+ if(row = @worksheet.rows.select{|x| x!=nil}.max{|a,b| a.outline_level <=> b.outline_level})
520
+ row_outline_level = row.outline_level
521
+ end
522
+ if(col = @worksheet.columns.select{|x| x!=nil}.max{|a,b| a.outline_level <=> b.outline_level})
523
+ col_outline_level = col.outline_level
524
+ end
525
+ # set data
526
+ data = [
527
+ 0, # Width of the area to display row outlines (left of the sheet), in pixel
528
+ 0, # Height of the area to display column outlines (above the sheet), in pixel
529
+ row_outline_level+1, # Number of visible row outline levels (used row levels+1; or 0,if not used)
530
+ col_outline_level+1 # Number of visible column outline levels (used column levels+1; or 0,if not used)
531
+ ]
532
+ # write record
533
+ write_op opcode(:guts), data.pack('v4')
534
+ end
535
+ def write_hlink row, col, link
536
+ # FIXME: only Hyperlinks are supported at present.
537
+ cell_range = [
538
+ row, row, # Cell range address of all cells containing this hyperlink
539
+ col, col, # (➜ 3.13.1)
540
+ ].pack 'v4'
541
+ guid = [
542
+ # GUID of StdLink:
543
+ # D0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B
544
+ # (79EAC9D0-BAF9-11CE-8C82-00AA004BA90B)
545
+ "d0c9ea79f9bace118c8200aa004ba90b",
546
+ ].pack 'H32'
547
+ opts = 0x01
548
+ opts |= 0x02
549
+ opts |= 0x14 unless link == link.url
550
+ opts |= 0x08 if link.fragment
551
+ opts |= 0x80 if link.target_frame
552
+ # TODO: UNC support
553
+ options = [
554
+ 2, # Unknown value: 0x00000002
555
+ opts, # Option flags
556
+ # Bit Mask Contents
557
+ # 0 0x00000001 0 = No link extant
558
+ # 1 = File link or URL
559
+ # 1 0x00000002 0 = Relative file path
560
+ # 1 = Absolute path or URL
561
+ # 2 and 4 0x00000014 0 = No description
562
+ # 1 (both bits) = Description
563
+ # 3 0x00000008 0 = No text mark
564
+ # 1 = Text mark
565
+ # 7 0x00000080 0 = No target frame
566
+ # 1 = Target frame
567
+ # 8 0x00000100 0 = File link or URL
568
+ # 1 = UNC path (incl. server name)
569
+
570
+ ].pack('V2')
571
+ tail = []
572
+ ## call internal to get the correct internal encoding in Ruby 1.9
573
+ nullstr = internal "\000"
574
+ unless link == link.url
575
+ desc = internal(link).dup << nullstr
576
+ tail.push [desc.size / 2].pack('V'), desc
577
+ end
578
+ if link.target_frame
579
+ frme = internal(link.target_frame).dup << nullstr
580
+ tail.push [frme.size / 2].pack('V'), frme
581
+ end
582
+ url = internal(link.url).dup << nullstr
583
+ tail.push [
584
+ # 6.53.2 Hyperlink containing a URL (Uniform Resource Locator)
585
+ # These data fields occur for links which are not local files or files
586
+ # in the local network (for instance HTTP and FTP links and e-mail
587
+ # addresses). The lower 9 bits of the option flags field must be
588
+ # 0.x00x.xx112 (x means optional, depending on hyperlink content). The
589
+ # GUID could be used to distinguish a URL from a file link.
590
+ # GUID of URL Moniker:
591
+ # E0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B
592
+ # (79EAC9E0-BAF9-11CE-8C82-00AA004BA90B)
593
+ 'e0c9ea79f9bace118c8200aa004ba90b',
594
+ url.size # Size of character array of the URL, including trailing zero
595
+ # word (us). There are us/2-1 characters in the following
596
+ # string.
597
+ ].pack('H32V'), url
598
+ if link.fragment
599
+ frag = internal(link.fragment).dup << nullstr
600
+ tail.push [frag.size / 2].pack('V'), frag
601
+ end
602
+ write_op opcode(:hlink), cell_range, guid, options, *tail
603
+ end
604
+ def write_hyperlink_table
605
+ # TODO: theoretically it's possible to write fewer records by combining
606
+ # identical neighboring links in cell-ranges
607
+ @worksheet.each do |row|
608
+ row.each_with_index do |cell, idx|
609
+ if cell.is_a? Link
610
+ write_hlink row.idx, idx, cell
611
+ end
612
+ end
613
+ end
614
+ end
615
+ def write_iteration
616
+ its = 0 # 0 = Iterations off; 1 = Iterations on
617
+ write_op 0x0011, [its].pack('v')
618
+ end
619
+ ##
620
+ # Write a cell with a String value. The String must have been stored in the
621
+ # Shared String Table.
622
+ def write_labelsst row, idx
623
+ write_cell :labelsst, row, idx, @workbook.sst_index(self, row[idx])
624
+ end
625
+ ##
626
+ # Write multiple consecutive blank cells.
627
+ def write_mulblank row, idx, multiples
628
+ data = [
629
+ row.idx, # Index to row
630
+ idx, # Index to first column (fc)
631
+ ]
632
+ # List of nc=lc-fc+1 16-bit indexes to XF records (➜ 6.115)
633
+ multiples.each_with_index do |blank, cell_idx|
634
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx + cell_idx)
635
+ data.push xf_idx
636
+ end
637
+ # Index to last column (lc)
638
+ data.push idx + multiples.size - 1
639
+ write_op opcode(:mulblank), data.pack('v*')
640
+ end
641
+ ##
642
+ # Write multiple consecutive cells with RK values (see #write_rk)
643
+ def write_mulrk row, idx, multiples
644
+ fmt = 'v2'
645
+ data = [
646
+ row.idx, # Index to row
647
+ idx, # Index to first column (fc)
648
+ ]
649
+ # List of nc=lc-fc+1 16-bit indexes to XF records (➜ 6.115)
650
+ multiples.each_with_index do |cell, cell_idx|
651
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx + cell_idx)
652
+ data.push xf_idx, encode_rk(cell)
653
+ fmt << 'vV'
654
+ end
655
+ # Index to last column (lc)
656
+ data.push idx + multiples.size - 1
657
+ write_op opcode(:mulrk), data.pack(fmt << 'v')
658
+ end
659
+ def write_multiples row, idx, multiples
660
+ case multiples.last
661
+ when NilClass
662
+ write_mulblank row, idx, multiples
663
+ when Numeric
664
+ if multiples.size > 1
665
+ write_mulrk row, idx, multiples
666
+ else
667
+ write_rk row, idx
668
+ end
669
+ end
670
+ end
671
+ ##
672
+ # Write a cell with a 64-bit double precision Float value
673
+ def write_number row, idx
674
+ # Offset Size Contents
675
+ # 0 2 Index to row
676
+ # 2 2 Index to column
677
+ # 4 2 Index to XF record (➜ 6.115)
678
+ # 6 8 IEEE 754 floating-point value (64-bit double precision)
679
+ value = row[idx]
680
+ case value
681
+ when Date, Time
682
+ value = encode_date(value)
683
+ end
684
+ write_cell :number, row, idx, value
685
+ end
686
+ def write_op op, *args
687
+ data = args.join
688
+ @io.write [op,data.size].pack("v2")
689
+ @io.write data
690
+ end
691
+ def write_refmode
692
+ # • The “RC” mode uses numeric indexes for rows and columns, for example
693
+ # “R(1)C(-1)”, or “R1C1:R2C2”.
694
+ # • The “A1” mode uses characters for columns and numbers for rows, for
695
+ # example “B1”, or “$A$1:$B$2”.
696
+ mode = 1 # 0 = RC mode; 1 = A1 mode
697
+ write_op 0x000f, [mode].pack('v')
698
+ end
699
+ ##
700
+ # Write a cell with a Numeric or Date value.
701
+ def write_rk row, idx
702
+ write_cell :rk, row, idx, encode_rk(row[idx])
703
+ end
704
+ def write_row row
705
+ # Offset Size Contents
706
+ # 0 2 Index of this row
707
+ # 2 2 Index to column of the first cell which
708
+ # is described by a cell record
709
+ # 4 2 Index to column of the last cell which is
710
+ # described by a cell record, increased by 1
711
+ # 6 2 Bit Mask Contents
712
+ # 14-0 0x7fff Height of the row, in twips = 1/20 of a point
713
+ # 15 0x8000 0 = Row has custom height;
714
+ # 1 = Row has default height
715
+ # 8 2 Not used
716
+ # 10 2 In BIFF3-BIFF4 this field contains a relative offset to
717
+ # calculate stream position of the first cell record for this
718
+ # row (➜ 5.7.1). In BIFF5-BIFF8 this field is not used
719
+ # anymore, but the DBCELL record (➜ 6.26) instead.
720
+ # 12 4 Option flags and default row formatting:
721
+ # Bit Mask Contents
722
+ # 2-0 0x00000007 Outline level of the row
723
+ # 4 0x00000010 1 = Outline group starts or ends here
724
+ # (depending on where the outline
725
+ # buttons are located, see WSBOOL
726
+ # record, ➜ 6.113), and is collapsed
727
+ # 5 0x00000020 1 = Row is hidden (manually, or by a
728
+ # filter or outline group)
729
+ # 6 0x00000040 1 = Row height and default font height
730
+ # do not match
731
+ # 7 0x00000080 1 = Row has explicit default format (fl)
732
+ # 8 0x00000100 Always 1
733
+ # 27-16 0x0fff0000 If fl = 1: Index to default XF record
734
+ # (➜ 6.115)
735
+ # 28 0x10000000 1 = Additional space above the row.
736
+ # This flag is set, if the upper
737
+ # border of at least one cell in this
738
+ # row or if the lower border of at
739
+ # least one cell in the row above is
740
+ # formatted with a thick line style.
741
+ # Thin and medium line styles are not
742
+ # taken into account.
743
+ # 29 0x20000000 1 = Additional space below the row.
744
+ # This flag is set, if the lower
745
+ # border of at least one cell in this
746
+ # row or if the upper border of at
747
+ # least one cell in the row below is
748
+ # formatted with a medium or thick
749
+ # line style. Thin line styles are
750
+ # not taken into account.
751
+ height = row.height || ROW_HEIGHT
752
+ opts = row.outline_level & 0x00000007
753
+ opts |= 0x00000010 if row.collapsed?
754
+ opts |= 0x00000020 if row.hidden?
755
+ opts |= 0x00000040 if height != ROW_HEIGHT
756
+ if fmt = row.default_format
757
+ xf_idx = @workbook.xf_index @worksheet.workbook, fmt
758
+ opts |= 0x00000080
759
+ opts |= xf_idx << 16
760
+ end
761
+ opts |= 0x00000100
762
+ height = if height == ROW_HEIGHT
763
+ (height * TWIPS).to_i | 0x8000
764
+ else
765
+ height * TWIPS
766
+ end
767
+
768
+ attrs = [
769
+ row.idx,
770
+ row.first_used,
771
+ row.first_unused,
772
+ height,
773
+ opts]
774
+
775
+ return if attrs.any?(&:nil?)
776
+
777
+ # TODO: Row spacing
778
+ data = attrs.pack binfmt(:row)
779
+ write_op opcode(:row), data
780
+ end
781
+ def write_rowblock block
782
+ # ●● ROW Properties of the used rows
783
+ # ○○ Cell Block(s) Cell records for all used cells
784
+ # ○ DBCELL Stream offsets to the cell records of each row
785
+ block.each do |row|
786
+ write_row row
787
+ end
788
+ block.each do |row|
789
+ write_cellblocks row
790
+ end
791
+ end
792
+ def write_rows
793
+ row_blocks.each do |block|
794
+ write_rowblock block
795
+ end
796
+ end
797
+ def write_saverecalc
798
+ # 0 = Do not recalculate; 1 = Recalculate before saving the document
799
+ write_op 0x005f, [1].pack('v')
800
+ end
801
+ def write_window2
802
+ # This record contains additional settings for the document window
803
+ # (BIFF2-BIFF4) or for the window of a specific worksheet (BIFF5-BIFF8).
804
+ # It is part of the Sheet View Settings Block (➜ 4.5).
805
+ # Offset Size Contents
806
+ # 0 2 Option flags:
807
+ # Bits Mask Contents
808
+ # 0 0x0001 0 = Show formula results
809
+ # 1 = Show formulas
810
+ # 1 0x0002 0 = Do not show grid lines
811
+ # 1 = Show grid lines
812
+ # 2 0x0004 0 = Do not show sheet headers
813
+ # 1 = Show sheet headers
814
+ # 3 0x0008 0 = Panes are not frozen
815
+ # 1 = Panes are frozen (freeze)
816
+ # 4 0x0010 0 = Show zero values as empty cells
817
+ # 1 = Show zero values
818
+ # 5 0x0020 0 = Manual grid line colour
819
+ # 1 = Automatic grid line colour
820
+ # 6 0x0040 0 = Columns from left to right
821
+ # 1 = Columns from right to left
822
+ # 7 0x0080 0 = Do not show outline symbols
823
+ # 1 = Show outline symbols
824
+ # 8 0x0100 0 = Keep splits if pane freeze is removed
825
+ # 1 = Remove splits if pane freeze is removed
826
+ # 9 0x0200 0 = Sheet not selected
827
+ # 1 = Sheet selected (BIFF5-BIFF8)
828
+ # 10 0x0400 0 = Sheet not active
829
+ # 1 = Sheet active (BIFF5-BIFF8)
830
+ # 11 0x0800 0 = Show in normal view
831
+ # 1 = Show in page break preview (BIFF8)
832
+ # 2 2 Index to first visible row
833
+ # 4 2 Index to first visible column
834
+ # 6 2 Colour index of grid line colour (➜ 5.74).
835
+ # Note that in BIFF2-BIFF5 an RGB colour is written instead.
836
+ # 8 2 Not used
837
+ # 10 2 Cached magnification factor in page break preview (in percent)
838
+ # 0 = Default (60%)
839
+ # 12 2 Cached magnification factor in normal view (in percent)
840
+ # 0 = Default (100%)
841
+ # 14 4 Not used
842
+ flags = 0x0536 # Show grid lines, sheet headers, zero values. Automatic
843
+ # grid line colour, Remove slits if pane freeze is removed,
844
+ # Sheet is active.
845
+ if @worksheet.selected
846
+ flags |= 0x0200
847
+ end
848
+ flags |= 0x0080 # Show outline symbols,
849
+ # but if [Row|Column]#outline_level = 0 the symbols are not shown.
850
+ data = [ flags, 0, 0, 0, 0, 0 ].pack binfmt(:window2)
851
+ write_op opcode(:window2), data
852
+ end
853
+
854
+ def write_merged_cells
855
+ return unless @worksheet.merged_cells.any?
856
+ # FIXME standards say the record is limited by 1027 records at once
857
+ # And no CONTINUE is supported
858
+
859
+ merge_cells = @worksheet.merged_cells.dup
860
+ while (window = merge_cells.slice!(0...1027)).any?
861
+ count = window.size
862
+ data = ([count] + window.flatten).pack('v2v*')
863
+ write_op opcode(:mergedcells), data
864
+ end
865
+ end
866
+
867
+ def write_pagesetup
868
+ return unless @worksheet.pagesetup
869
+ data = @worksheet.pagesetup[:orig_data].dup
870
+ if @worksheet.pagesetup[:orientation]
871
+ data[5] = @worksheet.pagesetup[:orientation] == :landscape ? 0 : 2
872
+ end
873
+
874
+ if @worksheet.pagesetup[:adjust_to]
875
+ data[1] = @worksheet.pagesetup[:adjust_to]
876
+ end
877
+
878
+ write_op opcode(:pagesetup), data.pack(binfmt(:pagesetup))
879
+ end
880
+
881
+ def write_margins
882
+ @worksheet.margins.each do |key, value|
883
+ next unless [:left, :top, :right, :bottom].include?(key)
884
+ write_op opcode(:"#{key}margin"), [value].pack(binfmt(:margin))
885
+ end
886
+ end
887
+
888
+ def write_proctection
889
+ return unless @worksheet.protected?
890
+ # ○ PROTECT Worksheet contents: 1 = protected (➜ 5.82)
891
+ write_op opcode(:protect), [1].pack('v')
892
+ # ○ OBJECTPROTECT Embedded objects: 1 = protected (➜ 5.72)
893
+ # ○ SCENPROTECT Scenarios: 1 = protected (➜ 5.91)
894
+ # ○ PASSWORD Hash value of the password; 0 = no password (➜ 5.76)
895
+ write_op opcode(:password), [@worksheet.password_hash].pack('v')
896
+ end
897
+
898
+ def write_wsbool
899
+ bits = [
900
+ # Bit Mask Contents
901
+ 1, # 0 0x0001 0 = Do not show automatic page breaks
902
+ # 1 = Show automatic page breaks
903
+ 0, # 4 0x0010 0 = Standard sheet
904
+ # 1 = Dialogue sheet (BIFF5-BIFF8)
905
+ 0, # 5 0x0020 0 = No automatic styles in outlines
906
+ # 1 = Apply automatic styles to outlines
907
+ 1, # 6 0x0040 0 = Outline buttons above outline group
908
+ # 1 = Outline buttons below outline group
909
+ 1, # 7 0x0080 0 = Outline buttons left of outline group
910
+ # 1 = Outline buttons right of outline group
911
+ 0, # 8 0x0100 0 = Scale printout in percent (➜ 6.89)
912
+ # 1 = Fit printout to number of pages (➜ 6.89)
913
+ 0, # 9 0x0200 0 = Save external linked values
914
+ # (BIFF3-BIFF4 only, ➜ 5.10)
915
+ # 1 = Do not save external linked values
916
+ # (BIFF3-BIFF4 only, ➜ 5.10)
917
+ 1, # 10 0x0400 0 = Do not show row outline symbols
918
+ # 1 = Show row outline symbols
919
+ 0, # 11 0x0800 0 = Do not show column outline symbols
920
+ # 1 = Show column outline symbols
921
+ 0, # 13-12 0x3000 These flags specify the arrangement of windows.
922
+ # They are stored in BIFF4 only.
923
+ # 00 = Arrange windows tiled
924
+ # 01 = Arrange windows horizontal
925
+ 0, # 10 = Arrange windows vertical
926
+ # 11 = Arrange windows cascaded
927
+ # The following flags are valid for BIFF4-BIFF8 only:
928
+ 0, # 14 0x4000 0 = Standard expression evaluation
929
+ # 1 = Alternative expression evaluation
930
+ 0, # 15 0x8000 0 = Standard formula entries
931
+ # 1 = Alternative formula entries
932
+ ]
933
+ weights = [4,5,6,7,8,9,10,11,12,13,14,15]
934
+ value = bits.inject do |a, b| a | (b << weights.shift) end
935
+ write_op 0x0081, [value].pack('v')
936
+ end
937
+ end
938
+ end
939
+ end
940
+ end