keeguon-spreadsheet 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
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