ttb-spreadsheet 0.6.5.8

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