ruby-spreadsheet 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. data/.document +5 -0
  2. data/GUIDE.txt +267 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +20 -0
  5. data/History.txt +307 -0
  6. data/LICENSE.txt +619 -0
  7. data/README.txt +91 -0
  8. data/Rakefile +53 -0
  9. data/VERSION +1 -0
  10. data/bin/xlsopcodes +18 -0
  11. data/lib/parseexcel.rb +27 -0
  12. data/lib/parseexcel/parseexcel.rb +75 -0
  13. data/lib/parseexcel/parser.rb +11 -0
  14. data/lib/spreadsheet.rb +79 -0
  15. data/lib/spreadsheet/column.rb +71 -0
  16. data/lib/spreadsheet/compatibility.rb +23 -0
  17. data/lib/spreadsheet/datatypes.rb +110 -0
  18. data/lib/spreadsheet/encodings.rb +46 -0
  19. data/lib/spreadsheet/excel.rb +88 -0
  20. data/lib/spreadsheet/excel/error.rb +26 -0
  21. data/lib/spreadsheet/excel/internals.rb +386 -0
  22. data/lib/spreadsheet/excel/internals/biff5.rb +17 -0
  23. data/lib/spreadsheet/excel/internals/biff8.rb +19 -0
  24. data/lib/spreadsheet/excel/offset.rb +41 -0
  25. data/lib/spreadsheet/excel/reader.rb +1173 -0
  26. data/lib/spreadsheet/excel/reader/biff5.rb +22 -0
  27. data/lib/spreadsheet/excel/reader/biff8.rb +193 -0
  28. data/lib/spreadsheet/excel/row.rb +92 -0
  29. data/lib/spreadsheet/excel/sst_entry.rb +46 -0
  30. data/lib/spreadsheet/excel/workbook.rb +80 -0
  31. data/lib/spreadsheet/excel/worksheet.rb +100 -0
  32. data/lib/spreadsheet/excel/writer.rb +1 -0
  33. data/lib/spreadsheet/excel/writer/biff8.rb +75 -0
  34. data/lib/spreadsheet/excel/writer/format.rb +253 -0
  35. data/lib/spreadsheet/excel/writer/workbook.rb +652 -0
  36. data/lib/spreadsheet/excel/writer/worksheet.rb +948 -0
  37. data/lib/spreadsheet/font.rb +92 -0
  38. data/lib/spreadsheet/format.rb +177 -0
  39. data/lib/spreadsheet/formula.rb +9 -0
  40. data/lib/spreadsheet/helpers.rb +11 -0
  41. data/lib/spreadsheet/link.rb +43 -0
  42. data/lib/spreadsheet/row.rb +132 -0
  43. data/lib/spreadsheet/workbook.rb +120 -0
  44. data/lib/spreadsheet/worksheet.rb +279 -0
  45. data/lib/spreadsheet/writer.rb +30 -0
  46. data/ruby-spreadsheet.gemspec +126 -0
  47. data/test/data/test_changes.xls +0 -0
  48. data/test/data/test_copy.xls +0 -0
  49. data/test/data/test_datetime.xls +0 -0
  50. data/test/data/test_empty.xls +0 -0
  51. data/test/data/test_formula.xls +0 -0
  52. data/test/data/test_missing_row.xls +0 -0
  53. data/test/data/test_version_excel5.xls +0 -0
  54. data/test/data/test_version_excel95.xls +0 -0
  55. data/test/data/test_version_excel97.xls +0 -0
  56. data/test/excel/row.rb +35 -0
  57. data/test/excel/writer/worksheet.rb +23 -0
  58. data/test/font.rb +163 -0
  59. data/test/integration.rb +1281 -0
  60. data/test/row.rb +33 -0
  61. data/test/suite.rb +14 -0
  62. data/test/workbook.rb +21 -0
  63. data/test/worksheet.rb +80 -0
  64. metadata +203 -0
