ricardoo27-writeexcel 0.6.12.1

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 (245) hide show
  1. data/.document +5 -0
  2. data/.gitattributes +1 -0
  3. data/README.rdoc +136 -0
  4. data/Rakefile +52 -0
  5. data/VERSION +1 -0
  6. data/charts/chartex.rb +316 -0
  7. data/charts/demo1.rb +46 -0
  8. data/charts/demo101.bin +0 -0
  9. data/charts/demo2.rb +65 -0
  10. data/charts/demo201.bin +0 -0
  11. data/charts/demo3.rb +117 -0
  12. data/charts/demo301.bin +0 -0
  13. data/charts/demo4.rb +119 -0
  14. data/charts/demo401.bin +0 -0
  15. data/charts/demo5.rb +48 -0
  16. data/charts/demo501.bin +0 -0
  17. data/examples/a_simple.rb +43 -0
  18. data/examples/autofilter.rb +265 -0
  19. data/examples/bigfile.rb +30 -0
  20. data/examples/chart_area.rb +121 -0
  21. data/examples/chart_bar.rb +120 -0
  22. data/examples/chart_column.rb +120 -0
  23. data/examples/chart_line.rb +120 -0
  24. data/examples/chart_pie.rb +108 -0
  25. data/examples/chart_scatter.rb +121 -0
  26. data/examples/chart_stock.rb +148 -0
  27. data/examples/chess.rb +142 -0
  28. data/examples/colors.rb +129 -0
  29. data/examples/comments1.rb +27 -0
  30. data/examples/comments2.rb +352 -0
  31. data/examples/copyformat.rb +52 -0
  32. data/examples/data_validate.rb +279 -0
  33. data/examples/date_time.rb +87 -0
  34. data/examples/defined_name.rb +32 -0
  35. data/examples/demo.rb +124 -0
  36. data/examples/diag_border.rb +36 -0
  37. data/examples/formats.rb +490 -0
  38. data/examples/formula_result.rb +30 -0
  39. data/examples/header.rb +137 -0
  40. data/examples/hide_sheet.rb +29 -0
  41. data/examples/hyperlink.rb +43 -0
  42. data/examples/images.rb +63 -0
  43. data/examples/indent.rb +31 -0
  44. data/examples/merge1.rb +40 -0
  45. data/examples/merge2.rb +45 -0
  46. data/examples/merge3.rb +66 -0
  47. data/examples/merge4.rb +83 -0
  48. data/examples/merge5.rb +80 -0
  49. data/examples/merge6.rb +67 -0
  50. data/examples/outline.rb +255 -0
  51. data/examples/outline_collapsed.rb +209 -0
  52. data/examples/panes.rb +113 -0
  53. data/examples/password_protection.rb +33 -0
  54. data/examples/properties.rb +34 -0
  55. data/examples/properties_jp.rb +33 -0
  56. data/examples/protection.rb +47 -0
  57. data/examples/regions.rb +53 -0
  58. data/examples/repeat.rb +43 -0
  59. data/examples/republic.png +0 -0
  60. data/examples/right_to_left.rb +27 -0
  61. data/examples/row_wrap.rb +53 -0
  62. data/examples/set_first_sheet.rb +14 -0
  63. data/examples/stats.rb +74 -0
  64. data/examples/stocks.rb +81 -0
  65. data/examples/store_formula.rb +15 -0
  66. data/examples/tab_colors.rb +31 -0
  67. data/examples/utf8.rb +15 -0
  68. data/examples/write_arrays.rb +83 -0
  69. data/html/en/doc_en.html +5946 -0
  70. data/html/images/a_simple.jpg +0 -0
  71. data/html/images/area1.jpg +0 -0
  72. data/html/images/bar1.jpg +0 -0
  73. data/html/images/chart_area.xls +0 -0
  74. data/html/images/column1.jpg +0 -0
  75. data/html/images/data_validation.jpg +0 -0
  76. data/html/images/line1.jpg +0 -0
  77. data/html/images/pie1.jpg +0 -0
  78. data/html/images/regions.jpg +0 -0
  79. data/html/images/scatter1.jpg +0 -0
  80. data/html/images/stats.jpg +0 -0
  81. data/html/images/stock1.jpg +0 -0
  82. data/html/images/stocks.jpg +0 -0
  83. data/html/index.html +16 -0
  84. data/html/style.css +433 -0
  85. data/lib/writeexcel.rb +1159 -0
  86. data/lib/writeexcel/biffwriter.rb +223 -0
  87. data/lib/writeexcel/caller_info.rb +12 -0
  88. data/lib/writeexcel/cell_range.rb +332 -0
  89. data/lib/writeexcel/chart.rb +1968 -0
  90. data/lib/writeexcel/charts/area.rb +154 -0
  91. data/lib/writeexcel/charts/bar.rb +177 -0
  92. data/lib/writeexcel/charts/column.rb +156 -0
  93. data/lib/writeexcel/charts/external.rb +66 -0
  94. data/lib/writeexcel/charts/line.rb +154 -0
  95. data/lib/writeexcel/charts/pie.rb +169 -0
  96. data/lib/writeexcel/charts/scatter.rb +192 -0
  97. data/lib/writeexcel/charts/stock.rb +213 -0
  98. data/lib/writeexcel/col_info.rb +87 -0
  99. data/lib/writeexcel/colors.rb +68 -0
  100. data/lib/writeexcel/comments.rb +460 -0
  101. data/lib/writeexcel/compatibility.rb +65 -0
  102. data/lib/writeexcel/convert_date_time.rb +117 -0
  103. data/lib/writeexcel/data_validations.rb +370 -0
  104. data/lib/writeexcel/debug_info.rb +41 -0
  105. data/lib/writeexcel/embedded_chart.rb +35 -0
  106. data/lib/writeexcel/excelformula.y +139 -0
  107. data/lib/writeexcel/excelformulaparser.rb +587 -0
  108. data/lib/writeexcel/format.rb +1575 -0
  109. data/lib/writeexcel/formula.rb +987 -0
  110. data/lib/writeexcel/helper.rb +78 -0
  111. data/lib/writeexcel/image.rb +218 -0
  112. data/lib/writeexcel/olewriter.rb +305 -0
  113. data/lib/writeexcel/outline.rb +24 -0
  114. data/lib/writeexcel/properties.rb +242 -0
  115. data/lib/writeexcel/shared_string_table.rb +153 -0
  116. data/lib/writeexcel/storage_lite.rb +984 -0
  117. data/lib/writeexcel/workbook.rb +2478 -0
  118. data/lib/writeexcel/worksheet.rb +6925 -0
  119. data/lib/writeexcel/worksheets.rb +25 -0
  120. data/lib/writeexcel/write_file.rb +63 -0
  121. data/test/excelfile/Chart1.xls +0 -0
  122. data/test/excelfile/Chart2.xls +0 -0
  123. data/test/excelfile/Chart3.xls +0 -0
  124. data/test/excelfile/Chart4.xls +0 -0
  125. data/test/excelfile/Chart5.xls +0 -0
  126. data/test/helper.rb +31 -0
  127. data/test/perl_output/Chart1.xls.data +0 -0
  128. data/test/perl_output/Chart2.xls.data +0 -0
  129. data/test/perl_output/Chart3.xls.data +0 -0
  130. data/test/perl_output/Chart4.xls.data +0 -0
  131. data/test/perl_output/Chart5.xls.data +0 -0
  132. data/test/perl_output/README +31 -0
  133. data/test/perl_output/a_simple.xls +0 -0
  134. data/test/perl_output/autofilter.xls +0 -0
  135. data/test/perl_output/biff_add_continue_testdata +0 -0
  136. data/test/perl_output/chart_area.xls +0 -0
  137. data/test/perl_output/chart_bar.xls +0 -0
  138. data/test/perl_output/chart_column.xls +0 -0
  139. data/test/perl_output/chart_line.xls +0 -0
  140. data/test/perl_output/chess.xls +0 -0
  141. data/test/perl_output/colors.xls +0 -0
  142. data/test/perl_output/comments0.xls +0 -0
  143. data/test/perl_output/comments1.xls +0 -0
  144. data/test/perl_output/comments2.xls +0 -0
  145. data/test/perl_output/data_validate.xls +0 -0
  146. data/test/perl_output/date_time.xls +0 -0
  147. data/test/perl_output/defined_name.xls +0 -0
  148. data/test/perl_output/demo.xls +0 -0
  149. data/test/perl_output/demo101.bin +0 -0
  150. data/test/perl_output/demo201.bin +0 -0
  151. data/test/perl_output/demo301.bin +0 -0
  152. data/test/perl_output/demo401.bin +0 -0
  153. data/test/perl_output/demo501.bin +0 -0
  154. data/test/perl_output/diag_border.xls +0 -0
  155. data/test/perl_output/f_font_biff +0 -0
  156. data/test/perl_output/f_font_key +1 -0
  157. data/test/perl_output/f_xf_biff +0 -0
  158. data/test/perl_output/file_font_biff +0 -0
  159. data/test/perl_output/file_font_key +1 -0
  160. data/test/perl_output/file_xf_biff +0 -0
  161. data/test/perl_output/formula_result.xls +0 -0
  162. data/test/perl_output/headers.xls +0 -0
  163. data/test/perl_output/hidden.xls +0 -0
  164. data/test/perl_output/hide_zero.xls +0 -0
  165. data/test/perl_output/hyperlink.xls +0 -0
  166. data/test/perl_output/images.xls +0 -0
  167. data/test/perl_output/indent.xls +0 -0
  168. data/test/perl_output/merge1.xls +0 -0
  169. data/test/perl_output/merge2.xls +0 -0
  170. data/test/perl_output/merge3.xls +0 -0
  171. data/test/perl_output/merge4.xls +0 -0
  172. data/test/perl_output/merge5.xls +0 -0
  173. data/test/perl_output/merge6.xls +0 -0
  174. data/test/perl_output/ole_write_header +0 -0
  175. data/test/perl_output/outline.xls +0 -0
  176. data/test/perl_output/outline_collapsed.xls +0 -0
  177. data/test/perl_output/panes.xls +0 -0
  178. data/test/perl_output/password_protection.xls +0 -0
  179. data/test/perl_output/protection.xls +0 -0
  180. data/test/perl_output/regions.xls +0 -0
  181. data/test/perl_output/right_to_left.xls +0 -0
  182. data/test/perl_output/set_first_sheet.xls +0 -0
  183. data/test/perl_output/stats.xls +0 -0
  184. data/test/perl_output/stocks.xls +0 -0
  185. data/test/perl_output/store_formula.xls +0 -0
  186. data/test/perl_output/tab_colors.xls +0 -0
  187. data/test/perl_output/unicode_cyrillic.xls +0 -0
  188. data/test/perl_output/utf8.xls +0 -0
  189. data/test/perl_output/workbook1.xls +0 -0
  190. data/test/perl_output/workbook2.xls +0 -0
  191. data/test/perl_output/ws_colinfo +1 -0
  192. data/test/perl_output/ws_store_colinfo +0 -0
  193. data/test/perl_output/ws_store_dimensions +0 -0
  194. data/test/perl_output/ws_store_filtermode +0 -0
  195. data/test/perl_output/ws_store_filtermode_off +0 -0
  196. data/test/perl_output/ws_store_filtermode_on +0 -0
  197. data/test/perl_output/ws_store_selection +0 -0
  198. data/test/perl_output/ws_store_window2 +1 -0
  199. data/test/republic.png +0 -0
  200. data/test/test_00_IEEE_double.rb +13 -0
  201. data/test/test_01_add_worksheet.rb +10 -0
  202. data/test/test_02_merge_formats.rb +49 -0
  203. data/test/test_04_dimensions.rb +388 -0
  204. data/test/test_05_rows.rb +175 -0
  205. data/test/test_06_extsst.rb +74 -0
  206. data/test/test_11_date_time.rb +475 -0
  207. data/test/test_12_date_only.rb +525 -0
  208. data/test/test_13_date_seconds.rb +477 -0
  209. data/test/test_21_escher.rb +624 -0
  210. data/test/test_22_mso_drawing_group.rb +741 -0
  211. data/test/test_23_note.rb +57 -0
  212. data/test/test_24_txo.rb +74 -0
  213. data/test/test_25_position_object.rb +80 -0
  214. data/test/test_26_autofilter.rb +309 -0
  215. data/test/test_27_autofilter.rb +126 -0
  216. data/test/test_28_autofilter.rb +156 -0
  217. data/test/test_29_process_jpg.rb +670 -0
  218. data/test/test_30_validation_dval.rb +74 -0
  219. data/test/test_31_validation_dv_strings.rb +123 -0
  220. data/test/test_32_validation_dv_formula.rb +203 -0
  221. data/test/test_40_property_types.rb +188 -0
  222. data/test/test_41_properties.rb +235 -0
  223. data/test/test_42_set_properties.rb +434 -0
  224. data/test/test_50_name_stored.rb +295 -0
  225. data/test/test_51_name_print_area.rb +353 -0
  226. data/test/test_52_name_print_titles.rb +450 -0
  227. data/test/test_53_autofilter.rb +199 -0
  228. data/test/test_60_chart_generic.rb +574 -0
  229. data/test/test_61_chart_subclasses.rb +84 -0
  230. data/test/test_62_chart_formats.rb +268 -0
  231. data/test/test_63_chart_area_formats.rb +645 -0
  232. data/test/test_biff.rb +71 -0
  233. data/test/test_big_workbook.rb +17 -0
  234. data/test/test_compatibility.rb +12 -0
  235. data/test/test_example_match.rb +3246 -0
  236. data/test/test_format.rb +1189 -0
  237. data/test/test_formula.rb +61 -0
  238. data/test/test_ole.rb +102 -0
  239. data/test/test_storage_lite.rb +116 -0
  240. data/test/test_workbook.rb +146 -0
  241. data/test/test_worksheet.rb +106 -0
  242. data/utils/add_magic_comment.rb +80 -0
  243. data/writeexcel.gemspec +278 -0
  244. data/writeexcel.rdoc +1425 -0
  245. metadata +292 -0
