ruby-spreadsheet 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
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