@@ -0,0 +1 @@
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 = ''
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,652 @@
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
+ found_selected = false
100
+ sheets.each do |sheet|
101
+ found_selected ||= sheet.selected
102
+ sheet.format_dates!
103
+ end
104
+ unless found_selected
105
+ sheets.first.selected = true
106
+ end
107
+ sheets
108
+ end
109
+ def worksheets workbook
110
+ @worksheets[workbook] ||= workbook.worksheets.collect do |worksheet|
111
+ Excel::Writer::Worksheet.new self, worksheet
112
+ end
113
+ end
114
+ def write_bof workbook, writer, type
115
+ data = [
116
+ @biff_version, # BIFF version (always 0x0600 for BIFF8)
117
+ @bof_types[type], # Type of the following data:
118
+ # 0x0005 = Workbook globals
119
+ # 0x0006 = Visual Basic module
120
+ # 0x0010 = Worksheet
121
+ # 0x0020 = Chart
122
+ # 0x0040 = Macro sheet
123
+ # 0x0100 = Workspace file
124
+ @build_id, # Build identifier
125
+ @build_year, # Build year
126
+ 0x000, # File history flags
127
+ 0x006, # Lowest Excel version that can read
128
+ # all records in this file
129
+ ]
130
+ write_op writer, @bof, data.pack("v4V2")
131
+ end
132
+ def write_bookbool workbook, writer
133
+ write_placeholder writer, 0x00da
134
+ end
135
+ def write_boundsheets workbook, writer, offset
136
+ worksheets = worksheets(workbook)
137
+ worksheets.each do |worksheet|
138
+ # account for boundsheet-entry
139
+ offset += worksheet.boundsheet_size
140
+ end
141
+ worksheets.each do |worksheet|
142
+ data = [
143
+ offset, # Absolute stream position of the BOF record of the sheet
144
+ # represented by this record. This field is never encrypted
145
+ # in protected files.
146
+ 0x00, # Visibility: 0x00 = Visible
147
+ # 0x01 = Hidden
148
+ # 0x02 = Strong hidden (see below)
149
+ 0x00, # Sheet type: 0x00 = Worksheet
150
+ # 0x02 = Chart
151
+ # 0x06 = Visual Basic module
152
+ ]
153
+ write_op writer, 0x0085, data.pack("VC2"), worksheet.name
154
+ offset += worksheet.size
155
+ end
156
+ end
157
+ ##
158
+ # Copy unchanged data verbatim, adjust offsets and write new records for
159
+ # changed data.
160
+ def write_changes workbook, io
161
+ sanitize_worksheets workbook.worksheets
162
+ collect_formats workbook, :existing_document => true
163
+ reader = workbook.ole
164
+ sheet_data = {}
165
+ sst_status, sst_total, sst_strings = complete_sst_update? workbook
166
+ sst = {}
167
+ sst_strings.each_with_index do |str, idx| sst.store str, idx end
168
+ sheets = worksheets(workbook)
169
+ positions = []
170
+ newsheets = []
171
+ sheets.each do |sheet|
172
+ @sst[sheet] = sst
173
+ pos, len = workbook.offsets[sheet.worksheet]
174
+ if pos
175
+ positions.push pos
176
+ sheet.write_changes reader, pos + len, sst_status
177
+ else
178
+ newsheets.push sheet
179
+ sheet.write_from_scratch
180
+ end
181
+ sheet_data[sheet.worksheet] = sheet.data
182
+ end
183
+ Ole::Storage.open io do |ole|
184
+ ole.file.open 'Workbook', 'w' do |writer|
185
+ reader.seek lastpos = 0
186
+ workbook.offsets.select do |key, pair|
187
+ workbook.changes.include? key
188
+ end.sort_by do |key, (pos, len)|
189
+ pos
190
+ end.each do |key, (pos, len)|
191
+ data = reader.read(pos - lastpos)
192
+ writer.write data
193
+ case key
194
+ when Spreadsheet::Worksheet
195
+ writer.write sheet_data[key]
196
+ when :boundsheets
197
+ ## boundsheets are hard to calculate. The offset below is only
198
+ # correct if there are no more changes in the workbook globals
199
+ # string after this.
200
+ oldoffset = positions.min - len
201
+ lastpos = pos + len
202
+ bytechange = 0
203
+ buffer = StringIO.new ''
204
+ if tuple = workbook.offsets[:sst]
205
+ write_sst_changes workbook, buffer, writer.pos,
206
+ sst_total, sst_strings
207
+ pos, len = tuple
208
+ if offset = workbook.offsets[:extsst]
209
+ len += offset[1].to_i
210
+ end
211
+ bytechange = buffer.size - len
212
+ write_boundsheets workbook, writer, oldoffset + bytechange
213
+ reader.seek lastpos
214
+ writer.write reader.read(pos - lastpos)
215
+ buffer.rewind
216
+ writer.write buffer.read
217
+ elsif sst.empty? || workbook.biff_version < 8
218
+ write_boundsheets workbook, writer, oldoffset + bytechange
219
+ else
220
+ write_sst workbook, buffer, writer.pos
221
+ write_boundsheets workbook, writer, oldoffset + buffer.size
222
+ pos = lastpos
223
+ len = positions.min - lastpos
224
+ if len > OPCODE_SIZE
225
+ reader.seek pos
226
+ writer.write reader.read(len - OPCODE_SIZE)
227
+ end
228
+ buffer.rewind
229
+ writer.write buffer.read
230
+ write_eof workbook, writer
231
+ end
232
+ else
233
+ send "write_#{key}", workbook, writer
234
+ end
235
+ lastpos = [pos + len, reader.size - 1].min
236
+ reader.seek lastpos
237
+ end
238
+ writer.write reader.read
239
+ newsheets.each do |sheet|
240
+ writer.write sheet.data
241
+ end
242
+ end
243
+ end
244
+ end
245
+ def write_datemode workbook, writer
246
+ mode = @date_base.year == 1899 ? 0x00 : 0x01
247
+ data = [
248
+ mode, # 0 = Base date is 1899-Dec-31
249
+ # (the cell value 1 represents 1900-Jan-01)
250
+ # 1 = Base date is 1904-Jan-01
251
+ # (the cell value 1 represents 1904-Jan-02)
252
+ ]
253
+ write_op writer, 0x0022, data.pack('v')
254
+ end
255
+ def write_dsf workbook, writer
256
+ data = [
257
+ 0x00, # 0 = Only the BIFF8 “Workbook” stream is present
258
+ # 1 = Additional BIFF5/BIFF7 “Book” stream is in the file
259
+ ]
260
+ write_op writer, 0x0161, data.pack('v')
261
+ end
262
+ def write_encoding workbook, writer
263
+ enc = workbook.encoding || 'UTF-16LE'
264
+ if RUBY_VERSION >= '1.9' && enc.is_a?(Encoding)
265
+ enc = enc.name.upcase
266
+ end
267
+ cp = SEGAPEDOC[enc] or raise "Invalid or Unknown Codepage '#{enc}'"
268
+ write_op writer, 0x0042, [cp].pack('v')
269
+ end
270
+ def write_eof workbook, writer
271
+ write_op writer, 0x000a
272
+ end
273
+ def write_extsst workbook, offsets, writer
274
+ header = [SST_CHUNKSIZE].pack('v')
275
+ data = offsets.collect do |pair| pair.push(0).pack('Vv2') end
276
+ write_op writer, 0x00ff, header, data
277
+ end
278
+ def write_font workbook, writer, font
279
+ # TODO: Colors/Palette index
280
+ size = font.size * TWIPS
281
+ color = SEDOC_ROLOC[font.color] || SEDOC_ROLOC[:text]
282
+ weight = FONT_WEIGHTS.fetch(font.weight, font.weight)
283
+ weight = [[weight, 1000].min, 100].max
284
+ esc = SEPYT_TNEMEPACSE.fetch(font.escapement, 0)
285
+ underline = SEPYT_ENILREDNU.fetch(font.underline, 0)
286
+ family = SEILIMAF_TNOF.fetch(font.family, 0)
287
+ encoding = SGNIDOCNE_TNOF.fetch(font.encoding, 0)
288
+ options = 0
289
+ options |= 0x0001 if weight > 600
290
+ options |= 0x0002 if font.italic?
291
+ options |= 0x0004 if underline > 0
292
+ options |= 0x0008 if font.strikeout?
293
+ options |= 0x0010 if font.outline?
294
+ options |= 0x0020 if font.shadow?
295
+ data = [
296
+ size, # Height of the font (in twips = 1/20 of a point)
297
+ options, # Option flags:
298
+ # Bit Mask Contents
299
+ # 0 0x0001 1 = Characters are bold (redundant, see below)
300
+ # 1 0x0002 1 = Characters are italic
301
+ # 2 0x0004 1 = Characters are underlined (redundant)
302
+ # 3 0x0008 1 = Characters are struck out
303
+ # 4 0x0010 1 = Characters are outlined (djberger)
304
+ # 5 0x0020 1 = Characters are shadowed (djberger)
305
+ color, # Palette index (➜ 6.70)
306
+ weight, # Font weight (100-1000). Standard values are
307
+ # 0x0190 (400) for normal text and
308
+ # 0x02bc (700) for bold text.
309
+ esc, # Escapement type: 0x0000 = None
310
+ # 0x0001 = Superscript
311
+ # 0x0002 = Subscript
312
+ underline,# Underline type: 0x00 = None
313
+ # 0x01 = Single
314
+ # 0x02 = Double
315
+ # 0x21 = Single accounting
316
+ # 0x22 = Double accounting
317
+ family, # Font family: 0x00 = None (unknown or don't care)
318
+ # 0x01 = Roman (variable width, serifed)
319
+ # 0x02 = Swiss (variable width, sans-serifed)
320
+ # 0x03 = Modern (fixed width,
321
+ # serifed or sans-serifed)
322
+ # 0x04 = Script (cursive)
323
+ # 0x05 = Decorative (specialised,
324
+ # e.g. Old English, Fraktur)
325
+ encoding, # Character set: 0x00 = 0 = ANSI Latin
326
+ # 0x01 = 1 = System default
327
+ # 0x02 = 2 = Symbol
328
+ # 0x4d = 77 = Apple Roman
329
+ # 0x80 = 128 = ANSI Japanese Shift-JIS
330
+ # 0x81 = 129 = ANSI Korean (Hangul)
331
+ # 0x82 = 130 = ANSI Korean (Johab)
332
+ # 0x86 = 134 = ANSI Chinese Simplified GBK
333
+ # 0x88 = 136 = ANSI Chinese Traditional BIG5
334
+ # 0xa1 = 161 = ANSI Greek
335
+ # 0xa2 = 162 = ANSI Turkish
336
+ # 0xa3 = 163 = ANSI Vietnamese
337
+ # 0xb1 = 177 = ANSI Hebrew
338
+ # 0xb2 = 178 = ANSI Arabic
339
+ # 0xba = 186 = ANSI Baltic
340
+ # 0xcc = 204 = ANSI Cyrillic
341
+ # 0xde = 222 = ANSI Thai
342
+ # 0xee = 238 = ANSI Latin II (Central European)
343
+ # 0xff = 255 = OEM Latin I
344
+ ]
345
+ name = unicode_string font.name # Font name: Unicode string,
346
+ # 8-bit string length (➜ 3.4)
347
+ write_op writer, opcode(:font), data.pack(binfmt(:font)), name
348
+ end
349
+ def write_fonts workbook, writer
350
+ fonts = @fonts[workbook] = {}
351
+ @formats[workbook].each do |format|
352
+ if(font = format.font) && !fonts.include?(font.key)
353
+ fonts.store font.key, fonts.size
354
+ write_font workbook, writer, font
355
+ end
356
+ end
357
+ end
358
+ def write_formats workbook, writer
359
+ # From BIFF5 on, the built-in number formats will be omitted. The built-in
360
+ # formats are dependent on the current regional settings of the operating
361
+ # system. BUILTIN_FORMATS shows which number formats are used by
362
+ # default in a US-English environment. All indexes from 0 to 163 are
363
+ # reserved for built-in formats.
364
+ # The first user-defined format starts at 164 (0xa4).
365
+ formats = @number_formats[workbook] = {}
366
+ BUILTIN_FORMATS.each do |idx, str|
367
+ formats.store client(str, 'UTF-8'), idx
368
+ end
369
+ ## Ensure at least a 'GENERAL' format is written
370
+ formats.delete client('GENERAL', 'UTF-8')
371
+ idx = 0xa4
372
+ workbook.formats.each do |fmt|
373
+ str = fmt.number_format
374
+ unless formats[str]
375
+ formats.store str, idx
376
+ # Number format string (Unicode string, 16-bit string length, ➜ 3.4)
377
+ write_op writer, opcode(:format), [idx].pack('v'), unicode_string(str, 2)
378
+ idx += 1
379
+ end
380
+ end
381
+ end
382
+ ##
383
+ # Write a new Excel file.
384
+ def write_from_scratch workbook, io
385
+ sanitize_worksheets workbook.worksheets
386
+ collect_formats workbook
387
+ sheets = worksheets workbook
388
+ buffer1 = StringIO.new ''
389
+ # ● BOF Type = workbook globals (➜ 6.8)
390
+ write_bof workbook, buffer1, :globals
391
+ # ○ File Protection Block ➜ 4.19
392
+ # ○ WRITEACCESS User name (BIFF3-BIFF8, ➜ 5.112)
393
+ # ○ FILESHARING File sharing options (BIFF3-BIFF8, ➜ 5.44)
394
+ # ○ CODEPAGE ➜ 6.17
395
+ write_encoding workbook, buffer1
396
+ # ○ DSF ➜ 6.32
397
+ write_dsf workbook, buffer1
398
+ # ○ TABID
399
+ write_tabid workbook, buffer1
400
+ # ○ FNGROUPCOUNT
401
+ # ○ Workbook Protection Block ➜ 4.18
402
+ # ○ WINDOWPROTECT Window settings: 1 = protected (➜ 5.111)
403
+ # ○ PROTECT Cell contents: 1 = protected (➜ 5.82)
404
+ write_protect workbook, buffer1
405
+ # ○ OBJECTPROTECT Embedded objects: 1 = protected (➜ 5.72)
406
+ # ○ PASSWORD Hash value of the password; 0 = No password (➜ 5.76)
407
+ write_password workbook, buffer1
408
+ # ○ BACKUP ➜ 5.5
409
+ # ○ HIDEOBJ ➜ 5.56
410
+ # ● WINDOW1 ➜ 5.109
411
+ write_window1 workbook, buffer1
412
+ # ○ DATEMODE ➜ 5.28
413
+ write_datemode workbook, buffer1
414
+ # ○ PRECISION ➜ 5.79
415
+ write_precision workbook, buffer1
416
+ # ○ REFRESHALL
417
+ write_refreshall workbook, buffer1
418
+ # ○ BOOKBOOL ➜ 5.9
419
+ write_bookbool workbook, buffer1
420
+ # ●● FONT ➜ 5.45
421
+ write_fonts workbook, buffer1
422
+ # ○○ FORMAT ➜ 5.49
423
+ write_formats workbook, buffer1
424
+ # ●● XF ➜ 5.115
425
+ write_xfs workbook, buffer1
426
+ # ●● STYLE ➜ 5.103
427
+ write_styles workbook, buffer1
428
+ # ○ PALETTE ➜ 5.74
429
+ # ○ USESELFS ➜ 5.106
430
+ buffer1.rewind
431
+ # ●● BOUNDSHEET ➜ 5.95
432
+ buffer2 = StringIO.new ''
433
+ # ○ COUNTRY ➜ 5.22
434
+ # ○ Link Table ➜ 4.10.3
435
+ # ○○ NAME ➜ 6.66
436
+ # ○ Shared String Table ➜ 4.11
437
+ # ● SST ➜ 5.100
438
+ # ● EXTSST ➜ 5.42
439
+ write_sst workbook, buffer2, buffer1.size
440
+ # ● EOF ➜ 5.37
441
+ write_eof workbook, buffer2
442
+ buffer2.rewind
443
+ # worksheet data can only be assembled after write_sst
444
+ sheets.each do |worksheet| worksheet.write_from_scratch end
445
+ Ole::Storage.open io do |ole|
446
+ ole.file.open 'Workbook', 'w' do |writer|
447
+ writer.write buffer1.read
448
+ write_boundsheets workbook, writer, buffer1.size + buffer2.size
449
+ writer.write buffer2.read
450
+ sheets.each do |worksheet|
451
+ writer.write worksheet.data
452
+ end
453
+ end
454
+ end
455
+ end
456
+ def write_op writer, op, *args
457
+ data = args.join
458
+ limited = data.slice!(0...@recordsize_limit)
459
+ writer.write [op,limited.size].pack("v2")
460
+ writer.write limited
461
+ data
462
+ end
463
+ def write_password workbook, writer
464
+ write_placeholder writer, 0x0013
465
+ end
466
+ def write_placeholder writer, op, value=0x0000, fmt='v'
467
+ write_op writer, op, [value].pack(fmt)
468
+ end
469
+ def write_precision workbook, writer
470
+ # 0 = Use displayed values; 1 = Use real cell values
471
+ write_placeholder writer, 0x000e, 0x0001
472
+ end
473
+ def write_protect workbook, writer
474
+ write_placeholder writer, 0x0012
475
+ end
476
+ def write_refreshall workbook, writer
477
+ write_placeholder writer, 0x01b7
478
+ end
479
+ def write_sst workbook, writer, offset
480
+ # Offset Size Contents
481
+ # 0 4 Total number of strings in the workbook (see below)
482
+ # 4 4 Number of following strings (nm)
483
+ # 8 var. List of nm Unicode strings, 16-bit string length (➜ 3.4)
484
+ strings = worksheets(workbook).inject [] do |memo, worksheet|
485
+ memo.concat worksheet.strings
486
+ end
487
+ total = strings.size
488
+ strings.uniq!
489
+ _write_sst workbook, writer, offset, total, strings
490
+ end
491
+ def _write_sst workbook, writer, offset, total, strings
492
+ sst = {}
493
+ worksheets(workbook).each do |worksheet|
494
+ offset += worksheet.boundsheet_size
495
+ @sst[worksheet] = sst
496
+ end
497
+ sst_size = strings.size
498
+ data = [total, sst_size].pack 'V2'
499
+ op = 0x00fc
500
+ wide = 0
501
+ offsets = []
502
+ strings.each_with_index do |string, idx|
503
+ sst.store string, idx
504
+ op_offset = data.size + 4
505
+ if idx % SST_CHUNKSIZE == 0
506
+ offsets.push [offset + writer.pos + op_offset, op_offset]
507
+ end
508
+ header, packed, next_wide = _unicode_string string, 2
509
+ # the first few bytes (header + first character) must not be split
510
+ must_fit = header.size + wide + 1
511
+ while data.size + must_fit > @recordsize_limit
512
+ op, data, wide = write_string_part writer, op, data, wide
513
+ end
514
+ wide = next_wide
515
+ data << header << packed
516
+ end
517
+ until data.empty?
518
+ op, data, wide = write_string_part writer, op, data, wide
519
+ end
520
+ write_extsst workbook, offsets, writer
521
+ end
522
+ def write_sst_changes workbook, writer, offset, total, strings
523
+ _write_sst workbook, writer, offset, total, strings
524
+ end
525
+ def write_string_part writer, op, data, wide
526
+ bef = data.size
527
+ ## if we're writing wide characters, we need to make sure we don't cut
528
+ # characters in half
529
+ if wide > 0 && data.size > @recordsize_limit
530
+ remove = @recordsize_limit - data.size
531
+ remove -= remove % 2
532
+ rest = data.slice!(remove..-1)
533
+ write_op writer, op, data
534
+ data = rest
535
+ else
536
+ data = write_op writer, op, data
537
+ end
538
+ op = 0x003c
539
+ # Unicode strings are split in a special way. At the beginning of each
540
+ # CONTINUE record the option flags byte is repeated. Only the
541
+ # character size flag will be set in this flags byte, the Rich-Text
542
+ # flag and the Far-East flag are set to zero.
543
+ unless data.empty?
544
+ if wide == 1
545
+ # check if we can compress the rest of the string
546
+ data, wide = compress_unicode_string data
547
+ end
548
+ data = [wide].pack('C') << data
549
+ end
550
+ [op, data, wide]
551
+ end
552
+ def write_styles workbook, writer
553
+ # TODO: Style implementation. The following is simply a standard builtin
554
+ # style.
555
+ # TODO: User defined styles
556
+ data = [
557
+ 0x8000, # Bit Mask Contents
558
+ # 11- 0 0x0fff Index to style XF record (➜ 6.115)
559
+ # 15 0x8000 Always 1 for built-in styles
560
+ 0x00, # Identifier of the built-in cell style:
561
+ # 0x00 = Normal
562
+ # 0x01 = RowLevel_lv (see next field)
563
+ # 0x02 = ColLevel_lv (see next field)
564
+ # 0x03 = Comma
565
+ # 0x04 = Currency
566
+ # 0x05 = Percent
567
+ # 0x06 = Comma [0] (BIFF4-BIFF8)
568
+ # 0x07 = Currency [0] (BIFF4-BIFF8)
569
+ # 0x08 = Hyperlink (BIFF8)
570
+ # 0x09 = Followed Hyperlink (BIFF8)
571
+ 0xff, # Level for RowLevel or ColLevel style (zero-based, lv),
572
+ # 0xff otherwise
573
+ # The RowLevel and ColLevel styles specify the formatting of
574
+ # subtotal cells in a specific outline level. The level is
575
+ # specified by the last field in the STYLE record. Valid values
576
+ # are 0…6 for the outline levels 1…7.
577
+ ]
578
+ write_op writer, 0x0293, data.pack('vC2')
579
+ end
580
+ def write_tabid workbook, writer
581
+ write_op writer, 0x013d, [1].pack('v')
582
+ end
583
+ def write_window1 workbook, writer
584
+ selected = workbook.worksheets.find do |sheet| sheet.selected end
585
+ actidx = workbook.worksheets.index selected
586
+ data = [
587
+ 0x0000, # Horizontal position of the document window
588
+ # (in twips = 1/20 of a point)
589
+ 0x0000, # Vertical position of the document window
590
+ # (in twips = 1/20 of a point)
591
+ 0x4000, # Width of the document window (in twips = 1/20 of a point)
592
+ 0x2000, # Height of the document window (in twips = 1/20 of a point)
593
+ 0x0038, # Option flags:
594
+ # Bit Mask Contents
595
+ # 0 0x0001 0 = Window is visible
596
+ # 1 = Window is hidden
597
+ # 1 0x0002 0 = Window is open
598
+ # 1 = Window is minimised
599
+ # 3 0x0008 0 = Horizontal scroll bar hidden
600
+ # 1 = Horizontal scroll bar visible
601
+ # 4 0x0010 0 = Vertical scroll bar hidden
602
+ # 1 = Vertical scroll bar visible
603
+ # 5 0x0020 0 = Worksheet tab bar hidden
604
+ # 1 = Worksheet tab bar visible
605
+ actidx, # Index to active (displayed) worksheet
606
+ 0x0000, # Index of first visible tab in the worksheet tab bar
607
+ 0x0001, # Number of selected worksheets
608
+ # (highlighted in the worksheet tab bar)
609
+ 0x00e5, # Width of worksheet tab bar (in 1/1000 of window width).
610
+ # The remaining space is used by the horizontal scrollbar.
611
+ ]
612
+ write_op writer, 0x003d, data.pack('v*')
613
+ end
614
+ ##
615
+ # The main writer method. Calls #write_from_scratch or #write_changes
616
+ # depending on the class and state of _workbook_.
617
+ def write_workbook workbook, io
618
+ unless workbook.is_a?(Excel::Workbook) && workbook.io
619
+ @date_base = Date.new 1899, 12, 31
620
+ write_from_scratch workbook, io
621
+ else
622
+ @date_base = workbook.date_base
623
+ if workbook.changes.empty?
624
+ super
625
+ else
626
+ write_changes workbook, io
627
+ end
628
+ end
629
+ ensure
630
+ cleanup workbook
631
+ end
632
+ def write_xfs workbook, writer
633
+ # The default cell format is always present in an Excel file, described by
634
+ # the XF record with the fixed index 15 (0-based). By default, it uses the
635
+ # worksheet/workbook default cell style, described by the very first XF
636
+ # record (index 0).
637
+ @formats[workbook].each do |fmt| fmt.write_xf writer end
638
+ end
639
+ def sst_index worksheet, str
640
+ @sst[worksheet][str]
641
+ end
642
+ def xf_index workbook, format
643
+ if fmt = @formats[workbook].find do |fm| fm.format == format end
644
+ fmt.xf_index
645
+ else
646
+ 0
647
+ end
648
+ end
649
+ end
650
+ end
651
+ end
652
+ end