spreadsheet 0.6.0

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