ruby-spreadsheet 0.6.5

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 (64) hide show
  1. data/.document +5 -0
  2. data/GUIDE.txt +267 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +20 -0
  5. data/History.txt +307 -0
  6. data/LICENSE.txt +619 -0
  7. data/README.txt +91 -0
  8. data/Rakefile +53 -0
  9. data/VERSION +1 -0
  10. data/bin/xlsopcodes +18 -0
  11. data/lib/parseexcel.rb +27 -0
  12. data/lib/parseexcel/parseexcel.rb +75 -0
  13. data/lib/parseexcel/parser.rb +11 -0
  14. data/lib/spreadsheet.rb +79 -0
  15. data/lib/spreadsheet/column.rb +71 -0
  16. data/lib/spreadsheet/compatibility.rb +23 -0
  17. data/lib/spreadsheet/datatypes.rb +110 -0
  18. data/lib/spreadsheet/encodings.rb +46 -0
  19. data/lib/spreadsheet/excel.rb +88 -0
  20. data/lib/spreadsheet/excel/error.rb +26 -0
  21. data/lib/spreadsheet/excel/internals.rb +386 -0
  22. data/lib/spreadsheet/excel/internals/biff5.rb +17 -0
  23. data/lib/spreadsheet/excel/internals/biff8.rb +19 -0
  24. data/lib/spreadsheet/excel/offset.rb +41 -0
  25. data/lib/spreadsheet/excel/reader.rb +1173 -0
  26. data/lib/spreadsheet/excel/reader/biff5.rb +22 -0
  27. data/lib/spreadsheet/excel/reader/biff8.rb +193 -0
  28. data/lib/spreadsheet/excel/row.rb +92 -0
  29. data/lib/spreadsheet/excel/sst_entry.rb +46 -0
  30. data/lib/spreadsheet/excel/workbook.rb +80 -0
  31. data/lib/spreadsheet/excel/worksheet.rb +100 -0
  32. data/lib/spreadsheet/excel/writer.rb +1 -0
  33. data/lib/spreadsheet/excel/writer/biff8.rb +75 -0
  34. data/lib/spreadsheet/excel/writer/format.rb +253 -0
  35. data/lib/spreadsheet/excel/writer/workbook.rb +652 -0
  36. data/lib/spreadsheet/excel/writer/worksheet.rb +948 -0
  37. data/lib/spreadsheet/font.rb +92 -0
  38. data/lib/spreadsheet/format.rb +177 -0
  39. data/lib/spreadsheet/formula.rb +9 -0
  40. data/lib/spreadsheet/helpers.rb +11 -0
  41. data/lib/spreadsheet/link.rb +43 -0
  42. data/lib/spreadsheet/row.rb +132 -0
  43. data/lib/spreadsheet/workbook.rb +120 -0
  44. data/lib/spreadsheet/worksheet.rb +279 -0
  45. data/lib/spreadsheet/writer.rb +30 -0
  46. data/ruby-spreadsheet.gemspec +126 -0
  47. data/test/data/test_changes.xls +0 -0
  48. data/test/data/test_copy.xls +0 -0
  49. data/test/data/test_datetime.xls +0 -0
  50. data/test/data/test_empty.xls +0 -0
  51. data/test/data/test_formula.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/worksheet.rb +23 -0
  58. data/test/font.rb +163 -0
  59. data/test/integration.rb +1281 -0
  60. data/test/row.rb +33 -0
  61. data/test/suite.rb +14 -0
  62. data/test/workbook.rb +21 -0
  63. data/test/worksheet.rb +80 -0
  64. metadata +203 -0
