ttb-spreadsheet 0.6.5.8

Sign up to get free protection for your applications and to get access to all the features.
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 @@
1
+ require 'spreadsheet/excel/writer/workbook'
@@ -0,0 +1,75 @@
1
+ require 'spreadsheet/encodings'
2
+
3
+ module Spreadsheet
4
+ module Excel
5
+ module Writer
6
+ ##
7
+ # This Module collects writer methods such as unicode_string that are specific
8
+ # to Biff8. This Module is likely to be expanded as Support for older Versions
9
+ # of Excel grows and methods get moved here for disambiguation.
10
+ module Biff8
11
+ include Spreadsheet::Encodings
12
+ ##
13
+ # Check whether the string _data_ can be compressed (i.e. every second byte
14
+ # is a Null-byte) and perform compression.
15
+ # Returns the data and compression_status (0/1)
16
+ def compress_unicode_string data
17
+ compressed = internal('')
18
+ expect_null = false
19
+ data.each_byte do |byte|
20
+ if expect_null
21
+ if byte != 0
22
+ return [data, 1] # 1 => Data consists of wide Chars
23
+ end
24
+ expect_null = false
25
+ else
26
+ compressed << byte
27
+ expect_null = true
28
+ end
29
+ end
30
+ [compressed, 0] # 0 => Data consists of compressed Chars
31
+ end
32
+ ##
33
+ # Encode _string_ into a Biff8 Unicode String. Header and body are encoded
34
+ # separately by #_unicode_string. This method simply combines the two.
35
+ def unicode_string string, count_length=1
36
+ header, data, _ = _unicode_string string, count_length
37
+ header << data
38
+ end
39
+ @@bytesize = RUBY_VERSION >= '1.9' ? :bytesize : :size
40
+ ##
41
+ # Encode _string_ into a Biff8 Unicode String Header and Body.
42
+ def _unicode_string string, count_length=1
43
+ data = internal string
44
+ size = data.send(@@bytesize) / 2
45
+ fmt = count_length == 1 ? 'C2' : 'vC'
46
+ data, wide = compress_unicode_string data
47
+ opts = wide
48
+ header = [
49
+ size, # Length of the string (character count, ln)
50
+ opts, # Option flags:
51
+ # Bit Mask Contents
52
+ # 0 0x01 Character compression (ccompr):
53
+ # 0 = Compressed (8-bit characters)
54
+ # 1 = Uncompressed (16-bit characters)
55
+ # 2 0x04 Asian phonetic settings (phonetic):
56
+ # 0 = Does not contain Asian phonetic settings
57
+ # 1 = Contains Asian phonetic settings
58
+ # 3 0x08 Rich-Text settings (richtext):
59
+ # 0 = Does not contain Rich-Text settings
60
+ # 1 = Contains Rich-Text settings
61
+ #0x00,# (optional, only if richtext=1) Number of Rich-Text
62
+ # formatting runs (rt)
63
+ #0x00,# (optional, only if phonetic=1) Size of Asian phonetic
64
+ # settings block (in bytes, sz)
65
+ ].pack fmt
66
+ data << '' # (optional, only if richtext=1)
67
+ # List of rt formatting runs (➜ 3.2)
68
+ data << '' # (optional, only if phonetic=1)
69
+ # Asian Phonetic Settings Block (➜ 3.4.2)
70
+ [header, data, wide]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,253 @@
1
+ require 'delegate'
2
+ require 'spreadsheet/format'
3
+ require 'spreadsheet/excel/internals'
4
+
5
+ module Spreadsheet
6
+ module Excel
7
+ module Writer
8
+ ##
9
+ # This class encapsulates everything that is needed to write an XF record.
10
+ class Format < DelegateClass Spreadsheet::Format
11
+ include Spreadsheet::Excel::Internals
12
+ def Format.boolean *args
13
+ args.each do |key|
14
+ define_method key do
15
+ @format.send("#{key}?") ? 1 : 0
16
+ end
17
+ end
18
+ end
19
+ def Format.color key, default
20
+ define_method key do
21
+ color_code(@format.send(key) || default)
22
+ end
23
+ end
24
+ boolean :hidden, :locked, :merge_range, :shrink, :text_justlast, :text_wrap,
25
+ :cross_down, :cross_up, :left, :right, :top, :bottom
26
+ color :left_color, :border
27
+ color :right_color, :border
28
+ color :top_color, :border
29
+ color :bottom_color, :border
30
+ color :diagonal_color, :border
31
+ color :pattern_fg_color, :pattern_bg
32
+ color :pattern_bg_color, :pattern_bg
33
+ attr_accessor :xf_index
34
+ attr_reader :format
35
+ def initialize writer, workbook, format=workbook.default_format, opts={}
36
+ @opts = { :type => :format }.merge opts
37
+ @format = format
38
+ @writer = writer
39
+ @workbook = workbook
40
+ super format
41
+ end
42
+ def color_code color
43
+ SEDOC_ROLOC[color]
44
+ end
45
+ def font_index
46
+ @writer.font_index @workbook, font.key
47
+ end
48
+ def horizontal_align
49
+ XF_H_ALIGN.fetch @format.horizontal_align, 0
50
+ end
51
+ def num_format
52
+ @writer.number_format_index @workbook, @format.number_format
53
+ end
54
+ def text_direction
55
+ XF_TEXT_DIRECTION.fetch @format.text_direction, 0
56
+ end
57
+ def vertical_align
58
+ XF_V_ALIGN.fetch @format.vertical_align, 2
59
+ end
60
+ def write_op writer, op, *args
61
+ data = args.join
62
+ writer.write [op,data.size].pack("v2")
63
+ writer.write data
64
+ end
65
+ def write_xf writer, type=@opts[:type]
66
+ xf_type = xf_type_prot type
67
+ data = [
68
+ font_index, # Index to FONT record (➜ 6.43)
69
+ num_format, # Index to FORMAT record (➜ 6.45)
70
+ xf_type, # Bit Mask Contents
71
+ # 2-0 0x0007 XF_TYPE_PROT – XF type, cell protection
72
+ # Bit Mask Contents
73
+ # 0 0x01 1 = Cell is locked
74
+ # 1 0x02 1 = Formula is hidden
75
+ # 2 0x04 0 = Cell XF; 1 = Style XF
76
+ # 15-4 0xfff0 Index to parent style XF
77
+ # (always 0xfff in style XFs)
78
+ xf_align, # Bit Mask Contents
79
+ # 2-0 0x07 XF_HOR_ALIGN – Horizontal alignment
80
+ # Value Horizontal alignment
81
+ # 0x00 General
82
+ # 0x01 Left
83
+ # 0x02 Centred
84
+ # 0x03 Right
85
+ # 0x04 Filled
86
+ # 0x05 Justified (BIFF4-BIFF8X)
87
+ # 0x06 Centred across selection
88
+ # (BIFF4-BIFF8X)
89
+ # 0x07 Distributed (BIFF8X)
90
+ # 3 0x08 1 = Text is wrapped at right border
91
+ # 6-4 0x70 XF_VERT_ALIGN – Vertical alignment
92
+ # Value Vertical alignment
93
+ # 0x00 Top
94
+ # 0x01 Centred
95
+ # 0x02 Bottom
96
+ # 0x03 Justified (BIFF5-BIFF8X)
97
+ # 0x04 Distributed (BIFF8X)
98
+ xf_rotation, # XF_ROTATION: Text rotation angle
99
+ # Value Text rotation
100
+ # 0 Not rotated
101
+ # 1-90 1 to 90 degrees counterclockwise
102
+ # 91-180 1 to 90 degrees clockwise
103
+ # 255 Letters are stacked top-to-bottom,
104
+ # but not rotated
105
+ xf_indent, # Bit Mask Contents
106
+ # 3-0 0x0f Indent level
107
+ # 4 0x10 1 = Shrink content to fit into cell
108
+ # 5 0x40 1 = Merge Range (djberger)
109
+ # 7-6 0xc0 Text direction (BIFF8X only)
110
+ # 0 = According to context
111
+ # 1 = Left-to-right
112
+ # 2 = Right-to-left
113
+ xf_used_attr, # Bit Mask Contents
114
+ # 7-2 0xfc XF_USED_ATTRIB – Used attributes
115
+ # Each bit describes the validity of a
116
+ # specific group of attributes. In cell XFs
117
+ # a cleared bit means the attributes of the
118
+ # parent style XF are used (but only if the
119
+ # attributes are valid there), a set bit
120
+ # means the attributes of this XF are used.
121
+ # In style XFs a cleared bit means the
122
+ # attribute setting is valid, a set bit
123
+ # means the attribute should be ignored.
124
+ # Bit Mask Contents
125
+ # 0 0x01 Flag for number format
126
+ # 1 0x02 Flag for font
127
+ # 2 0x04 Flag for horizontal and
128
+ # vertical alignment, text wrap,
129
+ # indentation, orientation,
130
+ # rotation, and text direction
131
+ # 3 0x08 Flag for border lines
132
+ # 4 0x10 Flag for background area style
133
+ # 5 0x20 Flag for cell protection (cell
134
+ # locked and formula hidden)
135
+ xf_borders, # Cell border lines and background area:
136
+ # Bit Mask Contents
137
+ # 3- 0 0x0000000f Left line style (➜ 3.10)
138
+ # 7- 4 0x000000f0 Right line style (➜ 3.10)
139
+ # 11- 8 0x00000f00 Top line style (➜ 3.10)
140
+ # 15-12 0x0000f000 Bottom line style (➜ 3.10)
141
+ # 22-16 0x007f0000 Colour index (➜ 6.70)
142
+ # for left line colour
143
+ # 29-23 0x3f800000 Colour index (➜ 6.70)
144
+ # for right line colour
145
+ # 30 0x40000000 1 = Diagonal line
146
+ # from top left to right bottom
147
+ # 31 0x80000000 1 = Diagonal line
148
+ # from bottom left to right top
149
+ xf_brdcolors, # Bit Mask Contents
150
+ # 6- 0 0x0000007f Colour index (➜ 6.70)
151
+ # for top line colour
152
+ # 13- 7 0x00003f80 Colour index (➜ 6.70)
153
+ # for bottom line colour
154
+ # 20-14 0x001fc000 Colour index (➜ 6.70)
155
+ # for diagonal line colour
156
+ # 24-21 0x01e00000 Diagonal line style (➜ 3.10)
157
+ # 31-26 0xfc000000 Fill pattern (➜ 3.11)
158
+ xf_pattern # Bit Mask Contents
159
+ # 6-0 0x007f Colour index (➜ 6.70)
160
+ # for pattern colour
161
+ # 13-7 0x3f80 Colour index (➜ 6.70)
162
+ # for pattern background
163
+ ]
164
+ write_op writer, 0x00e0, data.pack(binfmt(:xf))
165
+ end
166
+ def xf_align
167
+ align = horizontal_align
168
+ align |= text_wrap << 3
169
+ align |= vertical_align << 4
170
+ align |= text_justlast << 7
171
+ align
172
+ end
173
+ def xf_borders
174
+ border = left
175
+ border |= right << 4
176
+ border |= top << 8
177
+ border |= bottom << 12
178
+ border |= left_color << 16
179
+ border |= right_color << 23
180
+ border |= cross_down << 30
181
+ border |= cross_up << 31
182
+ border
183
+ end
184
+ def xf_brdcolors
185
+ border = top_color
186
+ border |= bottom_color << 7
187
+ border |= diagonal_color << 14
188
+ border |= pattern << 26
189
+ border
190
+ end
191
+ def xf_indent
192
+ indent = indent_level & 0x0f
193
+ indent |= shrink << 4
194
+ indent |= merge_range << 5
195
+ indent |= text_direction << 6
196
+ indent
197
+ end
198
+ def xf_pattern
199
+ ptrn = pattern_fg_color
200
+ ptrn |= pattern_bg_color << 7
201
+ ptrn
202
+ end
203
+ def xf_rotation
204
+ rot = @format.rotation
205
+ if @format.rotation_stacked?
206
+ rot = 255
207
+ elsif rot >= -90 or rotation <= 90
208
+ rot = -rot + 90 if rot < 0
209
+ else
210
+ warn "rotation outside -90..90; rotation set to 0"
211
+ rot = 0
212
+ end
213
+ rot
214
+ end
215
+ def xf_type_prot type
216
+ type = type.to_s.downcase == 'style' ? 0xfff5 : 0x0000
217
+ type |= locked
218
+ type |= hidden << 1
219
+ type
220
+ end
221
+ def xf_used_attr
222
+ atr_num = num_format & 1
223
+ atr_fnt = font_index & 1
224
+ atr_fnt = 1 unless @format.font.color == :text
225
+ atr_alc = 0
226
+ if horizontal_align != 0 \
227
+ || vertical_align != 2 \
228
+ || indent_level > 0 \
229
+ || shrink? || merge_range? || text_wrap?
230
+ then
231
+ atr_alc = 1
232
+ end
233
+ atr_bdr = [top, bottom, left, right, cross_up, cross_down].max
234
+ atr_pat = 0
235
+ if @format.pattern_fg_color != :border \
236
+ || @format.pattern_bg_color != :pattern_bg \
237
+ || pattern != 0x00
238
+ then
239
+ atr_pat = 1
240
+ end
241
+ atr_prot = hidden? || locked? ? 1 : 0
242
+ attrs = atr_num
243
+ attrs |= atr_fnt << 1
244
+ attrs |= atr_alc << 2
245
+ attrs |= atr_bdr << 3
246
+ attrs |= atr_pat << 4
247
+ attrs |= atr_prot << 5
248
+ attrs << 2
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,690 @@
1
+ require 'spreadsheet/excel/internals'
2
+ require 'spreadsheet/writer'
3
+ require 'spreadsheet/excel/writer/biff8'
4
+ require 'spreadsheet/excel/writer/format'
5
+ require 'spreadsheet/excel/writer/worksheet'
6
+ require 'ole/storage'
7
+
8
+ module Spreadsheet
9
+ module Excel
10
+ module Writer
11
+ ##
12
+ # Writer class for Excel Workbooks. Most write_* method correspond to an
13
+ # Excel-Record/Opcode. Designed to be able to write several Workbooks in
14
+ # parallel (just because I can't imagine why you would want to do that
15
+ # doesn't mean it shouldn't be possible ;). You should not need to call any of
16
+ # its methods directly. If you think you do, look at #write_workbook
17
+ class Workbook < Spreadsheet::Writer
18
+ include Spreadsheet::Excel::Writer::Biff8
19
+ include Spreadsheet::Excel::Internals
20
+ attr_reader :fonts, :date_base
21
+ def initialize *args
22
+ super
23
+ @biff_version = 0x0600
24
+ @bof = 0x0809
25
+ @build_id = 3515
26
+ @build_year = 1996
27
+ @bof_types = {
28
+ :globals => 0x0005,
29
+ :visual_basic => 0x0006,
30
+ :worksheet => 0x0010,
31
+ :chart => 0x0020,
32
+ :macro_sheet => 0x0040,
33
+ :workspace => 0x0100,
34
+ }
35
+ @worksheets = {}
36
+ @sst = {}
37
+ @recordsize_limit = 8224
38
+ @fonts = {}
39
+ @formats = {}
40
+ @number_formats = {}
41
+ end
42
+ def cleanup workbook
43
+ worksheets(workbook).each do |worksheet|
44
+ @sst.delete worksheet
45
+ end
46
+ @fonts.delete workbook
47
+ @formats.delete workbook
48
+ @number_formats.delete workbook
49
+ @worksheets.delete workbook
50
+ end
51
+ def collect_formats workbook, opts={}
52
+ # The default cell format is always present in an Excel file, described by
53
+ # the XF record with the fixed index 15 (0-based). By default, it uses the
54
+ # worksheet/workbook default cell style, described by the very first XF
55
+ # record (index 0).
56
+ formats = []
57
+ unless opts[:existing_document]
58
+ 15.times do
59
+ formats.push Format.new(self, workbook, workbook.default_format,
60
+ :type => :style)
61
+ end
62
+ formats.push Format.new(self, workbook)
63
+ end
64
+ workbook.formats.each do |fmt|
65
+ formats.push Format.new(self, workbook, fmt)
66
+ end
67
+ formats.each_with_index do |fmt, idx|
68
+ fmt.xf_index = idx
69
+ end
70
+ @formats[workbook] = formats
71
+ end
72
+ def complete_sst_update? workbook
73
+ stored = workbook.sst.collect do |entry| entry.content end
74
+ current = worksheets(workbook).inject [] do |memo, worksheet|
75
+ memo.concat worksheet.strings
76
+ end
77
+ total = current.size
78
+ current.uniq!
79
+ current.delete ''
80
+ if (stored - current).empty? && !stored.empty?
81
+ ## if all previously stored strings are still needed, we don't have to
82
+ # rewrite all cells because the sst-index of such string does not change.
83
+ additions = current - stored
84
+ [:partial_update, total, stored + additions]
85
+ else
86
+ [:complete_update, total, current]
87
+ end
88
+ end
89
+ def font_index workbook, font_key
90
+ idx = @fonts[workbook][font_key] || 0
91
+ ## this appears to be undocumented: the first 4 fonts seem to be accessed
92
+ # with a 0-based index, but all subsequent font indices are 1-based.
93
+ idx > 3 ? idx.next : idx
94
+ end
95
+ def number_format_index workbook, format
96
+ @number_formats[workbook][format] || 0
97
+ end
98
+ def sanitize_worksheets sheets
99
+ return sheets if sheets.empty?
100
+ found_selected = false
101
+ sheets.each do |sheet|
102
+ found_selected ||= sheet.selected
103
+ sheet.format_dates!
104
+ end
105
+ unless found_selected
106
+ sheets.first.selected = true
107
+ end
108
+ sheets
109
+ end
110
+ def worksheets workbook
111
+ @worksheets[workbook] ||= workbook.worksheets.collect do |worksheet|
112
+ Excel::Writer::Worksheet.new self, worksheet
113
+ end
114
+ end
115
+ def write_bof workbook, writer, type
116
+ data = [
117
+ @biff_version, # BIFF version (always 0x0600 for BIFF8)
118
+ @bof_types[type], # Type of the following data:
119
+ # 0x0005 = Workbook globals
120
+ # 0x0006 = Visual Basic module
121
+ # 0x0010 = Worksheet
122
+ # 0x0020 = Chart
123
+ # 0x0040 = Macro sheet
124
+ # 0x0100 = Workspace file
125
+ @build_id, # Build identifier
126
+ @build_year, # Build year
127
+ 0x000, # File history flags
128
+ 0x006, # Lowest Excel version that can read
129
+ # all records in this file
130
+ ]
131
+ write_op writer, @bof, data.pack("v4V2")
132
+ end
133
+ def write_bookbool workbook, writer
134
+ write_placeholder writer, 0x00da
135
+ end
136
+ def write_boundsheets workbook, writer, offset
137
+ worksheets = worksheets(workbook)
138
+ worksheets.each do |worksheet|
139
+ # account for boundsheet-entry
140
+ offset += worksheet.boundsheet_size
141
+ end
142
+ worksheets.each do |worksheet|
143
+ data = [
144
+ offset, # Absolute stream position of the BOF record of the sheet
145
+ # represented by this record. This field is never encrypted
146
+ # in protected files.
147
+ 0x00, # Visibility: 0x00 = Visible
148
+ # 0x01 = Hidden
149
+ # 0x02 = Strong hidden (see below)
150
+ 0x00, # Sheet type: 0x00 = Worksheet
151
+ # 0x02 = Chart
152
+ # 0x06 = Visual Basic module
153
+ ]
154
+ write_op writer, 0x0085, data.pack("VC2"), worksheet.name
155
+ offset += worksheet.size
156
+ end
157
+ end
158
+ ##
159
+ # Copy unchanged data verbatim, adjust offsets and write new records for
160
+ # changed data.
161
+ def write_changes workbook, io
162
+ sanitize_worksheets workbook.worksheets
163
+ collect_formats workbook, :existing_document => true
164
+ reader = workbook.ole
165
+ sheet_data = {}
166
+ sst_status, sst_total, sst_strings = complete_sst_update? workbook
167
+ sst = {}
168
+ sst_strings.each_with_index do |str, idx| sst.store str, idx end
169
+ sheets = worksheets(workbook)
170
+ positions = []
171
+ newsheets = []
172
+ sheets.each do |sheet|
173
+ @sst[sheet] = sst
174
+ pos, len = workbook.offsets[sheet.worksheet]
175
+ if pos
176
+ positions.push pos
177
+ sheet.write_changes reader, pos + len, sst_status
178
+ else
179
+ newsheets.push sheet
180
+ sheet.write_from_scratch
181
+ end
182
+ sheet_data[sheet.worksheet] = sheet.data
183
+ end
184
+ Ole::Storage.open io do |ole|
185
+ ole.file.open 'Workbook', 'w' do |writer|
186
+ reader.seek lastpos = 0
187
+ workbook.offsets.select do |key, pair|
188
+ workbook.changes.include? key
189
+ end.sort_by do |key, (pos, len)|
190
+ pos
191
+ end.each do |key, (pos, len)|
192
+ data = reader.read(pos - lastpos)
193
+ writer.write data
194
+ case key
195
+ when Spreadsheet::Worksheet
196
+ writer.write sheet_data[key]
197
+ when :boundsheets
198
+ ## boundsheets are hard to calculate. The offset below is only
199
+ # correct if there are no more changes in the workbook globals
200
+ # string after this.
201
+ oldoffset = positions.min - len
202
+ lastpos = pos + len
203
+ bytechange = 0
204
+ buffer = StringIO.new ''
205
+ if tuple = workbook.offsets[:sst]
206
+ write_sst_changes workbook, buffer, writer.pos,
207
+ sst_total, sst_strings
208
+ pos, len = tuple
209
+ if offset = workbook.offsets[:extsst]
210
+ len += offset[1].to_i
211
+ end
212
+ bytechange = buffer.size - len
213
+ write_boundsheets workbook, writer, oldoffset + bytechange
214
+ reader.seek lastpos
215
+ writer.write reader.read(pos - lastpos)
216
+ buffer.rewind
217
+ writer.write buffer.read
218
+ elsif sst.empty? || workbook.biff_version < 8
219
+ write_boundsheets workbook, writer, oldoffset + bytechange
220
+ else
221
+ write_sst workbook, buffer, writer.pos
222
+ write_boundsheets workbook, writer, oldoffset + buffer.size
223
+ pos = lastpos
224
+ len = positions.min - lastpos
225
+ if len > OPCODE_SIZE
226
+ reader.seek pos
227
+ writer.write reader.read(len - OPCODE_SIZE)
228
+ end
229
+ buffer.rewind
230
+ writer.write buffer.read
231
+ write_eof workbook, writer
232
+ end
233
+ else
234
+ send "write_#{key}", workbook, writer
235
+ end
236
+ lastpos = [pos + len, reader.size - 1].min
237
+ reader.seek lastpos
238
+ end
239
+ writer.write reader.read
240
+ newsheets.each do |sheet|
241
+ writer.write sheet.data
242
+ end
243
+ end
244
+ end
245
+ end
246
+ def write_datemode workbook, writer
247
+ mode = @date_base.year == 1899 ? 0x00 : 0x01
248
+ data = [
249
+ mode, # 0 = Base date is 1899-Dec-31
250
+ # (the cell value 1 represents 1900-Jan-01)
251
+ # 1 = Base date is 1904-Jan-01
252
+ # (the cell value 1 represents 1904-Jan-02)
253
+ ]
254
+ write_op writer, 0x0022, data.pack('v')
255
+ end
256
+ def write_dsf workbook, writer
257
+ data = [
258
+ 0x00, # 0 = Only the BIFF8 “Workbook” stream is present
259
+ # 1 = Additional BIFF5/BIFF7 “Book” stream is in the file
260
+ ]
261
+ write_op writer, 0x0161, data.pack('v')
262
+ end
263
+ def write_encoding workbook, writer
264
+ enc = workbook.encoding || 'UTF-16LE'
265
+ if RUBY_VERSION >= '1.9' && enc.is_a?(Encoding)
266
+ enc = enc.name.upcase
267
+ end
268
+ cp = SEGAPEDOC[enc] or raise "Invalid or Unknown Codepage '#{enc}'"
269
+ write_op writer, 0x0042, [cp].pack('v')
270
+ end
271
+ def write_eof workbook, writer
272
+ write_op writer, 0x000a
273
+ end
274
+ def write_extsst workbook, offsets, writer
275
+ header = [SST_CHUNKSIZE].pack('v')
276
+ data = offsets.collect do |pair| pair.push(0).pack('Vv2') end
277
+ write_op writer, 0x00ff, header, data
278
+ end
279
+ def write_font workbook, writer, font
280
+ # TODO: Colors/Palette index
281
+ size = font.size * TWIPS
282
+ color = SEDOC_ROLOC[font.color] || SEDOC_ROLOC[:text]
283
+ weight = FONT_WEIGHTS.fetch(font.weight, font.weight)
284
+ weight = [[weight, 1000].min, 100].max
285
+ esc = SEPYT_TNEMEPACSE.fetch(font.escapement, 0)
286
+ underline = SEPYT_ENILREDNU.fetch(font.underline, 0)
287
+ family = SEILIMAF_TNOF.fetch(font.family, 0)
288
+ encoding = SGNIDOCNE_TNOF.fetch(font.encoding, 0)
289
+ options = 0
290
+ options |= 0x0001 if weight > 600
291
+ options |= 0x0002 if font.italic?
292
+ options |= 0x0004 if underline > 0
293
+ options |= 0x0008 if font.strikeout?
294
+ options |= 0x0010 if font.outline?
295
+ options |= 0x0020 if font.shadow?
296
+ data = [
297
+ size, # Height of the font (in twips = 1/20 of a point)
298
+ options, # Option flags:
299
+ # Bit Mask Contents
300
+ # 0 0x0001 1 = Characters are bold (redundant, see below)
301
+ # 1 0x0002 1 = Characters are italic
302
+ # 2 0x0004 1 = Characters are underlined (redundant)
303
+ # 3 0x0008 1 = Characters are struck out
304
+ # 4 0x0010 1 = Characters are outlined (djberger)
305
+ # 5 0x0020 1 = Characters are shadowed (djberger)
306
+ color, # Palette index (➜ 6.70)
307
+ weight, # Font weight (100-1000). Standard values are
308
+ # 0x0190 (400) for normal text and
309
+ # 0x02bc (700) for bold text.
310
+ esc, # Escapement type: 0x0000 = None
311
+ # 0x0001 = Superscript
312
+ # 0x0002 = Subscript
313
+ underline,# Underline type: 0x00 = None
314
+ # 0x01 = Single
315
+ # 0x02 = Double
316
+ # 0x21 = Single accounting
317
+ # 0x22 = Double accounting
318
+ family, # Font family: 0x00 = None (unknown or don't care)
319
+ # 0x01 = Roman (variable width, serifed)
320
+ # 0x02 = Swiss (variable width, sans-serifed)
321
+ # 0x03 = Modern (fixed width,
322
+ # serifed or sans-serifed)
323
+ # 0x04 = Script (cursive)
324
+ # 0x05 = Decorative (specialised,
325
+ # e.g. Old English, Fraktur)
326
+ encoding, # Character set: 0x00 = 0 = ANSI Latin
327
+ # 0x01 = 1 = System default
328
+ # 0x02 = 2 = Symbol
329
+ # 0x4d = 77 = Apple Roman
330
+ # 0x80 = 128 = ANSI Japanese Shift-JIS
331
+ # 0x81 = 129 = ANSI Korean (Hangul)
332
+ # 0x82 = 130 = ANSI Korean (Johab)
333
+ # 0x86 = 134 = ANSI Chinese Simplified GBK
334
+ # 0x88 = 136 = ANSI Chinese Traditional BIG5
335
+ # 0xa1 = 161 = ANSI Greek
336
+ # 0xa2 = 162 = ANSI Turkish
337
+ # 0xa3 = 163 = ANSI Vietnamese
338
+ # 0xb1 = 177 = ANSI Hebrew
339
+ # 0xb2 = 178 = ANSI Arabic
340
+ # 0xba = 186 = ANSI Baltic
341
+ # 0xcc = 204 = ANSI Cyrillic
342
+ # 0xde = 222 = ANSI Thai
343
+ # 0xee = 238 = ANSI Latin II (Central European)
344
+ # 0xff = 255 = OEM Latin I
345
+ ]
346
+ name = unicode_string font.name # Font name: Unicode string,
347
+ # 8-bit string length (➜ 3.4)
348
+ write_op writer, opcode(:font), data.pack(binfmt(:font)), name
349
+ end
350
+ def write_fonts workbook, writer
351
+ fonts = @fonts[workbook] = {}
352
+ @formats[workbook].each do |format|
353
+ if(font = format.font) && !fonts.include?(font.key)
354
+ fonts.store font.key, fonts.size
355
+ write_font workbook, writer, font
356
+ end
357
+ end
358
+ end
359
+ def write_formats workbook, writer
360
+ # From BIFF5 on, the built-in number formats will be omitted. The built-in
361
+ # formats are dependent on the current regional settings of the operating
362
+ # system. BUILTIN_FORMATS shows which number formats are used by
363
+ # default in a US-English environment. All indexes from 0 to 163 are
364
+ # reserved for built-in formats.
365
+ # The first user-defined format starts at 164 (0xa4).
366
+ formats = @number_formats[workbook] = {}
367
+ BUILTIN_FORMATS.each do |idx, str|
368
+ formats.store client(str, 'UTF-8'), idx
369
+ end
370
+ ## Ensure at least a 'GENERAL' format is written
371
+ formats.delete client('GENERAL', 'UTF-8')
372
+ idx = 0xa4
373
+ workbook.formats.each do |fmt|
374
+ str = fmt.number_format
375
+ unless formats[str]
376
+ formats.store str, idx
377
+ # Number format string (Unicode string, 16-bit string length, ➜ 3.4)
378
+ write_op writer, opcode(:format), [idx].pack('v'), unicode_string(str, 2)
379
+ idx += 1
380
+ end
381
+ end
382
+ end
383
+ def write_autofilter_NAME_RECORD workbook, writer
384
+ return
385
+ selected_sheet = workbook.worksheets.find {|sheet| sheet.selected }
386
+ return unless selected_sheet.autofilter_enabled
387
+ active_idx = workbook.worksheets.index(selected_sheet)
388
+ data = [
389
+ 0x020, #Options => Built-in name
390
+ 0x000, #Keyboard shortcut
391
+ 0x001, #Length of the name
392
+ 0x00B, #Size of formula
393
+ 0x000, #Not used
394
+ 0x000, #Not used
395
+ 0x000, #Not used
396
+ active_idx + 1, #index to sheet
397
+ 0x000, #Length of menu text
398
+ 0x000, #Length of description text
399
+ 0x000, #Length of help topic text
400
+ 0x000, #Length of status bar text
401
+ 0x000,
402
+ 0x000, #Name -> consolidated area
403
+ #Formula in RPN Size
404
+ 0x00D, #Size
405
+ 0x03B, #AreaPtg id
406
+ active_idx, #sheet index, zero based?
407
+ 0x000,
408
+ 0x000, #firstrow
409
+ 0x000,
410
+ 0x000, #lastrow
411
+ 0x000,
412
+ selected_sheet.autofilter_left_column_index, #firstcolumn
413
+ 0x000,
414
+ selected_sheet.autofilter_right_column_index, #lastcolumn
415
+ 0x000
416
+ ]
417
+ write_op writer, 0x018, data.pack('vC*')
418
+ end
419
+ ##
420
+ # Write a new Excel file.
421
+ def write_from_scratch workbook, io
422
+ sanitize_worksheets workbook.worksheets
423
+ collect_formats workbook
424
+ sheets = worksheets workbook
425
+ buffer1 = StringIO.new ''
426
+ # ● BOF Type = workbook globals (➜ 6.8)
427
+ write_bof workbook, buffer1, :globals
428
+ # ○ File Protection Block ➜ 4.19
429
+ # ○ WRITEACCESS User name (BIFF3-BIFF8, ➜ 5.112)
430
+ # ○ FILESHARING File sharing options (BIFF3-BIFF8, ➜ 5.44)
431
+ # ○ CODEPAGE ➜ 6.17
432
+ write_encoding workbook, buffer1
433
+ # ○ DSF ➜ 6.32
434
+ write_dsf workbook, buffer1
435
+ # ○ TABID
436
+ write_tabid workbook, buffer1
437
+ # ○ FNGROUPCOUNT
438
+ # ○ Workbook Protection Block ➜ 4.18
439
+ # ○ WINDOWPROTECT Window settings: 1 = protected (➜ 5.111)
440
+ # ○ PROTECT Cell contents: 1 = protected (➜ 5.82)
441
+ write_protect workbook, buffer1
442
+ # ○ OBJECTPROTECT Embedded objects: 1 = protected (➜ 5.72)
443
+ # ○ PASSWORD Hash value of the password; 0 = No password (➜ 5.76)
444
+ write_password workbook, buffer1
445
+ # ○ BACKUP ➜ 5.5
446
+ # ○ HIDEOBJ ➜ 5.56
447
+ # ● WINDOW1 ➜ 5.109
448
+ write_window1 workbook, buffer1
449
+ # ○ DATEMODE ➜ 5.28
450
+ write_datemode workbook, buffer1
451
+ # ○ PRECISION ➜ 5.79
452
+ write_precision workbook, buffer1
453
+ # ○ REFRESHALL
454
+ write_refreshall workbook, buffer1
455
+ # ○ BOOKBOOL ➜ 5.9
456
+ write_bookbool workbook, buffer1
457
+ # ●● FONT ➜ 5.45
458
+ write_fonts workbook, buffer1
459
+ # ○○ FORMAT ➜ 5.49
460
+ write_formats workbook, buffer1
461
+ # ●● XF ➜ 5.115
462
+ write_xfs workbook, buffer1
463
+ # ●● STYLE ➜ 5.103
464
+ write_styles workbook, buffer1
465
+ # ○ PALETTE ➜ 5.74
466
+ # ○ USESELFS ➜ 5.106
467
+ buffer1.rewind
468
+ # ●● BOUNDSHEET ➜ 5.95
469
+ buffer2 = StringIO.new ''
470
+ # ○ COUNTRY ➜ 5.22
471
+ # ○ Link Table ➜ 4.10.3
472
+ write_autofilter_NAME_RECORD workbook, buffer2
473
+ # ○○ NAME ➜ 6.66
474
+ # ○ Shared String Table ➜ 4.11
475
+ # ● SST ➜ 5.100
476
+ # ● EXTSST ➜ 5.42
477
+ write_sst workbook, buffer2, buffer1.size
478
+ # ● EOF ➜ 5.37
479
+ write_eof workbook, buffer2
480
+ buffer2.rewind
481
+ # worksheet data can only be assembled after write_sst
482
+ sheets.each do |worksheet| worksheet.write_from_scratch end
483
+ Ole::Storage.open io do |ole|
484
+ ole.file.open 'Workbook', 'w' do |writer|
485
+ writer.write buffer1.read
486
+ write_boundsheets workbook, writer, buffer1.size + buffer2.size
487
+ writer.write buffer2.read
488
+ sheets.each do |worksheet|
489
+ writer.write worksheet.data
490
+ end
491
+ end
492
+ end
493
+ end
494
+ def write_op writer, op, *args
495
+ data = args.join
496
+ limited = data.slice!(0...@recordsize_limit)
497
+ writer.write [op,limited.size].pack("v2")
498
+ writer.write limited
499
+ data
500
+ end
501
+ def write_password workbook, writer
502
+ write_placeholder writer, 0x0013
503
+ end
504
+ def write_placeholder writer, op, value=0x0000, fmt='v'
505
+ write_op writer, op, [value].pack(fmt)
506
+ end
507
+ def write_precision workbook, writer
508
+ # 0 = Use displayed values; 1 = Use real cell values
509
+ write_placeholder writer, 0x000e, 0x0001
510
+ end
511
+ def write_protect workbook, writer
512
+ write_placeholder writer, 0x0012
513
+ end
514
+ def write_refreshall workbook, writer
515
+ write_placeholder writer, 0x01b7
516
+ end
517
+ def write_sst workbook, writer, offset
518
+ # Offset Size Contents
519
+ # 0 4 Total number of strings in the workbook (see below)
520
+ # 4 4 Number of following strings (nm)
521
+ # 8 var. List of nm Unicode strings, 16-bit string length (➜ 3.4)
522
+ strings = worksheets(workbook).inject [] do |memo, worksheet|
523
+ memo.concat worksheet.strings
524
+ end
525
+ total = strings.size
526
+ strings.uniq!
527
+ _write_sst workbook, writer, offset, total, strings
528
+ end
529
+ def _write_sst workbook, writer, offset, total, strings
530
+ sst = {}
531
+ worksheets(workbook).each do |worksheet|
532
+ offset += worksheet.boundsheet_size
533
+ @sst[worksheet] = sst
534
+ end
535
+ sst_size = strings.size
536
+ data = [total, sst_size].pack 'V2'
537
+ op = 0x00fc
538
+ wide = 0
539
+ offsets = []
540
+ strings.each_with_index do |string, idx|
541
+ sst.store string, idx
542
+ op_offset = data.size + 4
543
+ if idx % SST_CHUNKSIZE == 0
544
+ offsets.push [offset + writer.pos + op_offset, op_offset]
545
+ end
546
+ header, packed, next_wide = _unicode_string string, 2
547
+ # the first few bytes (header + first character) must not be split
548
+ must_fit = header.size + wide + 1
549
+ while data.size + must_fit > @recordsize_limit
550
+ op, data, wide = write_string_part writer, op, data, wide
551
+ end
552
+ wide = next_wide
553
+ data << header << packed
554
+ end
555
+ until data.empty?
556
+ op, data, wide = write_string_part writer, op, data, wide
557
+ end
558
+ write_extsst workbook, offsets, writer
559
+ end
560
+ def write_sst_changes workbook, writer, offset, total, strings
561
+ _write_sst workbook, writer, offset, total, strings
562
+ end
563
+ def write_string_part writer, op, data, wide
564
+ bef = data.size
565
+ ## if we're writing wide characters, we need to make sure we don't cut
566
+ # characters in half
567
+ if wide > 0 && data.size > @recordsize_limit
568
+ remove = @recordsize_limit - data.size
569
+ remove -= remove % 2
570
+ rest = data.slice!(remove..-1)
571
+ write_op writer, op, data
572
+ data = rest
573
+ else
574
+ data = write_op writer, op, data
575
+ end
576
+ op = 0x003c
577
+ # Unicode strings are split in a special way. At the beginning of each
578
+ # CONTINUE record the option flags byte is repeated. Only the
579
+ # character size flag will be set in this flags byte, the Rich-Text
580
+ # flag and the Far-East flag are set to zero.
581
+ unless data.empty?
582
+ if wide == 1
583
+ # check if we can compress the rest of the string
584
+ data, wide = compress_unicode_string data
585
+ end
586
+ data = [wide].pack('C') << data
587
+ end
588
+ [op, data, wide]
589
+ end
590
+ def write_styles workbook, writer
591
+ # TODO: Style implementation. The following is simply a standard builtin
592
+ # style.
593
+ # TODO: User defined styles
594
+ data = [
595
+ 0x8000, # Bit Mask Contents
596
+ # 11- 0 0x0fff Index to style XF record (➜ 6.115)
597
+ # 15 0x8000 Always 1 for built-in styles
598
+ 0x00, # Identifier of the built-in cell style:
599
+ # 0x00 = Normal
600
+ # 0x01 = RowLevel_lv (see next field)
601
+ # 0x02 = ColLevel_lv (see next field)
602
+ # 0x03 = Comma
603
+ # 0x04 = Currency
604
+ # 0x05 = Percent
605
+ # 0x06 = Comma [0] (BIFF4-BIFF8)
606
+ # 0x07 = Currency [0] (BIFF4-BIFF8)
607
+ # 0x08 = Hyperlink (BIFF8)
608
+ # 0x09 = Followed Hyperlink (BIFF8)
609
+ 0xff, # Level for RowLevel or ColLevel style (zero-based, lv),
610
+ # 0xff otherwise
611
+ # The RowLevel and ColLevel styles specify the formatting of
612
+ # subtotal cells in a specific outline level. The level is
613
+ # specified by the last field in the STYLE record. Valid values
614
+ # are 0…6 for the outline levels 1…7.
615
+ ]
616
+ write_op writer, 0x0293, data.pack('vC2')
617
+ end
618
+ def write_tabid workbook, writer
619
+ write_op writer, 0x013d, [1].pack('v')
620
+ end
621
+ def write_window1 workbook, writer
622
+ selected = workbook.worksheets.find do |sheet| sheet.selected end
623
+ actidx = workbook.worksheets.index selected
624
+ data = [
625
+ 0x0000, # Horizontal position of the document window
626
+ # (in twips = 1/20 of a point)
627
+ 0x0000, # Vertical position of the document window
628
+ # (in twips = 1/20 of a point)
629
+ 0x4000, # Width of the document window (in twips = 1/20 of a point)
630
+ 0x2000, # Height of the document window (in twips = 1/20 of a point)
631
+ 0x0038, # Option flags:
632
+ # Bit Mask Contents
633
+ # 0 0x0001 0 = Window is visible
634
+ # 1 = Window is hidden
635
+ # 1 0x0002 0 = Window is open
636
+ # 1 = Window is minimised
637
+ # 3 0x0008 0 = Horizontal scroll bar hidden
638
+ # 1 = Horizontal scroll bar visible
639
+ # 4 0x0010 0 = Vertical scroll bar hidden
640
+ # 1 = Vertical scroll bar visible
641
+ # 5 0x0020 0 = Worksheet tab bar hidden
642
+ # 1 = Worksheet tab bar visible
643
+ actidx, # Index to active (displayed) worksheet
644
+ 0x0000, # Index of first visible tab in the worksheet tab bar
645
+ 0x0001, # Number of selected worksheets
646
+ # (highlighted in the worksheet tab bar)
647
+ 0x00e5, # Width of worksheet tab bar (in 1/1000 of window width).
648
+ # The remaining space is used by the horizontal scrollbar.
649
+ ]
650
+ write_op writer, 0x003d, data.pack('v*')
651
+ end
652
+ ##
653
+ # The main writer method. Calls #write_from_scratch or #write_changes
654
+ # depending on the class and state of _workbook_.
655
+ def write_workbook workbook, io
656
+ unless workbook.is_a?(Excel::Workbook) && workbook.io
657
+ @date_base = Date.new 1899, 12, 31
658
+ write_from_scratch workbook, io
659
+ else
660
+ @date_base = workbook.date_base
661
+ if workbook.changes.empty?
662
+ super
663
+ else
664
+ write_changes workbook, io
665
+ end
666
+ end
667
+ ensure
668
+ cleanup workbook
669
+ end
670
+ def write_xfs workbook, writer
671
+ # The default cell format is always present in an Excel file, described by
672
+ # the XF record with the fixed index 15 (0-based). By default, it uses the
673
+ # worksheet/workbook default cell style, described by the very first XF
674
+ # record (index 0).
675
+ @formats[workbook].each do |fmt| fmt.write_xf writer end
676
+ end
677
+ def sst_index worksheet, str
678
+ @sst[worksheet][str]
679
+ end
680
+ def xf_index workbook, format
681
+ if fmt = @formats[workbook].find do |fm| fm.format == format end
682
+ fmt.xf_index
683
+ else
684
+ 0
685
+ end
686
+ end
687
+ end
688
+ end
689
+ end
690
+ end