ttb-spreadsheet 0.6.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/GUIDE.txt +267 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +18 -0
  4. data/History.txt +365 -0
  5. data/LICENSE.txt +619 -0
  6. data/Manifest.txt +62 -0
  7. data/README.txt +107 -0
  8. data/Rakefile +0 -0
  9. data/bin/xlsopcodes +18 -0
  10. data/lib/parseexcel.rb +27 -0
  11. data/lib/parseexcel/parseexcel.rb +75 -0
  12. data/lib/parseexcel/parser.rb +11 -0
  13. data/lib/spreadsheet.rb +79 -0
  14. data/lib/spreadsheet/column.rb +71 -0
  15. data/lib/spreadsheet/compatibility.rb +23 -0
  16. data/lib/spreadsheet/datatypes.rb +106 -0
  17. data/lib/spreadsheet/encodings.rb +57 -0
  18. data/lib/spreadsheet/excel.rb +88 -0
  19. data/lib/spreadsheet/excel/error.rb +26 -0
  20. data/lib/spreadsheet/excel/internals.rb +365 -0
  21. data/lib/spreadsheet/excel/internals/biff5.rb +17 -0
  22. data/lib/spreadsheet/excel/internals/biff8.rb +19 -0
  23. data/lib/spreadsheet/excel/offset.rb +41 -0
  24. data/lib/spreadsheet/excel/reader.rb +1173 -0
  25. data/lib/spreadsheet/excel/reader/biff5.rb +22 -0
  26. data/lib/spreadsheet/excel/reader/biff8.rb +199 -0
  27. data/lib/spreadsheet/excel/row.rb +92 -0
  28. data/lib/spreadsheet/excel/sst_entry.rb +46 -0
  29. data/lib/spreadsheet/excel/workbook.rb +80 -0
  30. data/lib/spreadsheet/excel/worksheet.rb +100 -0
  31. data/lib/spreadsheet/excel/writer.rb +1 -0
  32. data/lib/spreadsheet/excel/writer/biff8.rb +75 -0
  33. data/lib/spreadsheet/excel/writer/format.rb +253 -0
  34. data/lib/spreadsheet/excel/writer/workbook.rb +690 -0
  35. data/lib/spreadsheet/excel/writer/worksheet.rb +891 -0
  36. data/lib/spreadsheet/font.rb +92 -0
  37. data/lib/spreadsheet/format.rb +177 -0
  38. data/lib/spreadsheet/formula.rb +9 -0
  39. data/lib/spreadsheet/helpers.rb +11 -0
  40. data/lib/spreadsheet/link.rb +43 -0
  41. data/lib/spreadsheet/row.rb +132 -0
  42. data/lib/spreadsheet/workbook.rb +126 -0
  43. data/lib/spreadsheet/worksheet.rb +287 -0
  44. data/lib/spreadsheet/writer.rb +30 -0
  45. data/spreadsheet.gemspec +20 -0
  46. data/test/data/test_changes.xls +0 -0
  47. data/test/data/test_copy.xls +0 -0
  48. data/test/data/test_datetime.xls +0 -0
  49. data/test/data/test_empty.xls +0 -0
  50. data/test/data/test_formula.xls +0 -0
  51. data/test/data/test_long_sst_record.xls +0 -0
  52. data/test/data/test_missing_row.xls +0 -0
  53. data/test/data/test_version_excel5.xls +0 -0
  54. data/test/data/test_version_excel95.xls +0 -0
  55. data/test/data/test_version_excel97.xls +0 -0
  56. data/test/excel/row.rb +35 -0
  57. data/test/excel/writer/workbook.rb +23 -0
  58. data/test/excel/writer/worksheet.rb +24 -0
  59. data/test/font.rb +163 -0
  60. data/test/integration.rb +1311 -0
  61. data/test/row.rb +33 -0
  62. data/test/suite.rb +17 -0
  63. data/test/workbook.rb +29 -0
  64. data/test/worksheet.rb +80 -0
  65. metadata +151 -0
@@ -0,0 +1 @@
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