@@ -0,0 +1,2478 @@
1
+ # -*- coding: utf-8 -*-
2
+ ###############################################################################
3
+ #
4
+ # Workbook - A writer class for Excel Workbooks.
5
+ #
6
+ #
7
+ # Used in conjunction with WriteExcel
8
+ #
9
+ # Copyright 2000-2010, John McNamara, jmcnamara@cpan.org
10
+ #
11
+ # original written in Perl by John McNamara
12
+ # converted to Ruby by Hideo Nakamura, cxn03651@msj.biglobe.ne.jp
13
+ #
14
+ require 'nkf'
15
+ require 'forwardable'
16
+ require 'writeexcel/biffwriter'
17
+ require 'writeexcel/worksheet'
18
+ require 'writeexcel/chart'
19
+ require 'writeexcel/format'
20
+ require 'writeexcel/formula'
21
+ require 'writeexcel/olewriter'
22
+ require 'writeexcel/storage_lite'
23
+ require 'writeexcel/compatibility'
24
+ require 'writeexcel/shared_string_table'
25
+ require 'writeexcel/worksheets'
26
+
27
+ class Workbook < BIFFWriter
28
+ require 'writeexcel/properties'
29
+ require 'writeexcel/helper'
30
+
31
+ extend Forwardable
32
+
33
+ attr_reader :url_format, :parser, :tempdir, :date_1904
34
+ attr_reader :compatibility, :palette
35
+ attr_reader :ext_refs
36
+ attr_reader :worksheets
37
+
38
+ BOF = 12 # :nodoc:
39
+ EOF = 4 # :nodoc:
40
+
41
+ #
42
+ # _file_ is a filename (as string) or io object where to out spreadsheet data.
43
+ # you can set default format of workbook using _default_formats_.
44
+ #
45
+ # A new Excel workbook is created using the new() constructor which accepts
46
+ # either a filename or an IO object as a parameter. The following example
47
+ # creates a new Excel file based on a filename:
48
+ #
49
+ # workbook = WriteExcel.new('filename.xls')
50
+ # worksheet = workbook.add_worksheet
51
+ # worksheet.write(0, 0, 'Hi Excel!')
52
+ #
53
+ # Here are some other examples of using new():
54
+ #
55
+ # workbook1 = WriteExcel.new(filename)
56
+ # workbook2 = WriteExcel.new('/tmp/filename.xls')
57
+ # workbook3 = WriteExcel.new("c:\\tmp\\filename.xls")
58
+ # workbook4 = WriteExcel.new('c:\tmp\filename.xls')
59
+ #
60
+ # The last two examples demonstrates how to create a file on DOS or
61
+ # Windows where it is necessary to either escape the directory
62
+ # separator \ or to use single quotes to ensure that it isn't interpolated.
63
+ #
64
+ # The new() constructor returns a WriteExcel object that you can use to add
65
+ # worksheets and store data.
66
+ #
67
+ # If the file cannot be created, due to file permissions or some other reason,
68
+ # new will raise Exception Errno::EXXX.
69
+ #
70
+ # You can also pass a valid IO object to the new() constructor.:
71
+ # require 'stringio'
72
+ #
73
+ # io = StringIO.new
74
+ # workbook = WriteExcel.new(io) # After workbook.close, you can get excel data as io.string
75
+ #
76
+ # And, you can also pass default format properties.
77
+ #
78
+ # workbook = WriteExcel.new(filename, :font => 'Courier New', :size => 11)
79
+ #
80
+ def initialize(file, default_formats = {})
81
+ super()
82
+ @file = file
83
+ @default_formats = default_formats
84
+ @parser = Writeexcel::Formula.new(@byte_order)
85
+ @tempdir = nil
86
+ @date_1904 = false
87
+ @xf_index = 0
88
+ @biffsize = 0
89
+ @sheet_count = 0
90
+ @chart_count = 0
91
+ @codepage = 0x04E4
92
+ @country = 1
93
+ @worksheets = Worksheets.new
94
+ @formats = []
95
+ @palette = []
96
+ @biff_only = 0
97
+
98
+ @internal_fh = 0
99
+ @fh_out = ""
100
+
101
+ @shared_string_table = SharedStringTable.new
102
+ @extsst_offsets = [] # array of [global_offset, local_offset]
103
+ @extsst_buckets = 0
104
+ @extsst_bucket_size = 0
105
+
106
+ @ext_refs = {}
107
+
108
+ @mso_clusters = []
109
+ @mso_size = 0
110
+
111
+ @hideobj = false
112
+
113
+ @summary = ''
114
+ @doc_summary = ''
115
+ @localtime = Time.now
116
+
117
+ @defined_names = []
118
+
119
+ setup_built_in_formats(default_formats)
120
+
121
+ # Add the default format for hyperlinks
122
+ @url_format = add_format(:color => 'blue', :underline => 1)
123
+
124
+ if file.respond_to?(:to_str) && file != ''
125
+ @fh_out = open(file, "wb")
126
+ @internal_fh = 1
127
+ else
128
+ @fh_out = file
129
+ end
130
+
131
+ # Set colour palette.
132
+ set_palette_xl97
133
+ end
134
+
135
+ # return custom format from array at index
136
+ def format(index)
137
+ @formats[22+index]
138
+ end
139
+
140
+ #
141
+ # Calls finalization methods and explicitly close the OLEwriter files
142
+ # handle.
143
+ #
144
+ # An explicit close() is required if the file must be closed prior to performing
145
+ # some external action on it such as copying it, reading its size or attaching
146
+ # it to an email.
147
+ #
148
+ # In general, if you create a file with a size of 0 bytes or you fail to create
149
+ # a file you need to call close().
150
+ #
151
+ def close
152
+ return if fileclosed? # Prevent close() from being called twice.
153
+
154
+ @fileclosed = true
155
+ store_workbook
156
+ cleanup
157
+ end
158
+
159
+ # get array of Worksheet objects
160
+ #
161
+ # :call-seq:
162
+ # sheets -> array of all Wordsheet object
163
+ # sheets(1, 3, 4) -> array of spcified Worksheet object.
164
+ #
165
+ # The sheets() method returns a array, or a sliced array, of the worksheets
166
+ # in a workbook.
167
+ #
168
+ # If no arguments are passed the method returns a list of all the worksheets
169
+ # in the workbook. This is useful if you want to repeat an operation on each
170
+ # worksheet:
171
+ #
172
+ # workbook.sheets.each do |worksheet|
173
+ # print worksheet.get_name
174
+ # end
175
+ #
176
+ # You can also specify a slice list to return one or more worksheet objects:
177
+ #
178
+ # worksheet = workbook.sheets(0)
179
+ # worksheet.write('A1', 'Hello')
180
+ #
181
+ # you can write the above example as:
182
+ #
183
+ # workbook.sheets(0).write('A1', 'Hello')
184
+ #
185
+ # The following example returns the first and last worksheet in a workbook:
186
+ #
187
+ # workbook.sheets(0, -1).each do |sheet|
188
+ # # Do something
189
+ # end
190
+ #
191
+ def sheets(*args)
192
+ if args.empty?
193
+ @worksheets
194
+ else
195
+ args.collect{|i| @worksheets[i] }
196
+ end
197
+ end
198
+
199
+ #
200
+ # Add a new worksheet to the Excel workbook.
201
+ #
202
+ # if _sheetname_ is UTF-16BE format, pass true as _name_utf16be_.
203
+ #
204
+ # At least one worksheet should be added to a new workbook. A worksheet is
205
+ # used to write data into cells:
206
+ #
207
+ # worksheet1 = workbook.add_worksheet # Sheet1
208
+ # worksheet2 = workbook.add_worksheet('Foglio2') # Foglio2
209
+ # worksheet3 = workbook.add_worksheet('Data') # Data
210
+ # worksheet4 = workbook.add_worksheet # Sheet4
211
+ #
212
+ # If _sheetname_ is not specified the default Excel convention will be followed,
213
+ # i.e. Sheet1, Sheet2, etc. The utf_16_be parameter is optional, see below.
214
+ #
215
+ # The worksheet name must be a valid Excel worksheet name, i.e. it cannot
216
+ # contain any of the following characters, [ ] : * ? / \
217
+ # and it must be less than 32 characters. In addition, you cannot use the same,
218
+ # case insensitive, _sheetname_ for more than one worksheet.
219
+ #
220
+ # This method will also handle strings in UTF-8 format.
221
+ #
222
+ # worksheet = workbook.add_worksheet("シート名")
223
+ #
224
+ # UTF-16BE worksheet names using an additional optional parameter:
225
+ #
226
+ # name = [0x263a].pack('n')
227
+ # worksheet = workbook.add_worksheet(name, true) # Smiley
228
+ #
229
+ def add_worksheet(sheetname = '', name_utf16be = false)
230
+ name, name_utf16be = check_sheetname(sheetname, name_utf16be)
231
+
232
+ init_data = [
233
+ self,
234
+ name,
235
+ name_utf16be
236
+ ]
237
+ worksheet = Writeexcel::Worksheet.new(*init_data)
238
+ @worksheets << worksheet # Store ref for iterator
239
+ @parser.set_ext_sheets(name, worksheet.index) # Store names in Formula.rb
240
+ worksheet
241
+ end
242
+
243
+ #
244
+ # Create a chart for embedding or as as new sheet.
245
+ #
246
+ # This method is use to create a new chart either as a standalone worksheet
247
+ # (the default) or as an embeddable object that can be inserted into a
248
+ # worksheet via the insert_chart() Worksheet method.
249
+ #
250
+ # chart = workbook.add_chart(:type => 'Chart::Column')
251
+ #
252
+ # The properties that can be set are:
253
+ #
254
+ # :type (required)
255
+ # :name (optional)
256
+ # :name_utf16be (optional)
257
+ # :embedded (optional)
258
+ #
259
+ # * :type
260
+ #
261
+ # This is a required parameter. It defines the type of chart that will be created.
262
+ #
263
+ # chart = workbook.add_chart(:type => 'Chart::Line')
264
+ #
265
+ # The available types are:
266
+ #
267
+ # 'Chart::Column'
268
+ # 'Chart::Bar'
269
+ # 'Chart::Line'
270
+ # 'Chart::Area'
271
+ # 'Chart::Pie'
272
+ # 'Chart::Scatter'
273
+ # 'Chart::Stock'
274
+ #
275
+ # * :name
276
+ #
277
+ # Set the name for the chart sheet. The name property is optional and
278
+ # if it isn't supplied will default to Chart1 .. n. The name must be
279
+ # a valid Excel worksheet name. See add_worksheet() for more details
280
+ # on valid sheet names. The :name property can be omitted for embedded
281
+ # charts.
282
+ #
283
+ # chart = workbook.add_chart(
284
+ # :type => 'Chart::Line',
285
+ # :name => 'Results Chart'
286
+ # )
287
+ #
288
+ # * :name_utf16be
289
+ #
290
+ # if :name is UTF-16BE format, pass true as :name_utf16be
291
+ #
292
+ # * :encoding
293
+ #
294
+ # if :name is UTF-16BE format, pass 1 as :encoding.
295
+ # This key is obsolete in v0.7 or later. Use :name_utf16be instead.
296
+ #
297
+ # * :embedded
298
+ #
299
+ # Specifies true that the Chart object will be inserted in a worksheet via
300
+ # the insert_chart() Worksheet method. It is an error to try insert a
301
+ # Chart that doesn't have this flag set.
302
+ #
303
+ # chart = workbook.add_chart(:type => 'Chart::Line', :embedded => true)
304
+ #
305
+ # # Configure the chart.
306
+ # ...
307
+ #
308
+ # # Insert the chart into the a worksheet.
309
+ # worksheet.insert_chart('E2', chart)
310
+ #
311
+ # See WriteExcel::Chart for details on how to configure the
312
+ # chart object once it is created. See also the chart_*.rb programs in the
313
+ # examples directory of the distro.
314
+ #
315
+ def add_chart(properties)
316
+ name = ''
317
+ name_utf16be = false
318
+
319
+ # Type must be specified so we can create the required chart instance.
320
+ type = properties[:type]
321
+ raise "Must define chart type in add_chart()" if type.nil?
322
+
323
+ # Ensure that the chart defaults to non embedded.
324
+ embedded = properties[:embedded]
325
+
326
+ # Check the worksheet name for non-embedded charts.
327
+ unless embedded
328
+ properties[:name_utf16be] = true if properties[:encoding] == 1
329
+ name, name_utf16be =
330
+ check_sheetname(properties[:name], properties[:name_utf16be], true)
331
+ end
332
+
333
+ init_data = [
334
+ self,
335
+ name,
336
+ name_utf16be
337
+ ]
338
+
339
+ chart = Writeexcel::Chart.factory(type, *init_data)
340
+ # If the chart isn't embedded let the workbook control it.
341
+ if !embedded
342
+ @worksheets << chart # Store ref for iterator
343
+ else
344
+ chart.set_embedded_config_data
345
+ end
346
+ chart
347
+ end
348
+
349
+ #
350
+ # Add an externally created chart.
351
+ #
352
+ # This method is use to include externally generated charts in a WriteExcel
353
+ # file.
354
+ #
355
+ # chart = workbook.add_chart_ext('chart01.bin', 'Chart1')
356
+ #
357
+ # This feature is semi-deprecated in favour of the "native" charts created
358
+ # using add_chart(). Read external_charts.txt in the external_charts
359
+ # directory of the distro for a full explanation.
360
+ #
361
+ def add_chart_ext(filename, chartname, name_utf16be = false)
362
+ type = 'extarnal'
363
+
364
+ name, name_utf16be = check_sheetname(chartname, name_utf16be)
365
+
366
+ init_data = [
367
+ filename,
368
+ name,
369
+ name_utf16be
370
+ ]
371
+
372
+ chart = Writeexcel::Chart.factory(self, type, init_data)
373
+ @worksheets << chart # Store ref for iterator
374
+ chart
375
+ end
376
+
377
+ #
378
+ # The add_format method can be used to create new Format objects which are
379
+ # used to apply formatting to a cell. You can either define the properties
380
+ # at creation time via a hash of property values or later via method calls.
381
+ #
382
+ # format1 = workbook.add_format(props) # Set properties at creation
383
+ # format2 = workbook.add_format # Set properties later
384
+ #
385
+ # See the "CELL FORMATTING" section for more details about Format properties and how to set them.
386
+ #
387
+ def add_format(*args)
388
+ fmts = {}
389
+ args.each { |arg| fmts = fmts.merge(arg) }
390
+ format = Writeexcel::Format.new(@xf_index, @default_formats.merge(fmts))
391
+ @xf_index += 1
392
+ @formats.push format # Store format reference
393
+ format
394
+ end
395
+
396
+ #
397
+ # Set the compatibility mode.
398
+ #
399
+ # This method is used to improve compatibility with third party
400
+ # applications that read Excel files.
401
+ #
402
+ # workbook.compatibility_mode
403
+ #
404
+ # An Excel file is comprised of binary records that describe properties of
405
+ # a spreadsheet. Excel is reasonably liberal about this and, outside of a
406
+ # core subset, it doesn't require every possible record to be present when
407
+ # it reads a file. This is also true of Gnumeric and OpenOffice.Org Calc.
408
+ #
409
+ # WriteExcel takes advantage of this fact to omit some records in order to
410
+ # minimise the amount of data stored in memory and to simplify and speed up
411
+ # the writing of files. However, some third party applications that read
412
+ # Excel files often expect certain records to be present. In
413
+ # "compatibility mode" WriteExcel writes these records and tries to be as
414
+ # close to an Excel generated file as possible.
415
+ #
416
+ # Applications that require compatibility_mode() are Apache POI,
417
+ # Apple Numbers, and Quickoffice on Nokia, Palm and other devices. You should
418
+ # also use compatibility_mode() if your Excel file will be used as an external
419
+ # data source by another Excel file.
420
+ #
421
+ # If you encounter other situations that require compatibility_mode(),
422
+ # please let me know.
423
+ #
424
+ # It should be noted that compatibility_mode() requires additional data to be
425
+ # stored in memory and additional processing. This incurs a memory and speed
426
+ # penalty and may not be suitable for very large files (>20MB).
427
+ #
428
+ # You must call compatibility_mode() before calling add_worksheet().
429
+ #
430
+ #
431
+ # Excel doesn't require every possible Biff record to be present in a file.
432
+ # In particular if the indexing records INDEX, ROW and DBCELL aren't present
433
+ # it just ignores the fact and reads the cells anyway. This is also true of
434
+ # the EXTSST record. Gnumeric and OOo also take this approach. This allows
435
+ # WriteExcel to ignore these records in order to minimise the amount of data
436
+ # stored in memory. However, other third party applications that read Excel
437
+ # files often expect these records to be present. In "compatibility mode"
438
+ # WriteExcel writes these records and tries to be as close to an Excel
439
+ # generated file as possible.
440
+ #
441
+ # This requires additional data to be stored in memory until the file is
442
+ # about to be written. This incurs a memory and speed penalty and may not be
443
+ # suitable for very large files.
444
+ #
445
+ def compatibility_mode(mode = true)
446
+ unless sheets.empty?
447
+ raise "compatibility_mode() must be called before add_worksheet()"
448
+ end
449
+ @compatibility = mode
450
+ end
451
+
452
+ #
453
+ # Set the date system: false = 1900 (the default), true = 1904
454
+ #
455
+ # Excel stores dates as real numbers where the integer part stores the
456
+ # number of days since the epoch and the fractional part stores the
457
+ # percentage of the day. The epoch can be either 1900 or 1904. Excel for
458
+ # Windows uses 1900 and Excel for Macintosh uses 1904. However, Excel on
459
+ # either platform will convert automatically between one system and
460
+ # the other.
461
+ #
462
+ # WriteExcel stores dates in the 1900 format by default. If you wish to
463
+ # change this you can call the set_1904() workbook method. You can query
464
+ # the current value by calling the get_1904() workbook method. This returns
465
+ # false for 1900 and true for 1904.
466
+ #
467
+ # See also "DATES AND TIME IN EXCEL" for more information about working
468
+ # with Excel's date system.
469
+ #
470
+ # In general you probably won't need to use set_1904().
471
+ #
472
+ def set_1904(mode = true)
473
+ unless sheets.empty?
474
+ raise "set_1904() must be called before add_worksheet()"
475
+ end
476
+ @date_1904 = (!mode || mode == 0) ? false : true
477
+ end
478
+
479
+ def get_1904
480
+ @date_1904
481
+ end
482
+
483
+ #
484
+ # Change the RGB components of the elements in the colour palette.
485
+ #
486
+ # The set_custom_color() method can be used to override one of the built-in
487
+ # palette values with a more suitable colour.
488
+ #
489
+ # The value for _index_ should be in the range 8..63, see "COLOURS IN EXCEL".
490
+ #
491
+ # The default named colours use the following indices:
492
+ #
493
+ # 8 => black
494
+ # 9 => white
495
+ # 10 => red
496
+ # 11 => lime
497
+ # 12 => blue
498
+ # 13 => yellow
499
+ # 14 => magenta
500
+ # 15 => cyan
501
+ # 16 => brown
502
+ # 17 => green
503
+ # 18 => navy
504
+ # 20 => purple
505
+ # 22 => silver
506
+ # 23 => gray
507
+ # 33 => pink
508
+ # 53 => orange
509
+ #
510
+ # A new colour is set using its RGB (red green blue) components. The red,
511
+ # green and blue values must be in the range 0..255. You can determine the
512
+ # required values in Excel using the Tools->Options->Colors->Modify dialog.
513
+ #
514
+ # The set_custom_color() workbook method can also be used with a HTML style
515
+ # #rrggbb hex value:
516
+ #
517
+ # workbook.set_custom_color(40, 255, 102, 0 ) # Orange
518
+ # workbook.set_custom_color(40, 0xFF, 0x66, 0x00) # Same thing
519
+ # workbook.set_custom_color(40, '#FF6600' ) # Same thing
520
+ #
521
+ # font = workbook.add_format(:color => 40) # Use the modified colour
522
+ #
523
+ # The return value from set_custom_color() is the index of the colour that
524
+ # was changed:
525
+ #
526
+ # ferrari = workbook.set_custom_color(40, 216, 12, 12)
527
+ #
528
+ # format = workbook.add_format(
529
+ # :bg_color => $ferrari,
530
+ # :pattern => 1,
531
+ # :border => 1
532
+ # )
533
+ #
534
+ def set_custom_color(index, red = nil, green = nil, blue = nil)
535
+ # Match a HTML #xxyyzz style parameter
536
+ if !red.nil? && red =~ /^#(\w\w)(\w\w)(\w\w)/
537
+ red = $1.hex
538
+ green = $2.hex
539
+ blue = $3.hex
540
+ end
541
+
542
+ # Check that the colour index is the right range
543
+ if index < 8 || index > 64
544
+ raise "Color index #{index} outside range: 8 <= index <= 64"
545
+ end
546
+
547
+ # Check that the colour components are in the right range
548
+ if (red < 0 || red > 255) ||
549
+ (green < 0 || green > 255) ||
550
+ (blue < 0 || blue > 255)
551
+ raise "Color component outside range: 0 <= color <= 255"
552
+ end
553
+
554
+ index -=8 # Adjust colour index (wingless dragonfly)
555
+
556
+ # Set the RGB value
557
+ @palette[index] = [red, green, blue, 0]
558
+
559
+ index + 8
560
+ end
561
+
562
+ #
563
+ # Change the default temp directory
564
+ #
565
+ # For speed and efficiency WriteExcel stores worksheet data in temporary
566
+ # files prior to assembling the final workbook.
567
+ #
568
+ # If WriteExcel is unable to create these temporary files it will store
569
+ # the required data in memory. This can be slow for large files.
570
+ #
571
+ # The problem occurs mainly with IIS on Windows although it could feasibly
572
+ # occur on Unix systems as well. The problem generally occurs because the
573
+ # default temp file directory is defined as C:/ or some other directory that
574
+ # IIS doesn't provide write access to.
575
+ #
576
+ # To check if this might be a problem on a particular system you can run a
577
+ # simple test program with -w or use warnings. This will generate a warning
578
+ # if the module cannot create the required temporary files:
579
+ #
580
+ # #!/usr/bin/ruby -w
581
+ #
582
+ # require 'WriteExcel'
583
+ #
584
+ # workbook = WriteExcel.new('test.xls')
585
+ # worksheet = workbook.add_worksheet
586
+ # workbook.close
587
+ #
588
+ # To avoid this problem the set_tempdir() method can be used to specify a
589
+ # directory that is accessible for the creation of temporary files.
590
+ #
591
+ # Even if the default temporary file directory is accessible you may wish
592
+ # to specify an alternative location for security or maintenance reasons:
593
+ #
594
+ # workbook.set_tempdir('/tmp/writeexcel')
595
+ # workbook.set_tempdir('c:\windows\temp\writeexcel')
596
+ #
597
+ # The directory for the temporary file must exist, set_tempdir() will not
598
+ # create a new directory.
599
+ #
600
+ # One disadvantage of using the set_tempdir() method is that on some Windows
601
+ # systems it will limit you to approximately 800 concurrent tempfiles. This
602
+ # means that a single program running on one of these systems will be limited
603
+ # to creating a total of 800 workbook and worksheet objects. You can run
604
+ # multiple, non-concurrent programs to work around this if necessary.
605
+ #
606
+ def set_tempdir(dir = '')
607
+ raise "#{dir} is not a valid directory" if dir != '' && !FileTest.directory?(dir)
608
+ raise "set_tempdir must be called before add_worksheet" unless sheets.empty?
609
+
610
+ @tempdir = dir
611
+ end
612
+
613
+ #
614
+ # The default code page or character set used by WriteExcel is ANSI. This is
615
+ # also the default used by Excel for Windows. Occasionally however it may be
616
+ # necessary to change the code page via the set_codepage() method.
617
+ #
618
+ # Changing the code page may be required if your are using WriteExcel on the
619
+ # Macintosh and you are using characters outside the ASCII 128 character set:
620
+ #
621
+ # workbook.set_codepage(1) # ANSI, MS Windows
622
+ # workbook.set_codepage(2) # Apple Macintosh
623
+ #
624
+ # The set_codepage() method is rarely required.
625
+ #
626
+ def set_codepage(type = 1)
627
+ if type == 2
628
+ @codepage = 0x8000
629
+ else
630
+ @codepage = 0x04E4
631
+ end
632
+ end
633
+
634
+ #
635
+ # store the country code.
636
+ #
637
+ # Some non-english versions of Excel may need this set to some value other
638
+ # than 1 = "United States". In general the country code is equal to the
639
+ # international dialling code.
640
+ #
641
+ def set_country(code = 1)
642
+ @country = code
643
+ end
644
+
645
+ #
646
+ # This method is used to defined a name that can be used to represent a
647
+ # value, a single cell or a range of cells in a workbook.
648
+ #
649
+ # workbook.define_name('Exchange_rate', '=0.96')
650
+ # workbook.define_name('Sales', '=Sheet1!$G$1:$H$10')
651
+ # workbook.define_name('Sheet2!Sales', '=Sheet2!$G$1:$G$10')
652
+ #
653
+ # See the defined_name.rb program in the examples dir of the distro.
654
+ #
655
+ # Note: This currently a beta feature. More documentation and examples
656
+ # will be added.
657
+ #
658
+ def define_name(name, formula, encoding = 0)
659
+ sheet_index = 0
660
+ full_name = name.downcase
661
+
662
+ if name =~ /^(.*)!(.*)$/
663
+ sheetname = $1
664
+ name = $2
665
+ sheet_index = 1 + @parser.get_sheet_index(sheetname)
666
+ end
667
+
668
+ # Strip the = sign at the beginning of the formula string
669
+ formula = formula.sub(/^=/, '')
670
+
671
+ # Parse the formula using the parser in Formula.pm
672
+ parser = @parser
673
+
674
+ # In order to raise formula errors from the point of view of the calling
675
+ # program we use an eval block and re-raise the error from here.
676
+ #
677
+ tokens = parser.parse_formula(formula)
678
+
679
+ # Force 2d ranges to be a reference class.
680
+ tokens.collect! { |t| t.gsub(/_ref3d/, '_ref3dR') }
681
+ tokens.collect! { |t| t.gsub(/_range3d/, '_range3dR') }
682
+
683
+ # Parse the tokens into a formula string.
684
+ formula = parser.parse_tokens(tokens)
685
+
686
+ defined_names.push(
687
+ {
688
+ :name => name,
689
+ :encoding => encoding,
690
+ :sheet_index => sheet_index,
691
+ :formula => formula
692
+ }
693
+ )
694
+
695
+ index = defined_names.size
696
+
697
+ parser.set_ext_name(name, index)
698
+ end
699
+
700
+ #
701
+ # Set the document properties such as Title, Author etc. These are written to
702
+ # property sets in the OLE container.
703
+ #
704
+ # The set_properties method can be used to set the document properties of
705
+ # the Excel file created by WriteExcel. These properties are visible when you
706
+ # use the File->Properties menu option in Excel and are also available to
707
+ # external applications that read or index windows files.
708
+ #
709
+ # The properties should be passed as a hash of values as follows:
710
+ #
711
+ # workbook.set_properties(
712
+ # :title => 'This is an example spreadsheet',
713
+ # :author => 'cxn03651',
714
+ # :comments => 'Created with Ruby and WriteExcel',
715
+ # )
716
+ #
717
+ # The properties that can be set are:
718
+ #
719
+ # * :title
720
+ # * :subject
721
+ # * :author
722
+ # * :manager
723
+ # * :company
724
+ # * :category
725
+ # * :keywords
726
+ # * :comments
727
+ #
728
+ # User defined properties are not supported due to effort required.
729
+ #
730
+ # You can also pass UTF-8 strings as properties.
731
+ #
732
+ # workbook.set_properties(
733
+ # :subject => "住所録"
734
+ # )
735
+ #
736
+ # Usually WriteExcel allows you to use UTF-16. However, document properties
737
+ # don't support UTF-16 for these type of strings.
738
+ #
739
+ # In order to promote the usefulness of Ruby and the WriteExcel module
740
+ # consider adding a comment such as the following when using document
741
+ # properties:
742
+ #
743
+ # workbook.set_properties(
744
+ # ...,
745
+ # :comments => 'Created with Ruby and writeexcel',
746
+ # ...,
747
+ # )
748
+ #
749
+ # See also the properties.rb program in the examples directory of the distro.
750
+ #
751
+ def set_properties(params)
752
+ # Ignore if no args were passed.
753
+ return -1 if !params.respond_to?(:to_hash) || params.empty?
754
+
755
+ params.each do |k, v|
756
+ params[k] = convert_to_ascii_if_ascii(v) if v.respond_to?(:to_str)
757
+ end
758
+
759
+ # Check for valid input parameters.
760
+ check_valid_params_for_properties(params)
761
+
762
+ # Set the creation time unless specified by the user.
763
+ params[:created] = @localtime unless params.has_key?(:created)
764
+
765
+ #
766
+ # Create the SummaryInformation property set.
767
+ #
768
+
769
+ # Get the codepage of the strings in the property set.
770
+ properties = [:title, :subject, :author, :keywords, :comments, :last_author]
771
+ params[:codepage] = get_property_set_codepage(params, properties)
772
+
773
+ # Create an array of property set values.
774
+ properties.unshift(:codepage)
775
+ properties.push(:created)
776
+
777
+ # Pack the property sets.
778
+ @summary =
779
+ create_summary_property_set(property_sets(properties, params))
780
+
781
+ #
782
+ # Create the DocSummaryInformation property set.
783
+ #
784
+
785
+ # Get the codepage of the strings in the property set.
786
+ properties = [:category, :manager, :company]
787
+ params[:codepage] = get_property_set_codepage(params, properties)
788
+
789
+ # Create an array of property set values.
790
+ properties.unshift(:codepage)
791
+
792
+ # Pack the property sets.
793
+ @doc_summary =
794
+ create_doc_summary_property_set(property_sets(properties, params))
795
+
796
+ # Set a flag for when the files is written.
797
+ @add_doc_properties = true
798
+ end
799
+
800
+ def extsst_buckets # :nodoc:
801
+ @extsst_buckets
802
+ end
803
+
804
+ def extsst_bucket_size # :nodoc:
805
+ @extsst_bucket_size
806
+ end
807
+
808
+ def biff_only=(val) # :nodoc:
809
+ @biff_only = val
810
+ end
811
+
812
+ def summary # :nodoc:
813
+ @summary
814
+ end
815
+
816
+ def localtime=(val) # :nodoc:
817
+ @localtime = val
818
+ end
819
+
820
+ def update_str_table(str)
821
+ @shared_string_table << str
822
+ end
823
+
824
+ #==========================================
825
+ # Internal method
826
+ #==========================================
827
+
828
+ private
829
+
830
+ # Add the in-built style formats and the default cell format.
831
+ def setup_built_in_formats(default_formats) # :nodoc:
832
+ add_format(:type => 1) # 0 Normal
833
+ add_format(:type => 1) # 1 RowLevel 1
834
+ add_format(:type => 1) # 2 RowLevel 2
835
+ add_format(:type => 1) # 3 RowLevel 3
836
+ add_format(:type => 1) # 4 RowLevel 4
837
+ add_format(:type => 1) # 5 RowLevel 5
838
+ add_format(:type => 1) # 6 RowLevel 6
839
+ add_format(:type => 1) # 7 RowLevel 7
840
+ add_format(:type => 1) # 8 ColLevel 1
841
+ add_format(:type => 1) # 9 ColLevel 2
842
+ add_format(:type => 1) # 10 ColLevel 3
843
+ add_format(:type => 1) # 11 ColLevel 4
844
+ add_format(:type => 1) # 12 ColLevel 5
845
+ add_format(:type => 1) # 13 ColLevel 6
846
+ add_format(:type => 1) # 14 ColLevel 7
847
+ add_format(default_formats) # 15 Cell XF
848
+ add_format(:type => 1, :num_format => 0x2B) # 16 Comma
849
+ add_format(:type => 1, :num_format => 0x29) # 17 Comma[0]
850
+ add_format(:type => 1, :num_format => 0x2C) # 18 Currency
851
+ add_format(:type => 1, :num_format => 0x2A) # 19 Currency[0]
852
+ add_format(:type => 1, :num_format => 0x09) # 20 Percent
853
+ end
854
+
855
+ def fileclosed?
856
+ @fileclosed || false
857
+ end
858
+
859
+ #
860
+ # Check for valid worksheet names. We check the length, if it contains any
861
+ # invalid characters and if the name is unique in the workbook.
862
+ #
863
+ def check_sheetname(name, name_utf16be = false, chart = nil) #:nodoc:
864
+ # name_utf16be parameter may set 0/1 in v0.6.6 or earlier
865
+ name_utf16be = false if name_utf16be == 0
866
+ name_utf16be = true if name_utf16be == 1
867
+
868
+ increment_sheet_chart_count(chart)
869
+ if name.nil? || name == ""
870
+ name_utf16be = false
871
+ name = default_sheet_chart_name(chart)
872
+ end
873
+
874
+ ruby_19 { name = convert_to_ascii_if_ascii(name) }
875
+ check_sheetname_length(name, name_utf16be)
876
+ check_sheetname_even(name) if name_utf16be
877
+ check_sheetname_valid_chars(name, name_utf16be)
878
+
879
+ # Handle utf8 strings
880
+ if is_utf8?(name)
881
+ name = utf8_to_16be(name)
882
+ name_utf16be = true
883
+ end
884
+
885
+ check_sheetname_uniq(name, name_utf16be)
886
+ [name, name_utf16be]
887
+ end
888
+
889
+ # Increment the Sheet/Chart number used for default sheet names below.
890
+ def increment_sheet_chart_count(chart)
891
+ if chart
892
+ @chart_count += 1
893
+ else
894
+ @sheet_count += 1
895
+ end
896
+ end
897
+
898
+ # Supply default Sheet/Chart name if none has been defined.
899
+ def default_sheet_chart_name(chart)
900
+ if chart
901
+ "Chart#{@chart_count}"
902
+ else
903
+ "Sheet#{@sheet_count}"
904
+ end
905
+ end
906
+
907
+ def check_sheetname_length(name, name_utf16be) #:nodoc:
908
+ # Check that sheetname is <= 31 (1 or 2 byte chars). Excel limit.
909
+ limit = name_utf16be ? 62 : 100 #31 Increased length limit of sheet name to 100
910
+ raise "Sheetname $name must be <= 100 chars" if name.bytesize > limit
911
+ end
912
+
913
+ def check_sheetname_even(name) #:nodoc:
914
+ # Check that Unicode sheetname has an even number of bytes
915
+ if (name.bytesize % 2 != 0)
916
+ raise "Odd number of bytes in Unicode worksheet name: #{name}"
917
+ end
918
+ end
919
+
920
+ def check_sheetname_valid_chars(name, name_utf16be) #:nodoc:
921
+ # Check that sheetname doesn't contain any invalid characters
922
+ invalid_char = %r![\[\]:*?/\\]!
923
+ if !name_utf16be && name =~ invalid_char
924
+ # Check ASCII names
925
+ raise "Invalid character []:*?/\\ in worksheet name: #{name}"
926
+ else
927
+ # Extract any 8bit clean chars from the UTF16 name and validate them.
928
+ str = name.dup
929
+ while str =~ /../m
930
+ hi, lo = $~[0].unpack('aa')
931
+ if hi == "\0" and lo =~ invalid_char
932
+ raise 'Invalid character []:*?/\\ in worksheet name: ' + name
933
+ end
934
+ str = $~.post_match
935
+ end
936
+ end
937
+ end
938
+
939
+ # Check that the worksheet name doesn't already exist since this is a fatal
940
+ # error in Excel 97. The check must also exclude case insensitive matches
941
+ # since the names 'Sheet1' and 'sheet1' are equivalent. The tests also have
942
+ # to take the name_utf16be into account.
943
+ #
944
+ def check_sheetname_uniq(name, name_utf16be) #:nodoc:
945
+ @worksheets.each do |worksheet|
946
+ name_a = name
947
+ encd_a = name_utf16be
948
+ name_b = worksheet.name
949
+ encd_b = worksheet.is_name_utf16be?
950
+ error = false
951
+
952
+ if !encd_a and !encd_b
953
+ error = (name_a.downcase == name_b.downcase)
954
+ elsif !encd_a and encd_b
955
+ name_a = ascii_to_16be(name_a)
956
+ error = (name_a.downcase == name_b.downcase)
957
+ elsif encd_a and !encd_b
958
+ name_b = ascii_to_16be(name_b)
959
+ error = (name_a.downcase == name_b.downcase)
960
+ elsif encd_a and encd_b
961
+ error = (name_a.downcase == name_b.downcase)
962
+ # TODO : not converted yet.
963
+
964
+ # We can't easily do a case insensitive test of the UTF16 names.
965
+ # As a special case we check if all of the high bytes are nulls and
966
+ # then do an ASCII style case insensitive test.
967
+ #
968
+ # Strip out the high bytes (funkily).
969
+ # my $hi_a = grep {ord} $name_a =~ /(.)./sg;
970
+ # my $hi_b = grep {ord} $name_b =~ /(.)./sg;
971
+ #
972
+ # if ($hi_a or $hi_b) {
973
+ # $error = 1 if $name_a eq $name_b;
974
+ # }
975
+ # else {
976
+ # $error = 1 if lc($name_a) eq lc($name_b);
977
+ # }
978
+ end
979
+ if error
980
+ raise "Worksheet name '#{name}', with case ignored, is already in use"
981
+ end
982
+ end
983
+ end
984
+
985
+ #
986
+ # Sets the colour palette to the Excel 97+ default.
987
+ #
988
+ def set_palette_xl97 #:nodoc:
989
+ @palette = [
990
+ [0x00, 0x00, 0x00, 0x00], # 8
991
+ [0xff, 0xff, 0xff, 0x00], # 9
992
+ [0xff, 0x00, 0x00, 0x00], # 10
993
+ [0x00, 0xff, 0x00, 0x00], # 11
994
+ [0x00, 0x00, 0xff, 0x00], # 12
995
+ [0xff, 0xff, 0x00, 0x00], # 13
996
+ [0xff, 0x00, 0xff, 0x00], # 14
997
+ [0x00, 0xff, 0xff, 0x00], # 15
998
+ [0x80, 0x00, 0x00, 0x00], # 16
999
+ [0x00, 0x80, 0x00, 0x00], # 17
1000
+ [0x00, 0x00, 0x80, 0x00], # 18
1001
+ [0x80, 0x80, 0x00, 0x00], # 19
1002
+ [0x80, 0x00, 0x80, 0x00], # 20
1003
+ [0x00, 0x80, 0x80, 0x00], # 21
1004
+ [0xc0, 0xc0, 0xc0, 0x00], # 22
1005
+ [0x80, 0x80, 0x80, 0x00], # 23
1006
+ [0x99, 0x99, 0xff, 0x00], # 24
1007
+ [0x99, 0x33, 0x66, 0x00], # 25
1008
+ [0xff, 0xff, 0xcc, 0x00], # 26
1009
+ [0xcc, 0xff, 0xff, 0x00], # 27
1010
+ [0x66, 0x00, 0x66, 0x00], # 28
1011
+ [0xff, 0x80, 0x80, 0x00], # 29
1012
+ [0x00, 0x66, 0xcc, 0x00], # 30
1013
+ [0xcc, 0xcc, 0xff, 0x00], # 31
1014
+ [0x00, 0x00, 0x80, 0x00], # 32
1015
+ [0xff, 0x00, 0xff, 0x00], # 33
1016
+ [0xff, 0xff, 0x00, 0x00], # 34
1017
+ [0x00, 0xff, 0xff, 0x00], # 35
1018
+ [0x80, 0x00, 0x80, 0x00], # 36
1019
+ [0x80, 0x00, 0x00, 0x00], # 37
1020
+ [0x00, 0x80, 0x80, 0x00], # 38
1021
+ [0x00, 0x00, 0xff, 0x00], # 39
1022
+ [0x00, 0xcc, 0xff, 0x00], # 40
1023
+ [0xcc, 0xff, 0xff, 0x00], # 41
1024
+ [0xcc, 0xff, 0xcc, 0x00], # 42
1025
+ [0xff, 0xff, 0x99, 0x00], # 43
1026
+ [0x99, 0xcc, 0xff, 0x00], # 44
1027
+ [0xff, 0x99, 0xcc, 0x00], # 45
1028
+ [0xcc, 0x99, 0xff, 0x00], # 46
1029
+ [0xff, 0xcc, 0x99, 0x00], # 47
1030
+ [0x33, 0x66, 0xff, 0x00], # 48
1031
+ [0x33, 0xcc, 0xcc, 0x00], # 49
1032
+ [0x99, 0xcc, 0x00, 0x00], # 50
1033
+ [0xff, 0xcc, 0x00, 0x00], # 51
1034
+ [0xff, 0x99, 0x00, 0x00], # 52
1035
+ [0xff, 0x66, 0x00, 0x00], # 53
1036
+ [0x66, 0x66, 0x99, 0x00], # 54
1037
+ [0x96, 0x96, 0x96, 0x00], # 55
1038
+ [0x00, 0x33, 0x66, 0x00], # 56
1039
+ [0x33, 0x99, 0x66, 0x00], # 57
1040
+ [0x00, 0x33, 0x00, 0x00], # 58
1041
+ [0x33, 0x33, 0x00, 0x00], # 59
1042
+ [0x99, 0x33, 0x00, 0x00], # 60
1043
+ [0x99, 0x33, 0x66, 0x00], # 61
1044
+ [0x33, 0x33, 0x99, 0x00], # 62
1045
+ [0x33, 0x33, 0x33, 0x00] # 63
1046
+ ]
1047
+ end
1048
+
1049
+ def property_set(property, params) #:nodoc:
1050
+ valid_properties[property][0..1] + [params[property]]
1051
+ end
1052
+
1053
+ def property_sets(properties, params) #:nodoc:
1054
+ properties.select { |property| params[property.to_sym] }.
1055
+ collect do |property|
1056
+ property_set(property.to_sym, params)
1057
+ end
1058
+ end
1059
+
1060
+ # List of valid input parameters.
1061
+ def valid_properties #:nodoc:
1062
+ {
1063
+ :codepage => [0x0001, 'VT_I2' ],
1064
+ :title => [0x0002, 'VT_LPSTR' ],
1065
+ :subject => [0x0003, 'VT_LPSTR' ],
1066
+ :author => [0x0004, 'VT_LPSTR' ],
1067
+ :keywords => [0x0005, 'VT_LPSTR' ],
1068
+ :comments => [0x0006, 'VT_LPSTR' ],
1069
+ :last_author => [0x0008, 'VT_LPSTR' ],
1070
+ :created => [0x000C, 'VT_FILETIME'],
1071
+ :category => [0x0002, 'VT_LPSTR' ],
1072
+ :manager => [0x000E, 'VT_LPSTR' ],
1073
+ :company => [0x000F, 'VT_LPSTR' ],
1074
+ :utf8 => 1
1075
+ }
1076
+ end
1077
+
1078
+ def check_valid_params_for_properties(params) #:nodoc:
1079
+ params.each_key do |k|
1080
+ unless valid_properties.has_key?(k)
1081
+ raise "Unknown parameter '#{k}' in set_properties()"
1082
+ end
1083
+ end
1084
+ end
1085
+
1086
+ #
1087
+ # Get the character codepage used by the strings in a property set. If one of
1088
+ # the strings used is utf8 then the codepage is marked as utf8. Otherwise
1089
+ # Latin 1 is used (although in our case this is limited to 7bit ASCII).
1090
+ #
1091
+ def get_property_set_codepage(params, properties) #:nodoc:
1092
+ # Allow for manually marked utf8 strings.
1093
+ return 0xFDE9 unless params[:utf8].nil?
1094
+ properties.each do |property|
1095
+ next unless params.has_key?(property.to_sym)
1096
+ return 0xFDE9 if is_utf8?(params[property.to_sym])
1097
+ end
1098
+ return 0x04E4 # Default codepage, Latin 1.
1099
+ end
1100
+
1101
+ #
1102
+ # Assemble worksheets into a workbook and send the BIFF data to an OLE
1103
+ # storage.
1104
+ #
1105
+ def store_workbook #:nodoc:
1106
+ # Add a default worksheet if non have been added.
1107
+ add_worksheet if @worksheets.empty?
1108
+
1109
+ # Calculate size required for MSO records and update worksheets.
1110
+ calc_mso_sizes
1111
+
1112
+ # Ensure that at least one worksheet has been selected.
1113
+ unless @worksheets.activesheet
1114
+ @worksheets.activesheet = @worksheets.first
1115
+ @worksheets.activesheet.select
1116
+ end
1117
+
1118
+ # Add Workbook globals
1119
+ add_workbook_globals
1120
+
1121
+ # Calculate the offsets required by the BOUNDSHEET records
1122
+ calc_sheet_offsets
1123
+
1124
+ # Add BOUNDSHEET records.
1125
+ @worksheets.each do |sheet|
1126
+ store_boundsheet(sheet)
1127
+ end
1128
+
1129
+ # NOTE: If any records are added between here and EOF the
1130
+ # calc_sheet_offsets() should be updated to include the new length.
1131
+ store_country
1132
+ if @ext_refs.keys.size != 0
1133
+ store_supbook
1134
+ store_externsheet
1135
+ store_names
1136
+ end
1137
+ add_mso_drawing_group
1138
+ store_shared_strings
1139
+ store_extsst
1140
+
1141
+ # End Workbook globals
1142
+ store_eof
1143
+
1144
+ # Store the workbook in an OLE container
1145
+ store_ole_file
1146
+ end
1147
+
1148
+ def add_workbook_globals
1149
+ store_bof(0x0005)
1150
+ store_codepage
1151
+ store_window1
1152
+ store_hideobj
1153
+ store_1904
1154
+ store_all_fonts
1155
+ store_all_num_formats
1156
+ store_all_xfs
1157
+ store_all_styles
1158
+ store_palette
1159
+ end
1160
+
1161
+ #
1162
+ # Store the workbook in an OLE container using the default handler or using
1163
+ # OLE::Storage_Lite if the workbook data is > ~ 7MB.
1164
+ #
1165
+ def store_ole_file #:nodoc:
1166
+ maxsize = 7_087_104
1167
+ # maxsize = 1
1168
+
1169
+ if !add_doc_properties && @biffsize <= maxsize
1170
+ store_through_ole_writer
1171
+ else
1172
+ store_through_ole_storage_lite
1173
+ end
1174
+ end
1175
+
1176
+ def store_through_ole_writer
1177
+ # Write the OLE file using OLEwriter if data <= 7MB
1178
+ ole = OLEWriter.new(@fh_out)
1179
+
1180
+ # Write the BIFF data without the OLE container for testing.
1181
+ ole.biff_only = @biff_only
1182
+
1183
+ # Indicate that we created the filehandle and want to close it.
1184
+ ole.internal_fh = @internal_fh
1185
+
1186
+ ole.set_size(@biffsize)
1187
+ ole.write_header
1188
+
1189
+ while tmp = get_data
1190
+ ole.write(tmp)
1191
+ end
1192
+
1193
+ @worksheets.each do |worksheet|
1194
+ while tmp = worksheet.get_data
1195
+ ole.write(tmp)
1196
+ end
1197
+ end
1198
+
1199
+ return ole.close
1200
+ end
1201
+
1202
+ def store_through_ole_storage_lite
1203
+ # Create the Workbook stream.
1204
+ stream = 'Workbook'.unpack('C*').pack('v*')
1205
+ workbook = OLEStorageLitePPSFile.new(stream)
1206
+ workbook.set_file # use tempfile
1207
+
1208
+ while tmp = get_data
1209
+ workbook.append(tmp)
1210
+ end
1211
+
1212
+ @worksheets.each do |worksheet|
1213
+ while tmp = worksheet.get_data
1214
+ workbook.append(tmp)
1215
+ end
1216
+ end
1217
+
1218
+ streams = []
1219
+ streams << workbook
1220
+
1221
+ # Create the properties streams, if any.
1222
+ if add_doc_properties
1223
+ stream = "\5SummaryInformation".unpack('C*').pack('v*')
1224
+ summary = OLEStorageLitePPSFile.new(stream, @summary)
1225
+ streams << summary
1226
+ stream = "\5DocumentSummaryInformation".unpack('C*').pack('v*')
1227
+ summary = OLEStorageLitePPSFile.new(stream, @doc_summary)
1228
+ streams << summary
1229
+ end
1230
+ # Create the OLE root document and add the substreams.
1231
+ localtime = @localtime.to_a[0..5]
1232
+ localtime[4] -= 1 # month
1233
+ localtime[5] -= 1900
1234
+ ole_root = OLEStorageLitePPSRoot.new(
1235
+ localtime,
1236
+ localtime,
1237
+ streams
1238
+ )
1239
+ ole_root.save(@file)
1240
+
1241
+ # Close the filehandle if it was created internally.
1242
+ return @fh_out.close if @internal_fh != 0
1243
+ end
1244
+
1245
+ #
1246
+ # Calculate Worksheet BOF offsets records for use in the BOUNDSHEET records.
1247
+ #
1248
+ def calc_sheet_offsets #:nodoc:
1249
+ offset = @datasize
1250
+
1251
+ # Add the length of the COUNTRY record
1252
+ offset += 8
1253
+
1254
+ # Add the length of the SST and associated CONTINUEs
1255
+ offset += calculate_shared_string_sizes
1256
+
1257
+ # Add the length of the EXTSST record.
1258
+ offset += calculate_extsst_size(@shared_string_table.str_unique)
1259
+
1260
+ # Add the length of the SUPBOOK, EXTERNSHEET and NAME records
1261
+ offset += calculate_extern_sizes
1262
+
1263
+ # Add the length of the MSODRAWINGGROUP records including an extra 4 bytes
1264
+ # for any CONTINUE headers. See add_mso_drawing_group_continue().
1265
+ mso_size = @mso_size
1266
+ mso_size += 4 * Integer((mso_size -1) / Float(@limit))
1267
+ offset += mso_size
1268
+
1269
+ @worksheets.each do |sheet|
1270
+ offset += BOF + sheet.name.bytesize
1271
+ end
1272
+
1273
+ offset += EOF
1274
+ @worksheets.each do |sheet|
1275
+ sheet.offset = offset
1276
+ sheet.close
1277
+ offset += sheet.datasize
1278
+ end
1279
+
1280
+ @biffsize = offset
1281
+ end
1282
+
1283
+ #
1284
+ # Calculate the MSODRAWINGGROUP sizes and the indexes of the Worksheet
1285
+ # MSODRAWING records.
1286
+ #
1287
+ # In the following SPID is shape id, according to Escher nomenclature.
1288
+ #
1289
+ def calc_mso_sizes #:nodoc:
1290
+ mso_size = 0 # Size of the MSODRAWINGGROUP record
1291
+ max_spid = 1024 # spidMax
1292
+ drawings_saved = 0 # cdgSaved
1293
+ clusters = []
1294
+
1295
+ process_images
1296
+
1297
+ # Add Bstore container size if there are images.
1298
+ mso_size += 8 unless @images_data.empty?
1299
+
1300
+ # Iterate through the worksheets, calculate the MSODRAWINGGROUP parameters
1301
+ # and space required to store the record and the MSODRAWING parameters
1302
+ # required by each worksheet.
1303
+ #
1304
+ @worksheets.each do |sheet|
1305
+ next unless sheet.type == 0x0000
1306
+ next if sheet.num_shapes == 1
1307
+
1308
+ mso_size, drawings_saved, max_spid, start_spid =
1309
+ sheet.push_object_ids(mso_size, drawings_saved, max_spid, start_spid, clusters)
1310
+ end
1311
+
1312
+
1313
+ # Calculate the MSODRAWINGGROUP size if we have stored some shapes.
1314
+ mso_size += 86 if mso_size != 0 # Smallest size is 86+8=94
1315
+
1316
+ shapes_saved = sheets.collect { |sheet| sheet.num_shapes }.
1317
+ select { |num_shapes| num_shapes > 1 }.
1318
+ inject(0) { |result, num_shapes| result + num_shapes }
1319
+
1320
+ @mso_size = mso_size
1321
+ @mso_clusters = [
1322
+ max_spid, clusters.size + 1, shapes_saved,
1323
+ drawings_saved, clusters
1324
+ ]
1325
+ end
1326
+
1327
+ #
1328
+ # We need to process each image in each worksheet and extract information.
1329
+ # Some of this information is stored and used in the Workbook and some is
1330
+ # passed back into each Worksheet. The overall size for the image related
1331
+ # BIFF structures in the Workbook is calculated here.
1332
+ #
1333
+ # MSO size = 8 bytes for bstore_container +
1334
+ # 44 bytes for blip_store_entry +
1335
+ # 25 bytes for blip
1336
+ # = 77 + image size.
1337
+ #
1338
+ def process_images #:nodoc:
1339
+ images_seen = {}
1340
+ image_data = []
1341
+ previous_images = []
1342
+ image_id = 1
1343
+ images_size = 0
1344
+
1345
+ @worksheets.each do |sheet|
1346
+ next unless sheet.type == 0x0000
1347
+ next if sheet.images_size == 0
1348
+
1349
+ num_images = 0
1350
+ image_mso_size = 0
1351
+
1352
+ sheet.images_array.each do |image|
1353
+ num_images += 1
1354
+ unless images_seen[image.filename]
1355
+ # TODO should also match seen images based on checksum.
1356
+ image.id = image_id
1357
+ image.ref_count = 1
1358
+ image.import
1359
+
1360
+ # Also store new data for use in duplicate images.
1361
+ previous_images.push(image)
1362
+
1363
+ # Store information required by the Workbook.
1364
+ image_data.push(image)
1365
+
1366
+ # Keep track of overall data size.
1367
+ images_size += image.size + 61 # Size for bstore container.
1368
+ image_mso_size += image.size + 69 # Size for dgg container.
1369
+
1370
+ images_seen[image.filename] = image_id
1371
+ image_id += 1
1372
+ else
1373
+ # We've processed this file already.
1374
+ index = images_seen[image.filename] -1
1375
+
1376
+ # Increase image reference count.
1377
+ image_data[index].ref_count += 1
1378
+
1379
+ # Add previously calculated data back onto the Worksheet array.
1380
+ # image_id, type, width, height
1381
+ image.id = previous_images[index].id
1382
+ image.type = previous_images[index].type
1383
+ image.width = previous_images[index].width
1384
+ image.height = previous_images[index].height
1385
+ end
1386
+ end
1387
+
1388
+ # Store information required by the Worksheet.
1389
+ sheet.num_images = num_images
1390
+ sheet.image_mso_size = image_mso_size
1391
+ end
1392
+
1393
+ # Store information required by the Workbook.
1394
+ @images_size = images_size
1395
+ @images_data = image_data # Store the data for MSODRAWINGGROUP.
1396
+ end
1397
+
1398
+ #
1399
+ # Store the Excel FONT records.
1400
+ #
1401
+ def store_all_fonts #:nodoc:
1402
+ format = @formats[15] # The default cell format.
1403
+ font = format.get_font
1404
+
1405
+ # Fonts are 0-indexed. According to the SDK there is no index 4,
1406
+ 4.times { append(font) }
1407
+
1408
+ # Add the default fonts for charts and comments. This aren't connected
1409
+ # to XF formats. Note, the font size, and some other properties of
1410
+ # chart fonts are set in the FBI record of the chart.
1411
+
1412
+ # Index 5. Axis numbers.
1413
+ tmp_format = Writeexcel::Format.new(
1414
+ nil,
1415
+ :font_only => 1
1416
+ )
1417
+ append(tmp_format.get_font)
1418
+
1419
+ # Index 6. Series names.
1420
+ tmp_format = Writeexcel::Format.new(
1421
+ nil,
1422
+ :font_only => 1
1423
+ )
1424
+ append(tmp_format.get_font)
1425
+
1426
+ # Index 7. Title.
1427
+ tmp_format = Writeexcel::Format.new(
1428
+ nil,
1429
+ :font_only => 1,
1430
+ :bold => 1
1431
+ )
1432
+ append(tmp_format.get_font)
1433
+
1434
+ # Index 8. Axes.
1435
+ tmp_format = Writeexcel::Format.new(
1436
+ nil,
1437
+ :font_only => 1,
1438
+ :bold => 1
1439
+ )
1440
+ append(tmp_format.get_font)
1441
+
1442
+ # Index 9. Comments.
1443
+ tmp_format = Writeexcel::Format.new(
1444
+ nil,
1445
+ :font_only => 1,
1446
+ :font => 'Tahoma',
1447
+ :size => 8
1448
+ )
1449
+ append(tmp_format.get_font)
1450
+
1451
+ # Iterate through the XF objects and write a FONT record if it isn't the
1452
+ # same as the default FONT and if it hasn't already been used.
1453
+ #
1454
+ fonts = {}
1455
+ index = 10 # The first user defined FONT
1456
+
1457
+ key = format.get_font_key # The default font for cell formats.
1458
+ fonts[key] = 0 # Index of the default font
1459
+
1460
+ # Fonts that are marked as '_font_only' are always stored. These are used
1461
+ # mainly for charts and may not have an associated XF record.
1462
+
1463
+ @formats.each do |fmt|
1464
+ key = fmt.get_font_key
1465
+ if fmt.font_only == 0 and !fonts[key].nil?
1466
+ # FONT has already been used
1467
+ fmt.font_index = fonts[key]
1468
+ else
1469
+ # Add a new FONT record
1470
+
1471
+ if fmt.font_only == 0
1472
+ fonts[key] = index
1473
+ end
1474
+
1475
+ fmt.font_index = index
1476
+ index += 1
1477
+ font = fmt.get_font
1478
+ append(font)
1479
+ end
1480
+ end
1481
+ end
1482
+
1483
+ #
1484
+ # Store user defined numerical formats i.e. FORMAT records
1485
+ #
1486
+ def store_all_num_formats #:nodoc:
1487
+ num_formats = {}
1488
+ index = 164 # User defined FORMAT records start from 0xA4
1489
+
1490
+ # Iterate through the XF objects and write a FORMAT record if it isn't a
1491
+ # built-in format type and if the FORMAT string hasn't already been used.
1492
+ #
1493
+ @formats.each do |format|
1494
+ num_format = format.num_format
1495
+ encoding = format.num_format_enc
1496
+
1497
+ # Check if $num_format is an index to a built-in format.
1498
+ # Also check for a string of zeros, which is a valid format string
1499
+ # but would evaluate to zero.
1500
+ #
1501
+ unless num_format.to_s =~ /^0+\d/
1502
+ next if num_format.to_s =~ /^\d+$/ # built-in
1503
+ end
1504
+
1505
+ if num_formats[num_format]
1506
+ # FORMAT has already been used
1507
+ format.num_format = num_formats[num_format]
1508
+ else
1509
+ # Add a new FORMAT
1510
+ num_formats[num_format] = index
1511
+ format.num_format = index
1512
+ store_num_format(num_format, index, encoding)
1513
+ index += 1
1514
+ end
1515
+ end
1516
+ end
1517
+
1518
+ #
1519
+ # Write all XF records.
1520
+ #
1521
+ def store_all_xfs #:nodoc:
1522
+ @formats.each do |format|
1523
+ xf = format.get_xf
1524
+ append(xf)
1525
+ end
1526
+ end
1527
+
1528
+ #
1529
+ # Write all STYLE records.
1530
+ #
1531
+ def store_all_styles #:nodoc:
1532
+ # Excel adds the built-in styles in alphabetical order.
1533
+ built_ins = [
1534
+ [0x03, 16], # Comma
1535
+ [0x06, 17], # Comma[0]
1536
+ [0x04, 18], # Currency
1537
+ [0x07, 19], # Currency[0]
1538
+ [0x00, 0], # Normal
1539
+ [0x05, 20] # Percent
1540
+
1541
+ # We don't deal with these styles yet.
1542
+ #[0x08, 21], # Hyperlink
1543
+ #[0x02, 8], # ColLevel_n
1544
+ #[0x01, 1], # RowLevel_n
1545
+ ]
1546
+
1547
+ built_ins.each do |aref|
1548
+ type = aref[0]
1549
+ xf_index = aref[1]
1550
+
1551
+ store_style(type, xf_index)
1552
+ end
1553
+ end
1554
+
1555
+ #
1556
+ # Write the NAME record to define the print area and the repeat rows and cols.
1557
+ #
1558
+ def store_names # :nodoc:
1559
+ # Create the user defined names.
1560
+ defined_names.each do |defined_name|
1561
+ store_name(
1562
+ defined_name[:name],
1563
+ defined_name[:encoding],
1564
+ defined_name[:sheet_index],
1565
+ defined_name[:formula]
1566
+ )
1567
+ end
1568
+
1569
+ # Sort the worksheets into alphabetical order by name. This is a
1570
+ # requirement for some non-English language Excel patch levels.
1571
+ sorted_worksheets = @worksheets.sort_by{ |x| x.name }
1572
+
1573
+ # Create the autofilter NAME records
1574
+ create_autofilter_name_records(sorted_worksheets)
1575
+
1576
+ # Create the print area NAME records
1577
+ create_print_area_name_records(sorted_worksheets)
1578
+
1579
+ # Create the print title NAME records
1580
+ create_print_title_name_records(sorted_worksheets)
1581
+ end
1582
+
1583
+ def create_autofilter_name_records(sorted_worksheets) #:nodoc:
1584
+ sorted_worksheets.each do |worksheet|
1585
+ append(*worksheet.autofilter_name_record_short(true))
1586
+ end
1587
+ end
1588
+
1589
+ def create_print_area_name_records(sorted_worksheets) #:nodoc:
1590
+ sorted_worksheets.each do |worksheet|
1591
+ append(*worksheet.print_area_name_record_short)
1592
+ end
1593
+ end
1594
+
1595
+ def create_print_title_name_records(sorted_worksheets) #:nodoc:
1596
+ sorted_worksheets.each do |worksheet|
1597
+ # Determine if row + col, row, col or nothing has been defined
1598
+ # and write the appropriate record
1599
+ #
1600
+ if worksheet.title_range.row_min && worksheet.title_range.col_min
1601
+ # Row and column titles have been defined.
1602
+ # Row title has been defined.
1603
+ append(*worksheet.print_title_name_record_long)
1604
+ elsif worksheet.title_range.row_min
1605
+ # Row title has been defined.
1606
+ append(*worksheet.print_title_name_record_short)
1607
+ elsif worksheet.title_range.col_min
1608
+ # Column title has been defined.
1609
+ append(*worksheet.print_title_name_record_short)
1610
+ else
1611
+ # Nothing left to do
1612
+ end
1613
+ end
1614
+ end
1615
+
1616
+ ###############################################################################
1617
+ ###############################################################################
1618
+ #
1619
+ # BIFF RECORDS
1620
+ #
1621
+
1622
+
1623
+ #
1624
+ # Write Excel BIFF WINDOW1 record.
1625
+ #
1626
+ def store_window1 #:nodoc:
1627
+ record = 0x003D # Record identifier
1628
+ length = 0x0012 # Number of bytes to follow
1629
+
1630
+ x_pos = 0x0000 # Horizontal position of window
1631
+ y_pos = 0x0000 # Vertical position of window
1632
+ dx_win = 0x355C # Width of window
1633
+ dy_win = 0x30ED # Height of window
1634
+
1635
+ grbit = 0x0038 # Option flags
1636
+ ctabsel = @worksheets.selected_count # Number of workbook tabs selected
1637
+ tab_ratio = 0x0258 # Tab to scrollbar ratio
1638
+
1639
+ tab_cur = @worksheets.activesheet_index # Active worksheet
1640
+ tab_first = @worksheets.firstsheet_index # 1st displayed worksheet
1641
+ header = [record, length].pack("vv")
1642
+ data = [
1643
+ x_pos, y_pos, dx_win, dy_win,
1644
+ grbit,
1645
+ tab_cur, tab_first,
1646
+ ctabsel, tab_ratio
1647
+ ].pack("vvvvvvvvv")
1648
+
1649
+ append(header, data)
1650
+ end
1651
+
1652
+ #
1653
+ # Writes Excel BIFF BOUNDSHEET record.
1654
+ #
1655
+ # sheetname # Worksheet name
1656
+ # offset # Location of worksheet BOF
1657
+ # type # Worksheet type
1658
+ # hidden # Worksheet hidden flag
1659
+ # encoding # Sheet name encoding
1660
+ #
1661
+ def store_boundsheet(sheet) #:nodoc:
1662
+ append(sheet.boundsheet)
1663
+ end
1664
+
1665
+ #
1666
+ # Write Excel BIFF STYLE records.
1667
+ #
1668
+ # type # Built-in style
1669
+ # xf_index # Index to style XF
1670
+ #
1671
+ def store_style(type, xf_index) #:nodoc:
1672
+ record = 0x0293 # Record identifier
1673
+ length = 0x0004 # Bytes to follow
1674
+
1675
+ level = 0xff # Outline style level
1676
+
1677
+ xf_index |= 0x8000 # Add flag to indicate built-in style.
1678
+
1679
+ header = [record, length].pack("vv")
1680
+ data = [xf_index, type, level].pack("vCC")
1681
+
1682
+ append(header, data)
1683
+ end
1684
+
1685
+ #
1686
+ # Writes Excel FORMAT record for non "built-in" numerical formats.
1687
+ #
1688
+ # format # Custom format string
1689
+ # ifmt # Format index code
1690
+ # encoding # Char encoding for format string
1691
+ #
1692
+ def store_num_format(format, ifmt, encoding) #:nodoc:
1693
+ format = format.to_s unless format.respond_to?(:to_str)
1694
+ record = 0x041E # Record identifier
1695
+ # length # Number of bytes to follow
1696
+ # Char length of format string
1697
+ cch = format.bytesize
1698
+
1699
+ ruby_19 { format = convert_to_ascii_if_ascii(format) }
1700
+
1701
+ # Handle utf8 strings
1702
+ if is_utf8?(format)
1703
+ format = utf8_to_16be(format)
1704
+ encoding = 1
1705
+ end
1706
+
1707
+ # Handle Unicode format strings.
1708
+ if encoding == 1
1709
+ #raise "Uneven number of bytes in Unicode font name" if cch % 2 != 0
1710
+ #cch /= 2 if encoding != 0
1711
+ format = utf16be_to_16le(format)
1712
+ end
1713
+
1714
+ =begin
1715
+ # Special case to handle Euro symbol, 0x80, in non-Unicode strings.
1716
+ if encoding == 0 and format =~ /\x80/
1717
+ format = format.unpack('C*').pack('v*')
1718
+ format.gsub!(/\x80\x00/, "\xAC\x20")
1719
+ encoding = 1
1720
+ end
1721
+ =end
1722
+ length = 0x05 + format.bytesize
1723
+
1724
+ header = [record, length].pack("vv")
1725
+ data = [ifmt, cch, encoding].pack("vvC")
1726
+
1727
+ append(header, data, format)
1728
+ end
1729
+
1730
+ #
1731
+ # Write Excel 1904 record to indicate the date system in use.
1732
+ #
1733
+ def store_1904 #:nodoc:
1734
+ record = 0x0022 # Record identifier
1735
+ length = 0x0002 # Bytes to follow
1736
+
1737
+ f1904 = @date_1904 ? 1 : 0 # Flag for 1904 date system
1738
+
1739
+ header = [record, length].pack("vv")
1740
+ data = [f1904].pack("v")
1741
+
1742
+ append(header, data)
1743
+ end
1744
+
1745
+ #
1746
+ # Write BIFF record SUPBOOK to indicate that the workbook contains external
1747
+ # references, in our case, formula, print area and print title refs.
1748
+ #
1749
+ def store_supbook #:nodoc:
1750
+ record = 0x01AE # Record identifier
1751
+ length = 0x0004 # Number of bytes to follow
1752
+
1753
+ tabs = @worksheets.size # Number of worksheets
1754
+ virt_path = 0x0401 # Encoded workbook filename
1755
+
1756
+ header = [record, length].pack("vv")
1757
+ data = [tabs, virt_path].pack("vv")
1758
+
1759
+ append(header, data)
1760
+ end
1761
+
1762
+ #
1763
+ # Writes the Excel BIFF EXTERNSHEET record. These references are used by
1764
+ # formulas. TODO NAME record is required to define the print area and the
1765
+ # repeat rows and columns.
1766
+ #
1767
+ def store_externsheet # :nodoc:
1768
+ record = 0x0017 # Record identifier
1769
+
1770
+ # Get the external refs
1771
+ ext = @ext_refs.keys.sort
1772
+
1773
+ # Change the external refs from stringified "1:1" to [1, 1]
1774
+ ext.map! {|e| e.split(/:/).map! {|v| v.to_i} }
1775
+
1776
+ cxti = ext.size # Number of Excel XTI structures
1777
+ rgxti = '' # Array of XTI structures
1778
+
1779
+ # Write the XTI structs
1780
+ ext.each do |e|
1781
+ rgxti += [0, e[0], e[1]].pack("vvv")
1782
+ end
1783
+
1784
+ data = [cxti].pack("v") + rgxti
1785
+ header = [record, data.bytesize].pack("vv")
1786
+
1787
+ append(header, data)
1788
+ end
1789
+
1790
+ #
1791
+ # Store the NAME record used for storing the print area, repeat rows, repeat
1792
+ # columns, autofilters and defined names.
1793
+ #
1794
+ # TODO. This is a more generic version that will replace store_name_short()
1795
+ # and store_name_long().
1796
+ #
1797
+ def store_name(name, encoding, sheet_index, formula) # :nodoc:
1798
+ ruby_19 { formula = convert_to_ascii_if_ascii(formula) }
1799
+
1800
+ record = 0x0018 # Record identifier
1801
+
1802
+ text_length = name.bytesize
1803
+ formula_length = formula.bytesize
1804
+
1805
+ # UTF-16 string length is in characters not bytes.
1806
+ text_length /= 2 if encoding != 0
1807
+
1808
+ grbit = 0x0000 # Option flags
1809
+ shortcut = 0x00 # Keyboard shortcut
1810
+ ixals = 0x0000 # Unused index.
1811
+ menu_length = 0x00 # Length of cust menu text
1812
+ desc_length = 0x00 # Length of description text
1813
+ help_length = 0x00 # Length of help topic text
1814
+ status_length = 0x00 # Length of status bar text
1815
+
1816
+ # Set grbit built-in flag and the hidden flag for autofilters.
1817
+ if text_length == 1
1818
+ grbit = 0x0020 if name.ord == 0x06 # Print area
1819
+ grbit = 0x0020 if name.ord == 0x07 # Print titles
1820
+ grbit = 0x0021 if name.ord == 0x0D # Autofilter
1821
+ end
1822
+
1823
+ data = [grbit].pack("v")
1824
+ data += [shortcut].pack("C")
1825
+ data += [text_length].pack("C")
1826
+ data += [formula_length].pack("v")
1827
+ data += [ixals].pack("v")
1828
+ data += [sheet_index].pack("v")
1829
+ data += [menu_length].pack("C")
1830
+ data += [desc_length].pack("C")
1831
+ data += [help_length].pack("C")
1832
+ data += [status_length].pack("C")
1833
+ data += [encoding].pack("C")
1834
+ data += name
1835
+ data += formula
1836
+
1837
+ header = [record, data.bytesize].pack("vv")
1838
+
1839
+ append(header, data)
1840
+ end
1841
+
1842
+ #
1843
+ # Stores the PALETTE biff record.
1844
+ #
1845
+ def store_palette #:nodoc:
1846
+ record = 0x0092 # Record identifier
1847
+ length = 2 + 4 * @palette.size # Number of bytes to follow
1848
+ ccv = @palette.size # Number of RGB values to follow
1849
+ data = '' # The RGB data
1850
+
1851
+ # Pack the RGB data
1852
+ @palette.each do |p|
1853
+ data += p.pack('CCCC')
1854
+ end
1855
+
1856
+ header = [record, length, ccv].pack("vvv")
1857
+
1858
+ append(header, data)
1859
+ end
1860
+
1861
+ #
1862
+ # Stores the CODEPAGE biff record.
1863
+ #
1864
+ def store_codepage #:nodoc:
1865
+ record = 0x0042 # Record identifier
1866
+ length = 0x0002 # Number of bytes to follow
1867
+ cv = @codepage # The code page
1868
+
1869
+ store_common(record, length, cv)
1870
+ end
1871
+
1872
+ #
1873
+ # Stores the COUNTRY biff record.
1874
+ #
1875
+ def store_country #:nodoc:
1876
+ record = 0x008C # Record identifier
1877
+ length = 0x0004 # Number of bytes to follow
1878
+ country_default = @country
1879
+ country_win_ini = @country
1880
+
1881
+ store_common(record, length, country_default, country_win_ini)
1882
+ end
1883
+
1884
+ #
1885
+ # Stores the HIDEOBJ biff record.
1886
+ #
1887
+ def store_hideobj #:nodoc:
1888
+ record = 0x008D # Record identifier
1889
+ length = 0x0002 # Number of bytes to follow
1890
+ hide = @hideobj ? 1 : 0 # Option to hide objects
1891
+
1892
+ store_common(record, length, hide)
1893
+ end
1894
+
1895
+ def store_common(record, length, *data) #:nodoc:
1896
+ header = [record, length].pack("vv")
1897
+ add_data = [*data].pack("v*")
1898
+
1899
+ append(header, add_data)
1900
+ end
1901
+
1902
+ #
1903
+ # We need to calculate the space required by the SUPBOOK, EXTERNSHEET and NAME
1904
+ # records so that it can be added to the BOUNDSHEET offsets.
1905
+ #
1906
+ def calculate_extern_sizes # :nodoc:
1907
+ ext_refs = @parser.get_ext_sheets
1908
+ length = 0
1909
+ index = 0
1910
+
1911
+ unless defined_names.empty?
1912
+ index = 0
1913
+ key = "#{index}:#{index}"
1914
+
1915
+ add_ext_refs(ext_refs, key) unless ext_refs.has_key?(key)
1916
+ end
1917
+
1918
+ defined_names.each do |defined_name|
1919
+ length += 19 + defined_name[:name].bytesize + defined_name[:formula].bytesize
1920
+ end
1921
+
1922
+ @worksheets.each do |worksheet|
1923
+ key = "#{index}:#{index}"
1924
+ index += 1
1925
+
1926
+ # Add area NAME records
1927
+ #
1928
+ if worksheet.print_range.row_min
1929
+ add_ext_refs(ext_refs, key) unless ext_refs[key]
1930
+ length += 31
1931
+ end
1932
+
1933
+ # Add title NAME records
1934
+ #
1935
+ if worksheet.title_range.row_min && worksheet.title_range.col_min
1936
+ add_ext_refs(ext_refs, key) unless ext_refs[key]
1937
+ length += 46
1938
+ elsif worksheet.title_range.row_min || worksheet.title_range.col_min
1939
+ add_ext_refs(ext_refs, key) unless ext_refs[key]
1940
+ length += 31
1941
+ else
1942
+ # TODO, may need this later.
1943
+ end
1944
+
1945
+ # Add Autofilter NAME records
1946
+ #
1947
+ unless worksheet.filter_count == 0
1948
+ add_ext_refs(ext_refs, key) unless ext_refs[key]
1949
+ length += 31
1950
+ end
1951
+ end
1952
+
1953
+ # Update the ref counts.
1954
+ ext_ref_count = ext_refs.keys.size
1955
+ @ext_refs = ext_refs
1956
+
1957
+ # If there are no external refs then we don't write, SUPBOOK, EXTERNSHEET
1958
+ # and NAME. Therefore the length is 0.
1959
+
1960
+ return length = 0 if ext_ref_count == 0
1961
+
1962
+ # The SUPBOOK record is 8 bytes
1963
+ length += 8
1964
+
1965
+ # The EXTERNSHEET record is 6 bytes + 6 bytes for each external ref
1966
+ length += 6 * (1 + ext_ref_count)
1967
+
1968
+ length
1969
+ end
1970
+
1971
+ def add_ext_refs(ext_refs, key) #:nodoc:
1972
+ ext_refs[key] = ext_refs.keys.size
1973
+ end
1974
+
1975
+ #
1976
+ # Handling of the SST continue blocks is complicated by the need to include an
1977
+ # additional continuation byte depending on whether the string is split between
1978
+ # blocks or whether it starts at the beginning of the block. (There are also
1979
+ # additional complications that will arise later when/if Rich Strings are
1980
+ # supported). As such we cannot use the simple CONTINUE mechanism provided by
1981
+ # the add_continue() method in BIFFwriter.pm. Thus we have to make two passes
1982
+ # through the strings data. The first is to calculate the required block sizes
1983
+ # and the second, in store_shared_strings(), is to write the actual strings.
1984
+ # The first pass through the data is also used to calculate the size of the SST
1985
+ # and CONTINUE records for use in setting the BOUNDSHEET record offsets. The
1986
+ # downside of this is that the same algorithm repeated in store_shared_strings.
1987
+ #
1988
+ def calculate_shared_string_sizes #:nodoc:
1989
+ str_block_sizes = @shared_string_table.block_sizes
1990
+
1991
+ # Calculate the total length of the SST and associated CONTINUEs (if any).
1992
+ # The SST record will have a length even if it contains no strings.
1993
+ # This length is required to set the offsets in the BOUNDSHEET records since
1994
+ # they must be written before the SST records
1995
+ #
1996
+ # when array = [a, b, c] # array not empty?
1997
+ # length = 12 + a + 4 + b + 4 + c = 12 + a + b + c + 4 * (array.size - 1)
1998
+ #
1999
+ length = str_block_sizes.inject(12){|result, item| result + item}
2000
+ length += 4 * (str_block_sizes.size - 1) unless str_block_sizes.empty?
2001
+
2002
+ length
2003
+ end
2004
+
2005
+ def self.split_string_setup(encoding, split_string, continue_limit, written, continue) #:nodoc:
2006
+ # We need to avoid the case where a string is continued in the first
2007
+ # n bytes that contain the string header information.
2008
+ header_length = 3 # Min string + header size -1
2009
+ space_remaining = continue_limit - written - continue
2010
+
2011
+ # Unicode data should only be split on char (2 byte) boundaries.
2012
+ # Therefore, in some cases we need to reduce the amount of available
2013
+ # space by 1 byte to ensure the correct alignment.
2014
+ align = 0
2015
+
2016
+ # Only applies to Unicode strings
2017
+ if encoding == 1
2018
+ # Min string + header size -1
2019
+ header_length = 4
2020
+ if space_remaining > header_length
2021
+ # String contains 3 byte header => split on odd boundary
2022
+ if split_string == 0 and space_remaining % 2 != 1
2023
+ space_remaining -= 1
2024
+ align = 1
2025
+ # Split section without header => split on even boundary
2026
+ elsif split_string != 0 and space_remaining % 2 == 1
2027
+ space_remaining -= 1
2028
+ align = 1
2029
+ end
2030
+ split_string = 1
2031
+ end
2032
+ end
2033
+ [header_length, space_remaining, align, split_string]
2034
+ end
2035
+
2036
+ def_delegator self, :split_string_setup
2037
+
2038
+ #
2039
+ # Write all of the workbooks strings into an indexed array.
2040
+ #
2041
+ # See the comments in calculate_shared_string_sizes() for more information.
2042
+ #
2043
+ # We also use this routine to record the offsets required by the EXTSST table.
2044
+ # In order to do this we first identify the first string in an EXTSST bucket
2045
+ # and then store its global and local offset within the SST table. The offset
2046
+ # occurs wherever the start of the bucket string is written out via append().
2047
+ #
2048
+ def store_shared_strings #:nodoc:
2049
+ record = 0x00FC # Record identifier
2050
+ length = 0x0008 # Number of bytes to follow
2051
+ total = 0x0000
2052
+
2053
+ # Iterate through the strings to calculate the CONTINUE block sizes
2054
+ continue_limit = 8208
2055
+ block_length = 0
2056
+ written = 0
2057
+ continue = 0
2058
+
2059
+ # The SST and CONTINUE block sizes have been pre-calculated by
2060
+ # calculate_shared_string_sizes()
2061
+ block_sizes = @shared_string_table.block_sizes
2062
+
2063
+ # The SST record is required even if it contains no strings. Thus we will
2064
+ # always have a length
2065
+ #
2066
+ if block_sizes.size != 0
2067
+ length = 8 + block_sizes.shift
2068
+ else
2069
+ # No strings
2070
+ length = 8
2071
+ end
2072
+
2073
+ # Initialise variables used to track EXTSST bucket offsets.
2074
+ extsst_str_num = -1
2075
+ sst_block_start = @datasize
2076
+
2077
+ # Write the SST block header information
2078
+ header = [record, length].pack("vv")
2079
+ data = [@shared_string_table.str_total, @shared_string_table.str_unique].pack("VV")
2080
+ append(header, data)
2081
+
2082
+ # Iterate through the strings and write them out
2083
+ return if @shared_string_table.strings.empty?
2084
+ @shared_string_table.strings.each do |string|
2085
+
2086
+ string_length = string.bytesize
2087
+
2088
+ # Check if the string is at the start of a EXTSST bucket.
2089
+ extsst_str_num += 1
2090
+ # Used to track EXTSST bucket offsets.
2091
+ bucket_string = (extsst_str_num % @extsst_bucket_size == 0)
2092
+
2093
+ # Block length is the total length of the strings that will be
2094
+ # written out in a single SST or CONTINUE block.
2095
+ #
2096
+ block_length += string_length
2097
+
2098
+ # We can write the string if it doesn't cross a CONTINUE boundary
2099
+ if block_length < continue_limit
2100
+
2101
+ # Store location of EXTSST bucket string.
2102
+ if bucket_string
2103
+ @extsst_offsets.push([@datasize, @datasize - sst_block_start])
2104
+ bucket_string = false
2105
+ end
2106
+
2107
+ append(string)
2108
+ written += string_length
2109
+ next
2110
+ end
2111
+
2112
+ # Deal with the cases where the next string to be written will exceed
2113
+ # the CONTINUE boundary. If the string is very long it may need to be
2114
+ # written in more than one CONTINUE record.
2115
+ encoding = string.unpack("xx C")[0]
2116
+ split_string = 0
2117
+ while block_length >= continue_limit
2118
+ header_length, space_remaining, align, split_string =
2119
+ split_string_setup(encoding, split_string, continue_limit, written, continue)
2120
+
2121
+ if space_remaining > header_length
2122
+ # Write as much as possible of the string in the current block
2123
+ tmp = string[0, space_remaining]
2124
+
2125
+ # Store location of EXTSST bucket string.
2126
+ if bucket_string
2127
+ @extsst_offsets.push([@datasize, @datasize - sst_block_start])
2128
+ bucket_string = false
2129
+ end
2130
+
2131
+ append(tmp)
2132
+
2133
+ # The remainder will be written in the next block(s)
2134
+ string = string[space_remaining .. string.length-1]
2135
+
2136
+ # Reduce the current block length by the amount written
2137
+ block_length -= continue_limit -continue -align
2138
+
2139
+ # If the current string was split then the next CONTINUE block
2140
+ # should have the string continue flag (grbit) set unless the
2141
+ # split string fits exactly into the remaining space.
2142
+ #
2143
+ if block_length > 0
2144
+ continue = 1
2145
+ else
2146
+ continue = 0
2147
+ end
2148
+ else
2149
+ # Not enough space to start the string in the current block
2150
+ block_length -= continue_limit -space_remaining -continue
2151
+ continue = 0
2152
+ end
2153
+
2154
+ # Write the CONTINUE block header
2155
+ if block_sizes.size != 0
2156
+ sst_block_start= @datasize # Reset EXTSST offset.
2157
+
2158
+ record = 0x003C
2159
+ length = block_sizes.shift
2160
+
2161
+ header = [record, length].pack("vv")
2162
+ header += [encoding].pack("C") if continue != 0
2163
+
2164
+ append(header)
2165
+ end
2166
+
2167
+ # If the string (or substr) is small enough we can write it in the
2168
+ # new CONTINUE block. Else, go through the loop again to write it in
2169
+ # one or more CONTINUE blocks
2170
+ #
2171
+ if block_length < continue_limit
2172
+
2173
+ # Store location of EXTSST bucket string.
2174
+ if bucket_string
2175
+ @extsst_offsets.push([@datasize, @datasize - sst_block_start])
2176
+ bucket_string = false
2177
+ end
2178
+ append(string)
2179
+
2180
+ written = block_length
2181
+ else
2182
+ written = 0
2183
+ end
2184
+ end
2185
+ end
2186
+ end
2187
+
2188
+ #
2189
+ # The number of buckets used in the EXTSST is between 0 and 128. The number of
2190
+ # strings per bucket (bucket size) has a minimum value of 8 and a theoretical
2191
+ # maximum of 2^16. For "number of strings" < 1024 there is a constant bucket
2192
+ # size of 8. The following algorithm generates the same size/bucket ratio
2193
+ # as Excel.
2194
+ #
2195
+ def calculate_extsst_size(str_unique) #:nodoc:
2196
+ unique_strings = str_unique
2197
+
2198
+ if unique_strings < 1024
2199
+ bucket_size = 8
2200
+ else
2201
+ bucket_size = 1 + Integer(unique_strings / 128.0)
2202
+ end
2203
+
2204
+ buckets = Integer((unique_strings + bucket_size -1) / Float(bucket_size))
2205
+
2206
+ @extsst_buckets = buckets
2207
+ @extsst_bucket_size = bucket_size
2208
+
2209
+ 6 + 8 * buckets
2210
+ end
2211
+
2212
+ #
2213
+ # Write EXTSST table using the offsets calculated in store_shared_strings().
2214
+ #
2215
+ def store_extsst #:nodoc:
2216
+ offsets = @extsst_offsets
2217
+ bucket_size = @extsst_bucket_size
2218
+
2219
+ record = 0x00FF # Record identifier
2220
+ length = 2 + 8 * offsets.size # Bytes to follow
2221
+
2222
+ header = [record, length].pack('vv')
2223
+ data = [bucket_size].pack('v')
2224
+
2225
+ offsets.each do |offset|
2226
+ data += [offset[0], offset[1], 0].pack('Vvv')
2227
+ end
2228
+
2229
+ append(header, data)
2230
+ end
2231
+
2232
+ #
2233
+ # Methods related to comments and MSO objects.
2234
+ #
2235
+
2236
+ #
2237
+ # Write the MSODRAWINGGROUP record that keeps track of the Escher drawing
2238
+ # objects in the file such as images, comments and filters.
2239
+ #
2240
+ def add_mso_drawing_group #:nodoc:
2241
+ return unless @mso_size != 0
2242
+
2243
+ record = 0x00EB # Record identifier
2244
+ length = 0x0000 # Number of bytes to follow
2245
+
2246
+ data = store_mso_dgg_container
2247
+ data += store_mso_dgg(*@mso_clusters)
2248
+ data += store_mso_bstore_container
2249
+ @images_data.each do |image|
2250
+ data += store_mso_images(image)
2251
+ end
2252
+ data += store_mso_opt
2253
+ data += store_mso_split_menu_colors
2254
+
2255
+ length = data.bytesize
2256
+ header = [record, length].pack("vv")
2257
+
2258
+ add_mso_drawing_group_continue(header + data)
2259
+
2260
+ header + data # For testing only.
2261
+ end
2262
+
2263
+ #
2264
+ # See first BIFFwriter#add_continue() method.
2265
+ #
2266
+ # Add specialised CONTINUE headers to large MSODRAWINGGROUP data block.
2267
+ # We use the Excel 97 max block size of 8228 - 4 bytes for the header = 8224.
2268
+ #
2269
+ # The structure depends on the size of the data block:
2270
+ #
2271
+ # Case 1: <= 8224 bytes 1 MSODRAWINGGROUP
2272
+ # Case 2: <= 2*8224 bytes 1 MSODRAWINGGROUP + 1 CONTINUE
2273
+ # Case 3: > 2*8224 bytes 2 MSODRAWINGGROUP + n CONTINUE
2274
+ #
2275
+ def add_mso_drawing_group_continue(data) #:nodoc:
2276
+ limit = 8228 -4
2277
+ mso_group = 0x00EB # Record identifier
2278
+ continue = 0x003C # Record identifier
2279
+ block_count = 1
2280
+
2281
+ # Ignore the base class add_continue() method.
2282
+ @ignore_continue = true
2283
+
2284
+ # Case 1 above. Just return the data as it is.
2285
+ if data.bytesize <= limit
2286
+ append(data)
2287
+ return
2288
+ end
2289
+
2290
+ # Change length field of the first MSODRAWINGGROUP block. Case 2 and 3.
2291
+ tmp, data = devide_string(data, limit + 4)
2292
+ tmp[2, 2] = [limit].pack('v')
2293
+ append(tmp)
2294
+
2295
+ # Add MSODRAWINGGROUP and CONTINUE blocks for Case 3 above.
2296
+ while data.bytesize > limit
2297
+ if block_count == 1
2298
+ # Add extra MSODRAWINGGROUP block header.
2299
+ header = [mso_group, limit].pack("vv")
2300
+ block_count += 1
2301
+ else
2302
+ # Add normal CONTINUE header.
2303
+ header = [continue, limit].pack("vv")
2304
+ end
2305
+
2306
+ tmp, data = devide_string(data, limit)
2307
+ append(header, tmp)
2308
+ end
2309
+
2310
+ # Last CONTINUE block for remaining data. Case 2 and 3 above.
2311
+ header = [continue, data.bytesize].pack("vv")
2312
+ append(header, data)
2313
+
2314
+ # Turn the base class add_continue() method back on.
2315
+ @ignore_continue = false
2316
+ end
2317
+
2318
+ def devide_string(string, nth) #:nodoc:
2319
+ first_string = string[0, nth]
2320
+ latter_string = string[nth, string.size - nth]
2321
+ [first_string, latter_string]
2322
+ end
2323
+
2324
+ #
2325
+ # Write the Escher DggContainer record that is part of MSODRAWINGGROUP.
2326
+ #
2327
+ def store_mso_dgg_container #:nodoc:
2328
+ type = 0xF000
2329
+ version = 15
2330
+ instance = 0
2331
+ data = ''
2332
+ length = @mso_size -12 # -4 (biff header) -8 (for this).
2333
+
2334
+ add_mso_generic(type, version, instance, data, length)
2335
+ end
2336
+
2337
+ #
2338
+ # Write the Escher Dgg record that is part of MSODRAWINGGROUP.
2339
+ #
2340
+ def store_mso_dgg(max_spid, num_clusters, shapes_saved, drawings_saved, clusters) #:nodoc:
2341
+ type = 0xF006
2342
+ version = 0
2343
+ instance = 0
2344
+ data = ''
2345
+ length = nil # Calculate automatically.
2346
+
2347
+ data = [max_spid, num_clusters,
2348
+ shapes_saved, drawings_saved].pack("VVVV")
2349
+
2350
+ clusters.each do |aref|
2351
+ drawing_id = aref[0]
2352
+ shape_ids_used = aref[1]
2353
+
2354
+ data += [drawing_id, shape_ids_used].pack("VV")
2355
+ end
2356
+
2357
+ add_mso_generic(type, version, instance, data, length)
2358
+ end
2359
+
2360
+ #
2361
+ # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP.
2362
+ #
2363
+ def store_mso_bstore_container #:nodoc:
2364
+ return '' if @images_size == 0
2365
+
2366
+ type = 0xF001
2367
+ version = 15
2368
+ instance = @images_data.size # Number of images.
2369
+ data = ''
2370
+ length = @images_size +8 *instance
2371
+
2372
+ add_mso_generic(type, version, instance, data, length)
2373
+ end
2374
+
2375
+ #
2376
+ # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP.
2377
+ #
2378
+ def store_mso_images(image)
2379
+ blip_store_entry = store_mso_blip_store_entry(
2380
+ image.ref_count, image.type, image.size, image.checksum1
2381
+ )
2382
+
2383
+ blip = store_mso_blip(
2384
+ image.type, image.data, image.size, image.checksum1, image.checksum2
2385
+ )
2386
+
2387
+ blip_store_entry + blip
2388
+ end
2389
+
2390
+ #
2391
+ # Write the Escher BlipStoreEntry record that is part of MSODRAWINGGROUP.
2392
+ #
2393
+ def store_mso_blip_store_entry(ref_count, image_type, size, checksum1) #:nodoc:
2394
+ type = 0xF007
2395
+ version = 2
2396
+ instance = image_type
2397
+ length = size +61
2398
+ data = [image_type].pack('C') + # Win32
2399
+ [image_type].pack('C') + # Mac
2400
+ [checksum1].pack('H*') + # Uid checksum
2401
+ [0xFF].pack('v') + # Tag
2402
+ [size +25].pack('V') + # Next Blip size
2403
+ [ref_count].pack('V') + # Image ref count
2404
+ [0x00000000].pack('V') + # File offset
2405
+ [0x00].pack('C') + # Usage
2406
+ [0x00].pack('C') + # Name length
2407
+ [0x00].pack('C') + # Unused
2408
+ [0x00].pack('C') # Unused
2409
+
2410
+ add_mso_generic(type, version, instance, data, length)
2411
+ end
2412
+
2413
+ #
2414
+ # Write the Escher Blip record that is part of MSODRAWINGGROUP.
2415
+ #
2416
+ def store_mso_blip(image_type, image_data, size, checksum1, checksum2) #:nodoc:
2417
+ instance = 0x046A if image_type == 5 # JPG
2418
+ instance = 0x06E0 if image_type == 6 # PNG
2419
+ instance = 0x07A9 if image_type == 7 # BMP
2420
+
2421
+ # BMPs contain an extra checksum for the stripped data.
2422
+ if image_type == 7
2423
+ checksum1 = checksum2 + checksum1
2424
+ end
2425
+
2426
+ type = 0xF018 + image_type
2427
+ version = 0x0000
2428
+ length = size +17
2429
+ data = [checksum1].pack('H*') + # Uid checksum
2430
+ [0xFF].pack('C') + # Tag
2431
+ image_data # Image
2432
+
2433
+ add_mso_generic(type, version, instance, data, length)
2434
+ end
2435
+
2436
+ #
2437
+ # Write the Escher Opt record that is part of MSODRAWINGGROUP.
2438
+ #
2439
+ def store_mso_opt #:nodoc:
2440
+ type = 0xF00B
2441
+ version = 3
2442
+ instance = 3
2443
+ data = ''
2444
+ length = 18
2445
+
2446
+ data = ['BF0008000800810109000008C0014000'+'0008'].pack("H*")
2447
+
2448
+ add_mso_generic(type, version, instance, data, length)
2449
+ end
2450
+
2451
+ #
2452
+ # Write the Escher SplitMenuColors record that is part of MSODRAWINGGROUP.
2453
+ #
2454
+ def store_mso_split_menu_colors #:nodoc:
2455
+ type = 0xF11E
2456
+ version = 0
2457
+ instance = 4
2458
+ data = ''
2459
+ length = 16
2460
+
2461
+ data = ['0D0000080C00000817000008F7000010'].pack("H*")
2462
+
2463
+ add_mso_generic(type, version, instance, data, length)
2464
+ end
2465
+
2466
+ def cleanup #:nodoc:
2467
+ super
2468
+ sheets.each { |sheet| sheet.cleanup }
2469
+ end
2470
+
2471
+ def add_doc_properties
2472
+ @add_doc_properties ||= false
2473
+ end
2474
+
2475
+ def defined_names
2476
+ @defined_names
2477
+ end
2478
+ end