@@ -0,0 +1,948 @@
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)
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
+ def write_colinfos
342
+ cols = @worksheet.columns
343
+ bunch = []
344
+ cols.each_with_index do |column, idx|
345
+ if column
346
+ bunch << column
347
+ if cols[idx.next] != column
348
+ write_colinfo bunch
349
+ bunch.clear
350
+ end
351
+ end
352
+ end
353
+ end
354
+ def write_defaultrowheight
355
+ data = [
356
+ 0x00, # Option flags:
357
+ # Bit Mask Contents
358
+ # 0 0x01 1 = Row height and default font height do not match
359
+ # 1 0x02 1 = Row is hidden
360
+ # 2 0x04 1 = Additional space above the row
361
+ # 3 0x08 1 = Additional space below the row
362
+ 0xf2, # Default height for unused rows, in twips = 1/20 of a point
363
+ ]
364
+ write_op 0x0225, data.pack('v2')
365
+ end
366
+ def write_page_settings
367
+ # TODO Other Page Settings Block options:
368
+ # * HORIZONTALPAGEBREAKS
369
+ # * VERTICALPAGEBREAKS
370
+ # * HEADER
371
+ # * FOOTER
372
+ # * HCENTER
373
+ # * VCENTER
374
+ # * LEFTMARGIN
375
+ # * RIGHTMARGIN
376
+ # * TOPMARGIN
377
+ # * BOTTOMMARGIN
378
+ # * PLS
379
+ write_pagesetup # PAGESETUP (5.73)
380
+ # * BITMAP
381
+ end
382
+ # PAGESETUP (5.73)
383
+ def write_pagesetup
384
+ # TODO wire up these other options (5.73). All of these commented-out
385
+ # options should default to false.
386
+
387
+ opts = 0
388
+ # opts |= 0x0001 if print_pages_in_rows?
389
+ opts |= 0x0002 if @worksheet.orientation == :portrait
390
+ # opts |= 0x0004 if page_options_uninitialized?
391
+ # opts |= 0x0008 if print_black_and_white?
392
+ # opts |= 0x0010 if print_draft_quality?
393
+ # opts |= 0x0020 if print_cell_notes?
394
+ # opts |= 0x0040 if use_default_orientation?
395
+ # opts |= 0x0080 if use_start_page_number?
396
+
397
+ # BIFF8+:
398
+ # opts |= 0x0200 if print_notes_at_end_of_sheet?
399
+ # case error_style
400
+ # when :as_displayed
401
+ # # default: nothing
402
+ # when :hide
403
+ # opts |= 0x0400
404
+ # when :dash
405
+ # opts |= 0x0800
406
+ # when :na
407
+ # opts |= 0x0C00
408
+ # end
409
+
410
+ data = [
411
+ 1, # Paper size (0=undefined), see Paper Size Table (5.73)
412
+ 100, # Scaling factor in percent
413
+ 0, # Start page number
414
+ @worksheet.fit_width_to_pages || 0, # Fit worksheet width to this number
415
+ # of pages (0 = use as many as needed)
416
+ @worksheet.fit_height_to_pages || 0, # Fit worksheet height to this
417
+ # number of pages (0 = use as many
418
+ # as needed)
419
+ opts,
420
+ 300, # Print resolution in dpi
421
+ 300, # Vertical print resolution in dpi
422
+ 0, # Header margin (TODO)
423
+ 0, # Footer margin (TODO)
424
+ 1 # Number of copies to print
425
+ ]
426
+ write_op opcode(:pagesetup), data.pack(binfmt(:pagesetup))
427
+ end
428
+ def write_defcolwidth
429
+ # Offset Size Contents
430
+ # 0 2 Column width in characters, using the width of the zero
431
+ # character from default font (first FONT record in the
432
+ # file). Excel adds some extra space to the default width,
433
+ # depending on the default font and default font size. The
434
+ # algorithm how to exactly calculate the resulting column
435
+ # width is not known.
436
+ #
437
+ # Example: The default width of 8 set in this record results
438
+ # in a column width of 8.43 using Arial font with a size of
439
+ # 10 points.
440
+ write_op 0x0055, [8].pack('v')
441
+ end
442
+ def write_dimensions
443
+ # Offset Size Contents
444
+ # 0 4 Index to first used row
445
+ # 4 4 Index to last used row, increased by 1
446
+ # 8 2 Index to first used column
447
+ # 10 2 Index to last used column, increased by 1
448
+ # 12 2 Not used
449
+ write_op 0x0200, @worksheet.dimensions.pack(binfmt(:dimensions))
450
+ end
451
+ def write_eof
452
+ write_op 0x000a
453
+ end
454
+ ##
455
+ # Write a cell with a Formula. May write an additional String record depending
456
+ # on the stored result of the Formula.
457
+ def write_formula row, idx
458
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx)
459
+ cell = row[idx]
460
+ data1 = [
461
+ row.idx, # Index to row
462
+ idx, # Index to column
463
+ xf_idx, # Index to XF record (➜ 6.115)
464
+ ].pack 'v3'
465
+ data2 = nil
466
+ case value = cell.value
467
+ when Numeric # IEEE 754 floating-point value (64-bit double precision)
468
+ data2 = [value].pack EIGHT_BYTE_DOUBLE
469
+ when String
470
+ data2 = [
471
+ 0x00, # (identifier for a string value)
472
+ 0xffff, #
473
+ ].pack 'Cx5v'
474
+ when true, false
475
+ value = value ? 1 : 0
476
+ data2 = [
477
+ 0x01, # (identifier for a Boolean value)
478
+ value, # 0 = FALSE, 1 = TRUE
479
+ 0xffff, #
480
+ ].pack 'CxCx3v'
481
+ when Error
482
+ data2 = [
483
+ 0x02, # (identifier for an error value)
484
+ value.code, # Error code
485
+ 0xffff, #
486
+ ].pack 'CxCx3v'
487
+ when nil
488
+ data2 = [
489
+ 0x03, # (identifier for an empty cell)
490
+ 0xffff, #
491
+ ].pack 'Cx5v'
492
+ else
493
+ data2 = [
494
+ 0x02, # (identifier for an error value)
495
+ 0x2a, # Error code: #N/A! Argument or function not available
496
+ 0xffff, #
497
+ ].pack 'CxCx3v'
498
+ end
499
+ opts = 0x03
500
+ opts |= 0x08 if cell.shared
501
+ data3 = [
502
+ opts # Option flags:
503
+ # Bit Mask Contents
504
+ # 0 0x0001 1 = Recalculate always
505
+ # 1 0x0002 1 = Calculate on open
506
+ # 3 0x0008 1 = Part of a shared formula
507
+ ].pack 'vx4'
508
+ write_op opcode(:formula), data1, data2, data3, cell.data
509
+ if cell.value.is_a?(String)
510
+ write_op opcode(:string), unicode_string(cell.value, 2)
511
+ end
512
+ end
513
+ ##
514
+ # Write a new Worksheet.
515
+ def write_from_scratch
516
+ # ● BOF Type = worksheet (➜ 5.8)
517
+ write_bof
518
+ # ○ UNCALCED ➜ 5.105
519
+ # ○ INDEX ➜ 4.7 (Row Blocks), ➜ 5.59
520
+ # ○ Calculation Settings Block ➜ 4.3
521
+ write_calccount
522
+ write_refmode
523
+ write_iteration
524
+ write_saverecalc
525
+ # ○ PRINTHEADERS ➜ 5.81
526
+ # ○ PRINTGRIDLINES ➜ 5.80
527
+ # ○ GRIDSET ➜ 5.52
528
+ # ○ GUTS ➜ 5.53
529
+ write_guts
530
+ # ○ DEFAULTROWHEIGHT ➜ 5.31
531
+ write_defaultrowheight
532
+ # ○ WSBOOL ➜ 5.113
533
+ write_wsbool
534
+ # ○ Page Settings Block ➜ 4.4
535
+ write_page_settings
536
+ # ○ Worksheet Protection Block ➜ 4.18
537
+ # ○ DEFCOLWIDTH ➜ 5.32
538
+ write_defcolwidth
539
+ # ○○ COLINFO ➜ 5.18
540
+ write_colinfos
541
+ # ○ SORT ➜ 5.99
542
+ # ● DIMENSIONS ➜ 5.35
543
+ write_dimensions
544
+ # ○○ Row Blocks ➜ 4.7
545
+ write_rows
546
+ # ● Worksheet View Settings Block ➜ 4.5
547
+ # ● WINDOW2 ➜ 5.110
548
+ write_window2
549
+ # ○ SCL ➜ 5.92 (BIFF4-BIFF8 only)
550
+ # ○ PANE ➜ 5.75
551
+ # ○○ SELECTION ➜ 5.93
552
+ # ○ STANDARDWIDTH ➜ 5.101
553
+ # ○○ MERGEDCELLS ➜ 5.67
554
+ # ○ LABELRANGES ➜ 5.64
555
+ # ○ PHONETIC ➜ 5.77
556
+ # ○ Conditional Formatting Table ➜ 4.12
557
+ # ○ Hyperlink Table ➜ 4.13
558
+ write_hyperlink_table
559
+ # ○ Data Validity Table ➜ 4.14
560
+ # ○ SHEETLAYOUT ➜ 5.96 (BIFF8X only)
561
+ # ○ SHEETPROTECTION Additional protection, ➜ 5.98 (BIFF8X only)
562
+ # ○ RANGEPROTECTION Additional protection, ➜ 5.84 (BIFF8X only)
563
+ # ● EOF ➜ 5.36
564
+ write_eof
565
+ end
566
+ ##
567
+ # Write record that contains information about the layout of outline symbols.
568
+ def write_guts
569
+ # find the maximum outline_level in rows and columns
570
+ row_outline_level = 0
571
+ col_outline_level = 0
572
+ if(row = @worksheet.rows.select{|x| x!=nil}.max{|a,b| a.outline_level <=> b.outline_level})
573
+ row_outline_level = row.outline_level
574
+ end
575
+ if(col = @worksheet.columns.select{|x| x!=nil}.max{|a,b| a.outline_level <=> b.outline_level})
576
+ col_outline_level = col.outline_level
577
+ end
578
+ # set data
579
+ data = [
580
+ 0, # Width of the area to display row outlines (left of the sheet), in pixel
581
+ 0, # Height of the area to display column outlines (above the sheet), in pixel
582
+ row_outline_level+1, # Number of visible row outline levels (used row levels+1; or 0,if not used)
583
+ col_outline_level+1 # Number of visible column outline levels (used column levels+1; or 0,if not used)
584
+ ]
585
+ # write record
586
+ write_op opcode(:guts), data.pack('v4')
587
+ end
588
+ def write_hlink row, col, link
589
+ # FIXME: only Hyperlinks are supported at present.
590
+ cell_range = [
591
+ row, row, # Cell range address of all cells containing this hyperlink
592
+ col, col, # (➜ 3.13.1)
593
+ ].pack 'v4'
594
+ guid = [
595
+ # GUID of StdLink:
596
+ # D0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B
597
+ # (79EAC9D0-BAF9-11CE-8C82-00AA004BA90B)
598
+ "d0c9ea79f9bace118c8200aa004ba90b",
599
+ ].pack 'H32'
600
+ opts = 0x01
601
+ opts |= 0x02
602
+ opts |= 0x14 unless link == link.url
603
+ opts |= 0x08 if link.fragment
604
+ opts |= 0x80 if link.target_frame
605
+ # TODO: UNC support
606
+ options = [
607
+ 2, # Unknown value: 0x00000002
608
+ opts, # Option flags
609
+ # Bit Mask Contents
610
+ # 0 0x00000001 0 = No link extant
611
+ # 1 = File link or URL
612
+ # 1 0x00000002 0 = Relative file path
613
+ # 1 = Absolute path or URL
614
+ # 2 and 4 0x00000014 0 = No description
615
+ # 1 (both bits) = Description
616
+ # 3 0x00000008 0 = No text mark
617
+ # 1 = Text mark
618
+ # 7 0x00000080 0 = No target frame
619
+ # 1 = Target frame
620
+ # 8 0x00000100 0 = File link or URL
621
+ # 1 = UNC path (incl. server name)
622
+
623
+ ].pack('V2')
624
+ tail = []
625
+ ## call internal to get the correct internal encoding in Ruby 1.9
626
+ nullstr = internal "\000"
627
+ unless link == link.url
628
+ desc = internal(link).dup << nullstr
629
+ tail.push [desc.size / 2].pack('V'), desc
630
+ end
631
+ if link.target_frame
632
+ frme = internal(link.target_frame).dup << nullstr
633
+ tail.push [frme.size / 2].pack('V'), frme
634
+ end
635
+ url = internal(link.url).dup << nullstr
636
+ tail.push [
637
+ # 6.53.2 Hyperlink containing a URL (Uniform Resource Locator)
638
+ # These data fields occur for links which are not local files or files
639
+ # in the local network (for instance HTTP and FTP links and e-mail
640
+ # addresses). The lower 9 bits of the option flags field must be
641
+ # 0.x00x.xx112 (x means optional, depending on hyperlink content). The
642
+ # GUID could be used to distinguish a URL from a file link.
643
+ # GUID of URL Moniker:
644
+ # E0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B
645
+ # (79EAC9E0-BAF9-11CE-8C82-00AA004BA90B)
646
+ 'e0c9ea79f9bace118c8200aa004ba90b',
647
+ url.size # Size of character array of the URL, including trailing zero
648
+ # word (us). There are us/2-1 characters in the following
649
+ # string.
650
+ ].pack('H32V'), url
651
+ if link.fragment
652
+ frag = internal(link.fragment).dup << nullstr
653
+ tail.push [frag.size / 2].pack('V'), frag
654
+ end
655
+ write_op opcode(:hlink), cell_range, guid, options, *tail
656
+ end
657
+ def write_hyperlink_table
658
+ # TODO: theoretically it's possible to write fewer records by combining
659
+ # identical neighboring links in cell-ranges
660
+ links = []
661
+ @worksheet.each do |row|
662
+ row.each_with_index do |cell, idx|
663
+ if cell.is_a? Link
664
+ write_hlink row.idx, idx, cell
665
+ end
666
+ end
667
+ end
668
+ end
669
+ def write_iteration
670
+ its = 0 # 0 = Iterations off; 1 = Iterations on
671
+ write_op 0x0011, [its].pack('v')
672
+ end
673
+ ##
674
+ # Write a cell with a String value. The String must have been stored in the
675
+ # Shared String Table.
676
+ def write_labelsst row, idx
677
+ write_cell :labelsst, row, idx, @workbook.sst_index(self, row[idx])
678
+ end
679
+ ##
680
+ # Write multiple consecutive blank cells.
681
+ def write_mulblank row, idx, multiples
682
+ data = [
683
+ row.idx, # Index to row
684
+ idx, # Index to first column (fc)
685
+ ]
686
+ # List of nc=lc-fc+1 16-bit indexes to XF records (➜ 6.115)
687
+ multiples.each_with_index do |blank, cell_idx|
688
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx + cell_idx)
689
+ data.push xf_idx
690
+ end
691
+ # Index to last column (lc)
692
+ data.push idx + multiples.size - 1
693
+ write_op opcode(:mulblank), data.pack('v*')
694
+ end
695
+ ##
696
+ # Write multiple consecutive cells with RK values (see #write_rk)
697
+ def write_mulrk row, idx, multiples
698
+ fmt = 'v2'
699
+ data = [
700
+ row.idx, # Index to row
701
+ idx, # Index to first column (fc)
702
+ ]
703
+ # List of nc=lc-fc+1 16-bit indexes to XF records (➜ 6.115)
704
+ multiples.each_with_index do |cell, cell_idx|
705
+ xf_idx = @workbook.xf_index @worksheet.workbook, row.format(idx + cell_idx)
706
+ data.push xf_idx, encode_rk(cell)
707
+ fmt << 'vV'
708
+ end
709
+ # Index to last column (lc)
710
+ data.push idx + multiples.size - 1
711
+ write_op opcode(:mulrk), data.pack(fmt << 'v')
712
+ end
713
+ def write_multiples row, idx, multiples
714
+ case multiples.last
715
+ when NilClass
716
+ write_mulblank row, idx, multiples
717
+ when Numeric
718
+ if multiples.size > 1
719
+ write_mulrk row, idx, multiples
720
+ else
721
+ write_rk row, idx
722
+ end
723
+ end
724
+ end
725
+ ##
726
+ # Write a cell with a 64-bit double precision Float value
727
+ def write_number row, idx
728
+ # Offset Size Contents
729
+ # 0 2 Index to row
730
+ # 2 2 Index to column
731
+ # 4 2 Index to XF record (➜ 6.115)
732
+ # 6 8 IEEE 754 floating-point value (64-bit double precision)
733
+ value = row[idx]
734
+ case value
735
+ when Date, Time
736
+ value = encode_date(value)
737
+ end
738
+ write_cell :number, row, idx, value
739
+ end
740
+ def write_op op, *args
741
+ data = args.join
742
+ @io.write [op,data.size].pack("v2")
743
+ @io.write data
744
+ end
745
+ def write_refmode
746
+ # • The “RC” mode uses numeric indexes for rows and columns, for example
747
+ # “R(1)C(-1)”, or “R1C1:R2C2”.
748
+ # • The “A1” mode uses characters for columns and numbers for rows, for
749
+ # example “B1”, or “$A$1:$B$2”.
750
+ mode = 1 # 0 = RC mode; 1 = A1 mode
751
+ write_op 0x000f, [mode].pack('v')
752
+ end
753
+ ##
754
+ # Write a cell with a Numeric or Date value.
755
+ def write_rk row, idx
756
+ write_cell :rk, row, idx, encode_rk(row[idx])
757
+ end
758
+ def write_row row
759
+ # Offset Size Contents
760
+ # 0 2 Index of this row
761
+ # 2 2 Index to column of the first cell which
762
+ # is described by a cell record
763
+ # 4 2 Index to column of the last cell which is
764
+ # described by a cell record, increased by 1
765
+ # 6 2 Bit Mask Contents
766
+ # 14-0 0x7fff Height of the row, in twips = 1/20 of a point
767
+ # 15 0x8000 0 = Row has custom height;
768
+ # 1 = Row has default height
769
+ # 8 2 Not used
770
+ # 10 2 In BIFF3-BIFF4 this field contains a relative offset to
771
+ # calculate stream position of the first cell record for this
772
+ # row (➜ 5.7.1). In BIFF5-BIFF8 this field is not used
773
+ # anymore, but the DBCELL record (➜ 6.26) instead.
774
+ # 12 4 Option flags and default row formatting:
775
+ # Bit Mask Contents
776
+ # 2-0 0x00000007 Outline level of the row
777
+ # 4 0x00000010 1 = Outline group starts or ends here
778
+ # (depending on where the outline
779
+ # buttons are located, see WSBOOL
780
+ # record, ➜ 6.113), and is collapsed
781
+ # 5 0x00000020 1 = Row is hidden (manually, or by a
782
+ # filter or outline group)
783
+ # 6 0x00000040 1 = Row height and default font height
784
+ # do not match
785
+ # 7 0x00000080 1 = Row has explicit default format (fl)
786
+ # 8 0x00000100 Always 1
787
+ # 27-16 0x0fff0000 If fl = 1: Index to default XF record
788
+ # (➜ 6.115)
789
+ # 28 0x10000000 1 = Additional space above the row.
790
+ # This flag is set, if the upper
791
+ # border of at least one cell in this
792
+ # row or if the lower border of at
793
+ # least one cell in the row above is
794
+ # formatted with a thick line style.
795
+ # Thin and medium line styles are not
796
+ # taken into account.
797
+ # 29 0x20000000 1 = Additional space below the row.
798
+ # This flag is set, if the lower
799
+ # border of at least one cell in this
800
+ # row or if the upper border of at
801
+ # least one cell in the row below is
802
+ # formatted with a medium or thick
803
+ # line style. Thin line styles are
804
+ # not taken into account.
805
+ height = row.height || ROW_HEIGHT
806
+ opts = row.outline_level & 0x00000007
807
+ opts |= 0x00000010 if row.collapsed?
808
+ opts |= 0x00000020 if row.hidden?
809
+ opts |= 0x00000040 if height != ROW_HEIGHT
810
+ if fmt = row.default_format
811
+ xf_idx = @workbook.xf_index @worksheet.workbook, fmt
812
+ opts |= 0x00000080
813
+ opts |= xf_idx << 16
814
+ end
815
+ opts |= 0x00000100
816
+ height = if height == ROW_HEIGHT
817
+ (height * TWIPS).to_i | 0x8000
818
+ else
819
+ height * TWIPS
820
+ end
821
+ # TODO: Row spacing
822
+ data = [
823
+ row.idx,
824
+ row.first_used,
825
+ row.first_unused,
826
+ height,
827
+ opts,
828
+ ].pack binfmt(:row)
829
+ write_op opcode(:row), data
830
+ end
831
+ def write_rowblock block
832
+ # ●● ROW Properties of the used rows
833
+ # ○○ Cell Block(s) Cell records for all used cells
834
+ # ○ DBCELL Stream offsets to the cell records of each row
835
+ block.each do |row|
836
+ write_row row
837
+ end
838
+ block.each do |row|
839
+ write_cellblocks row
840
+ end
841
+ end
842
+ def write_rows
843
+ row_blocks.each do |block|
844
+ write_rowblock block
845
+ end
846
+ end
847
+ def write_saverecalc
848
+ # 0 = Do not recalculate; 1 = Recalculate before saving the document
849
+ write_op 0x005f, [1].pack('v')
850
+ end
851
+ def write_window2
852
+ # This record contains additional settings for the document window
853
+ # (BIFF2-BIFF4) or for the window of a specific worksheet (BIFF5-BIFF8).
854
+ # It is part of the Sheet View Settings Block (➜ 4.5).
855
+ # Offset Size Contents
856
+ # 0 2 Option flags:
857
+ # Bits Mask Contents
858
+ # 0 0x0001 0 = Show formula results
859
+ # 1 = Show formulas
860
+ # 1 0x0002 0 = Do not show grid lines
861
+ # 1 = Show grid lines
862
+ # 2 0x0004 0 = Do not show sheet headers
863
+ # 1 = Show sheet headers
864
+ # 3 0x0008 0 = Panes are not frozen
865
+ # 1 = Panes are frozen (freeze)
866
+ # 4 0x0010 0 = Show zero values as empty cells
867
+ # 1 = Show zero values
868
+ # 5 0x0020 0 = Manual grid line colour
869
+ # 1 = Automatic grid line colour
870
+ # 6 0x0040 0 = Columns from left to right
871
+ # 1 = Columns from right to left
872
+ # 7 0x0080 0 = Do not show outline symbols
873
+ # 1 = Show outline symbols
874
+ # 8 0x0100 0 = Keep splits if pane freeze is removed
875
+ # 1 = Remove splits if pane freeze is removed
876
+ # 9 0x0200 0 = Sheet not selected
877
+ # 1 = Sheet selected (BIFF5-BIFF8)
878
+ # 10 0x0400 0 = Sheet not active
879
+ # 1 = Sheet active (BIFF5-BIFF8)
880
+ # 11 0x0800 0 = Show in normal view
881
+ # 1 = Show in page break preview (BIFF8)
882
+ # 2 2 Index to first visible row
883
+ # 4 2 Index to first visible column
884
+ # 6 2 Colour index of grid line colour (➜ 5.74).
885
+ # Note that in BIFF2-BIFF5 an RGB colour is written instead.
886
+ # 8 2 Not used
887
+ # 10 2 Cached magnification factor in page break preview (in percent)
888
+ # 0 = Default (60%)
889
+ # 12 2 Cached magnification factor in normal view (in percent)
890
+ # 0 = Default (100%)
891
+ # 14 4 Not used
892
+ flags = 0x0536 # Show grid lines, sheet headers, zero values. Automatic
893
+ # grid line colour, Remove slits if pane freeze is removed,
894
+ # Sheet is active.
895
+ if @worksheet.selected
896
+ flags |= 0x0200
897
+ end
898
+ flags |= 0x0080 # Show outline symbols,
899
+ # but if [Row|Column]#outline_level = 0 the symbols are not shown.
900
+ data = [ flags, 0, 0, 0, 0, 0 ].pack binfmt(:window2)
901
+ write_op opcode(:window2), data
902
+ end
903
+ def write_wsbool
904
+ fit_print_to_pages = (@worksheet.fit_width_to_pages ||
905
+ @worksheet.fit_height_to_pages) ? 1 : 0
906
+ bits = [
907
+ # Bit Mask Contents
908
+ 1, # 0 0x0001 0 = Do not show automatic page breaks
909
+ # 1 = Show automatic page breaks
910
+ 0, # 4 0x0010 0 = Standard sheet
911
+ # 1 = Dialogue sheet (BIFF5-BIFF8)
912
+ 0, # 5 0x0020 0 = No automatic styles in outlines
913
+ # 1 = Apply automatic styles to outlines
914
+ 1, # 6 0x0040 0 = Outline buttons above outline group
915
+ # 1 = Outline buttons below outline group
916
+ 1, # 7 0x0080 0 = Outline buttons left of outline group
917
+ # 1 = Outline buttons right of outline group
918
+ fit_print_to_pages,
919
+ # 8 0x0100 0 = Scale printout in percent (➜ 6.89)
920
+ # 1 = Fit printout to number of pages (➜ 6.89)
921
+ 0, # 9 0x0200 0 = Save external linked values
922
+ # (BIFF3-BIFF4 only, ➜ 5.10)
923
+ # 1 = Do not save external linked values
924
+ # (BIFF3-BIFF4 only, ➜ 5.10)
925
+ 1, # 10 0x0400 0 = Do not show row outline symbols
926
+ # 1 = Show row outline symbols
927
+ 0, # 11 0x0800 0 = Do not show column outline symbols
928
+ # 1 = Show column outline symbols
929
+ 0, # 13-12 0x3000 These flags specify the arrangement of windows.
930
+ # They are stored in BIFF4 only.
931
+ # 00 = Arrange windows tiled
932
+ # 01 = Arrange windows horizontal
933
+ 0, # 10 = Arrange windows vertical
934
+ # 11 = Arrange windows cascaded
935
+ # The following flags are valid for BIFF4-BIFF8 only:
936
+ 0, # 14 0x4000 0 = Standard expression evaluation
937
+ # 1 = Alternative expression evaluation
938
+ 0, # 15 0x8000 0 = Standard formula entries
939
+ # 1 = Alternative formula entries
940
+ ]
941
+ weights = [4,5,6,7,8,9,10,11,12,13,14,15]
942
+ value = bits.inject do |a, b| a | (b << weights.shift) end
943
+ write_op 0x0081, [value].pack('v')
944
+ end
945
+ end
946
+ end
947
+ end
948
+ end