roo 1.13.2 → 2.0.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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.simplecov +4 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG.md +515 -0
  6. data/Gemfile +16 -10
  7. data/Guardfile +24 -0
  8. data/LICENSE +3 -1
  9. data/README.md +254 -0
  10. data/Rakefile +23 -23
  11. data/examples/roo_soap_client.rb +28 -31
  12. data/examples/roo_soap_server.rb +4 -6
  13. data/examples/write_me.rb +9 -10
  14. data/lib/roo/base.rb +334 -395
  15. data/lib/roo/csv.rb +120 -113
  16. data/lib/roo/excelx/cell.rb +77 -0
  17. data/lib/roo/excelx/comments.rb +22 -0
  18. data/lib/roo/excelx/extractor.rb +22 -0
  19. data/lib/roo/excelx/relationships.rb +25 -0
  20. data/lib/roo/excelx/shared_strings.rb +37 -0
  21. data/lib/roo/excelx/sheet.rb +107 -0
  22. data/lib/roo/excelx/sheet_doc.rb +200 -0
  23. data/lib/roo/excelx/styles.rb +64 -0
  24. data/lib/roo/excelx/workbook.rb +59 -0
  25. data/lib/roo/excelx.rb +413 -597
  26. data/lib/roo/font.rb +17 -0
  27. data/lib/roo/libre_office.rb +5 -0
  28. data/lib/roo/link.rb +15 -0
  29. data/lib/roo/{openoffice.rb → open_office.rb} +681 -496
  30. data/lib/roo/spreadsheet.rb +20 -23
  31. data/lib/roo/utils.rb +78 -0
  32. data/lib/roo/version.rb +3 -0
  33. data/lib/roo.rb +18 -24
  34. data/roo.gemspec +20 -204
  35. data/spec/lib/roo/base_spec.rb +1 -4
  36. data/spec/lib/roo/csv_spec.rb +21 -13
  37. data/spec/lib/roo/excelx/format_spec.rb +7 -6
  38. data/spec/lib/roo/excelx_spec.rb +424 -11
  39. data/spec/lib/roo/libreoffice_spec.rb +16 -6
  40. data/spec/lib/roo/openoffice_spec.rb +13 -8
  41. data/spec/lib/roo/spreadsheet_spec.rb +40 -12
  42. data/spec/lib/roo/utils_spec.rb +106 -0
  43. data/spec/spec_helper.rb +2 -1
  44. data/test/test_generic_spreadsheet.rb +117 -139
  45. data/test/test_helper.rb +9 -56
  46. data/test/test_roo.rb +274 -478
  47. metadata +65 -303
  48. data/CHANGELOG +0 -417
  49. data/Gemfile.lock +0 -78
  50. data/README.markdown +0 -126
  51. data/VERSION +0 -1
  52. data/lib/roo/excel.rb +0 -355
  53. data/lib/roo/excel2003xml.rb +0 -300
  54. data/lib/roo/google.rb +0 -292
  55. data/lib/roo/roo_rails_helper.rb +0 -83
  56. data/lib/roo/worksheet.rb +0 -18
  57. data/scripts/txt2html +0 -67
  58. data/spec/lib/roo/excel2003xml_spec.rb +0 -15
  59. data/spec/lib/roo/excel_spec.rb +0 -17
  60. data/spec/lib/roo/google_spec.rb +0 -64
  61. data/test/files/1900_base.xls +0 -0
  62. data/test/files/1900_base.xlsx +0 -0
  63. data/test/files/1904_base.xls +0 -0
  64. data/test/files/1904_base.xlsx +0 -0
  65. data/test/files/Bibelbund.csv +0 -3741
  66. data/test/files/Bibelbund.ods +0 -0
  67. data/test/files/Bibelbund.xls +0 -0
  68. data/test/files/Bibelbund.xlsx +0 -0
  69. data/test/files/Bibelbund.xml +0 -62518
  70. data/test/files/Bibelbund1.ods +0 -0
  71. data/test/files/Pfand_from_windows_phone.xlsx +0 -0
  72. data/test/files/bad_excel_date.xls +0 -0
  73. data/test/files/bbu.ods +0 -0
  74. data/test/files/bbu.xls +0 -0
  75. data/test/files/bbu.xlsx +0 -0
  76. data/test/files/bbu.xml +0 -152
  77. data/test/files/bode-v1.ods.zip +0 -0
  78. data/test/files/bode-v1.xls.zip +0 -0
  79. data/test/files/boolean.csv +0 -2
  80. data/test/files/boolean.ods +0 -0
  81. data/test/files/boolean.xls +0 -0
  82. data/test/files/boolean.xlsx +0 -0
  83. data/test/files/boolean.xml +0 -112
  84. data/test/files/borders.ods +0 -0
  85. data/test/files/borders.xls +0 -0
  86. data/test/files/borders.xlsx +0 -0
  87. data/test/files/borders.xml +0 -144
  88. data/test/files/bug-numbered-sheet-names.xlsx +0 -0
  89. data/test/files/bug-row-column-fixnum-float.xls +0 -0
  90. data/test/files/bug-row-column-fixnum-float.xml +0 -127
  91. data/test/files/comments.ods +0 -0
  92. data/test/files/comments.xls +0 -0
  93. data/test/files/comments.xlsx +0 -0
  94. data/test/files/csvtypes.csv +0 -1
  95. data/test/files/datetime.ods +0 -0
  96. data/test/files/datetime.xls +0 -0
  97. data/test/files/datetime.xlsx +0 -0
  98. data/test/files/datetime.xml +0 -142
  99. data/test/files/datetime_floatconv.xls +0 -0
  100. data/test/files/datetime_floatconv.xml +0 -148
  101. data/test/files/dreimalvier.ods +0 -0
  102. data/test/files/emptysheets.ods +0 -0
  103. data/test/files/emptysheets.xls +0 -0
  104. data/test/files/emptysheets.xlsx +0 -0
  105. data/test/files/emptysheets.xml +0 -105
  106. data/test/files/excel2003.xml +0 -21140
  107. data/test/files/false_encoding.xls +0 -0
  108. data/test/files/false_encoding.xml +0 -132
  109. data/test/files/file_item_error.xlsx +0 -0
  110. data/test/files/formula.ods +0 -0
  111. data/test/files/formula.xls +0 -0
  112. data/test/files/formula.xlsx +0 -0
  113. data/test/files/formula.xml +0 -134
  114. data/test/files/formula_parse_error.xls +0 -0
  115. data/test/files/formula_parse_error.xml +0 -1833
  116. data/test/files/formula_string_error.xlsx +0 -0
  117. data/test/files/html-escape.ods +0 -0
  118. data/test/files/link.xls +0 -0
  119. data/test/files/link.xlsx +0 -0
  120. data/test/files/matrix.ods +0 -0
  121. data/test/files/matrix.xls +0 -0
  122. data/test/files/named_cells.ods +0 -0
  123. data/test/files/named_cells.xls +0 -0
  124. data/test/files/named_cells.xlsx +0 -0
  125. data/test/files/no_spreadsheet_file.txt +0 -1
  126. data/test/files/numbers1.csv +0 -18
  127. data/test/files/numbers1.ods +0 -0
  128. data/test/files/numbers1.xls +0 -0
  129. data/test/files/numbers1.xlsx +0 -0
  130. data/test/files/numbers1.xml +0 -312
  131. data/test/files/numeric-link.xlsx +0 -0
  132. data/test/files/only_one_sheet.ods +0 -0
  133. data/test/files/only_one_sheet.xls +0 -0
  134. data/test/files/only_one_sheet.xlsx +0 -0
  135. data/test/files/only_one_sheet.xml +0 -67
  136. data/test/files/paragraph.ods +0 -0
  137. data/test/files/paragraph.xls +0 -0
  138. data/test/files/paragraph.xlsx +0 -0
  139. data/test/files/paragraph.xml +0 -127
  140. data/test/files/prova.xls +0 -0
  141. data/test/files/ric.ods +0 -0
  142. data/test/files/simple_spreadsheet.ods +0 -0
  143. data/test/files/simple_spreadsheet.xls +0 -0
  144. data/test/files/simple_spreadsheet.xlsx +0 -0
  145. data/test/files/simple_spreadsheet.xml +0 -225
  146. data/test/files/simple_spreadsheet_from_italo.ods +0 -0
  147. data/test/files/simple_spreadsheet_from_italo.xls +0 -0
  148. data/test/files/simple_spreadsheet_from_italo.xml +0 -242
  149. data/test/files/so_datetime.csv +0 -7
  150. data/test/files/style.ods +0 -0
  151. data/test/files/style.xls +0 -0
  152. data/test/files/style.xlsx +0 -0
  153. data/test/files/style.xml +0 -154
  154. data/test/files/time-test.csv +0 -2
  155. data/test/files/time-test.ods +0 -0
  156. data/test/files/time-test.xls +0 -0
  157. data/test/files/time-test.xlsx +0 -0
  158. data/test/files/time-test.xml +0 -131
  159. data/test/files/type_excel.ods +0 -0
  160. data/test/files/type_excel.xlsx +0 -0
  161. data/test/files/type_excelx.ods +0 -0
  162. data/test/files/type_excelx.xls +0 -0
  163. data/test/files/type_openoffice.xls +0 -0
  164. data/test/files/type_openoffice.xlsx +0 -0
  165. data/test/files/whitespace.ods +0 -0
  166. data/test/files/whitespace.xls +0 -0
  167. data/test/files/whitespace.xlsx +0 -0
  168. data/test/files/whitespace.xml +0 -184
  169. data/test/rm_sub_test.rb +0 -12
  170. data/test/rm_test.rb +0 -7
  171. data/website/index.html +0 -385
  172. data/website/index.txt +0 -423
  173. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  174. data/website/stylesheets/screen.css +0 -130
  175. data/website/template.rhtml +0 -48
@@ -1,496 +1,681 @@
1
- require 'date'
2
- require 'nokogiri'
3
- require 'cgi'
4
-
5
- class Roo::OpenOffice < Roo::Base
6
- class << self
7
- def extract_content(tmpdir, filename)
8
- Roo::ZipFile.open(filename) do |zip|
9
- process_zipfile(tmpdir, zip)
10
- end
11
- end
12
-
13
- def process_zipfile(tmpdir, zip, path='')
14
- if zip.file.file? path
15
- if path == "content.xml"
16
- open(File.join(tmpdir, 'roo_content.xml'),'wb') {|f|
17
- f << zip.read(path)
18
- }
19
- end
20
- else
21
- unless path.empty?
22
- path += '/'
23
- end
24
- zip.dir.foreach(path) do |filename|
25
- process_zipfile(tmpdir, zip, path+filename)
26
- end
27
- end
28
- end
29
- end
30
-
31
- # initialization and opening of a spreadsheet file
32
- # values for packed: :zip
33
- def initialize(filename, options={}, deprecated_file_warning=:error, deprecated_tmpdir_root=nil)
34
- if Hash === options
35
- packed = options[:packed]
36
- file_warning = options[:file_warning] || :error
37
- tmpdir_root = options[:tmpdir_root]
38
- else
39
- warn 'Supplying `packed`, `file_warning`, or `tmpdir_root` as separate arguments to `Roo::OpenOffice.new` is deprecated. Use an options hash instead.'
40
- packed = options
41
- file_warning = deprecated_file_warning
42
- tmpdir_root = deprecated_tmpdir_root
43
- end
44
-
45
- file_type_check(filename,'.ods','an Roo::OpenOffice', file_warning, packed)
46
- make_tmpdir(tmpdir_root) do |tmpdir|
47
- filename = download_uri(filename, tmpdir) if uri?(filename)
48
- filename = unzip(filename, tmpdir) if packed == :zip
49
- #TODO: @cells_read[:default] = false
50
- @filename = filename
51
- unless File.file?(@filename)
52
- raise IOError, "file #{@filename} does not exist"
53
- end
54
- self.class.extract_content(tmpdir, @filename)
55
- @doc = load_xml(File.join(tmpdir, "roo_content.xml"))
56
- end
57
- super(filename, options)
58
- @formula = Hash.new
59
- @style = Hash.new
60
- @style_defaults = Hash.new { |h,k| h[k] = [] }
61
- @style_definitions = Hash.new
62
- @comment = Hash.new
63
- @comments_read = Hash.new
64
- end
65
-
66
- def method_missing(m,*args)
67
- read_labels
68
- # is method name a label name
69
- if @label.has_key?(m.to_s)
70
- row,col = label(m.to_s)
71
- cell(row,col)
72
- else
73
- # call super for methods like #a1
74
- super
75
- end
76
- end
77
-
78
- # Returns the content of a spreadsheet-cell.
79
- # (1,1) is the upper left corner.
80
- # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
81
- # cell at the first line and first row.
82
- def cell(row, col, sheet=nil)
83
- sheet ||= @default_sheet
84
- read_cells(sheet)
85
- row,col = normalize(row,col)
86
- if celltype(row,col,sheet) == :date
87
- yyyy,mm,dd = @cell[sheet][[row,col]].to_s.split('-')
88
- return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
89
- end
90
- @cell[sheet][[row,col]]
91
- end
92
-
93
- # Returns the formula at (row,col).
94
- # Returns nil if there is no formula.
95
- # The method #formula? checks if there is a formula.
96
- def formula(row,col,sheet=nil)
97
- sheet ||= @default_sheet
98
- read_cells(sheet)
99
- row,col = normalize(row,col)
100
- @formula[sheet][[row,col]]
101
- end
102
- alias_method :formula?, :formula
103
-
104
- # returns each formula in the selected sheet as an array of elements
105
- # [row, col, formula]
106
- def formulas(sheet=nil)
107
- sheet ||= @default_sheet
108
- read_cells(sheet)
109
- if @formula[sheet]
110
- @formula[sheet].each.collect do |elem|
111
- [elem[0][0], elem[0][1], elem[1]]
112
- end
113
- else
114
- []
115
- end
116
- end
117
-
118
- class Font
119
- attr_accessor :bold, :italic, :underline
120
-
121
- def bold?
122
- @bold == 'bold'
123
- end
124
-
125
- def italic?
126
- @italic == 'italic'
127
- end
128
-
129
- def underline?
130
- @underline != nil
131
- end
132
- end
133
-
134
- # Given a cell, return the cell's style
135
- def font(row, col, sheet=nil)
136
- sheet ||= @default_sheet
137
- read_cells(sheet)
138
- row,col = normalize(row,col)
139
- style_name = @style[sheet][[row,col]] || @style_defaults[sheet][col - 1] || 'Default'
140
- @style_definitions[style_name]
141
- end
142
-
143
- # returns the type of a cell:
144
- # * :float
145
- # * :string
146
- # * :date
147
- # * :percentage
148
- # * :formula
149
- # * :time
150
- # * :datetime
151
- def celltype(row,col,sheet=nil)
152
- sheet ||= @default_sheet
153
- read_cells(sheet)
154
- row,col = normalize(row,col)
155
- if @formula[sheet][[row,col]]
156
- return :formula
157
- else
158
- @cell_type[sheet][[row,col]]
159
- end
160
- end
161
-
162
- def sheets
163
- @doc.xpath("//*[local-name()='table']").map do |sheet|
164
- sheet.attributes["name"].value
165
- end
166
- end
167
-
168
- # version of the Roo::OpenOffice document
169
- # at 2007 this is always "1.0"
170
- def officeversion
171
- oo_version
172
- @officeversion
173
- end
174
-
175
- # shows the internal representation of all cells
176
- # mainly for debugging purposes
177
- def to_s(sheet=nil)
178
- sheet ||= @default_sheet
179
- read_cells(sheet)
180
- @cell[sheet].inspect
181
- end
182
-
183
- # returns the row,col values of the labelled cell
184
- # (nil,nil) if label is not defined
185
- def label(labelname)
186
- read_labels
187
- unless @label.size > 0
188
- return nil,nil,nil
189
- end
190
- if @label.has_key? labelname
191
- return @label[labelname][1].to_i,
192
- Roo::Base.letter_to_number(@label[labelname][2]),
193
- @label[labelname][0]
194
- else
195
- return nil,nil,nil
196
- end
197
- end
198
-
199
- # Returns an array which all labels. Each element is an array with
200
- # [labelname, [row,col,sheetname]]
201
- def labels(sheet=nil)
202
- read_labels
203
- @label.map do |label|
204
- [ label[0], # name
205
- [ label[1][1].to_i, # row
206
- Roo::Base.letter_to_number(label[1][2]), # column
207
- label[1][0], # sheet
208
- ] ]
209
- end
210
- end
211
-
212
- # returns the comment at (row/col)
213
- # nil if there is no comment
214
- def comment(row,col,sheet=nil)
215
- sheet ||= @default_sheet
216
- read_cells(sheet)
217
- row,col = normalize(row,col)
218
- return nil unless @comment[sheet]
219
- @comment[sheet][[row,col]]
220
- end
221
-
222
- # true, if there is a comment
223
- def comment?(row,col,sheet=nil)
224
- sheet ||= @default_sheet
225
- read_cells(sheet)
226
- row,col = normalize(row,col)
227
- comment(row,col) != nil
228
- end
229
-
230
-
231
- # returns each comment in the selected sheet as an array of elements
232
- # [row, col, comment]
233
- def comments(sheet=nil)
234
- sheet ||= @default_sheet
235
- read_comments(sheet) unless @comments_read[sheet]
236
- if @comment[sheet]
237
- @comment[sheet].each.collect do |elem|
238
- [elem[0][0],elem[0][1],elem[1]]
239
- end
240
- else
241
- []
242
- end
243
- end
244
-
245
- private
246
-
247
- # read the version of the OO-Version
248
- def oo_version
249
- @doc.xpath("//*[local-name()='document-content']").each do |office|
250
- @officeversion = attr(office,'version')
251
- end
252
- end
253
-
254
- # helper function to set the internal representation of cells
255
- def set_cell_values(sheet,x,y,i,v,value_type,formula,table_cell,str_v,style_name)
256
- key = [y,x+i]
257
- @cell_type[sheet] = {} unless @cell_type[sheet]
258
- @cell_type[sheet][key] = Roo::OpenOffice.oo_type_2_roo_type(value_type)
259
- @formula[sheet] = {} unless @formula[sheet]
260
- if formula
261
- ['of:', 'oooc:'].each do |prefix|
262
- if formula[0,prefix.length] == prefix
263
- formula = formula[prefix.length..-1]
264
- end
265
- end
266
- @formula[sheet][key] = formula
267
- end
268
- @cell[sheet] = {} unless @cell[sheet]
269
- @style[sheet] = {} unless @style[sheet]
270
- @style[sheet][key] = style_name
271
- case @cell_type[sheet][key]
272
- when :float
273
- @cell[sheet][key] = v.to_f
274
- when :string
275
- @cell[sheet][key] = str_v
276
- when :date
277
- #TODO: if table_cell.attributes['date-value'].size != "XXXX-XX-XX".size
278
- if attr(table_cell,'date-value').size != "XXXX-XX-XX".size
279
- #-- dann ist noch eine Uhrzeit vorhanden
280
- #-- "1961-11-21T12:17:18"
281
- @cell[sheet][key] = DateTime.parse(attr(table_cell,'date-value').to_s)
282
- @cell_type[sheet][key] = :datetime
283
- else
284
- @cell[sheet][key] = table_cell.attributes['date-value']
285
- end
286
- when :percentage
287
- @cell[sheet][key] = v.to_f
288
- when :time
289
- hms = v.split(':')
290
- @cell[sheet][key] = hms[0].to_i*3600 + hms[1].to_i*60 + hms[2].to_i
291
- else
292
- @cell[sheet][key] = v
293
- end
294
- end
295
-
296
- # read all cells in the selected sheet
297
- #--
298
- # the following construct means '4 blanks'
299
- # some content <text:s text:c="3"/>
300
- #++
301
- def read_cells(sheet=nil)
302
- sheet ||= @default_sheet
303
- validate_sheet!(sheet)
304
- return if @cells_read[sheet]
305
-
306
- sheet_found = false
307
- @doc.xpath("//*[local-name()='table']").each do |ws|
308
- if sheet == attr(ws,'name')
309
- sheet_found = true
310
- col = 1
311
- row = 1
312
- ws.children.each do |table_element|
313
- case table_element.name
314
- when 'table-column'
315
- @style_defaults[sheet] << table_element.attributes['default-cell-style-name']
316
- when 'table-row'
317
- if table_element.attributes['number-rows-repeated']
318
- skip_row = attr(table_element,'number-rows-repeated').to_s.to_i
319
- row = row + skip_row - 1
320
- end
321
- table_element.children.each do |cell|
322
- skip_col = attr(cell, 'number-columns-repeated')
323
- formula = attr(cell,'formula')
324
- value_type = attr(cell,'value-type')
325
- v = attr(cell,'value')
326
- style_name = attr(cell,'style-name')
327
- case value_type
328
- when 'string'
329
- str_v = ''
330
- # insert \n if there is more than one paragraph
331
- para_count = 0
332
- cell.children.each do |str|
333
- # begin comments
334
- =begin
335
- - <table:table-cell office:value-type="string">
336
- - <office:annotation office:display="true" draw:style-name="gr1" draw:text-style-name="P1" svg:width="1.1413in" svg:height="0.3902in" svg:x="2.0142in" svg:y="0in" draw:caption-point-x="-0.2402in" draw:caption-point-y="0.5661in">
337
- <dc:date>2011-09-20T00:00:00</dc:date>
338
- <text:p text:style-name="P1">Kommentar fuer B4</text:p>
339
- </office:annotation>
340
- <text:p>B4 (mit Kommentar)</text:p>
341
- </table:table-cell>
342
- =end
343
- if str.name == 'annotation'
344
- str.children.each do |annotation|
345
- if annotation.name == 'p'
346
- # @comment ist ein Hash mit Sheet als Key (wie bei @cell)
347
- # innerhalb eines Elements besteht ein Eintrag aus einem
348
- # weiteren Hash mit Key [row,col] und dem eigentlichen
349
- # Kommentartext als Inhalt
350
- @comment[sheet] = Hash.new unless @comment[sheet]
351
- key = [row,col]
352
- @comment[sheet][key] = annotation.text
353
- end
354
- end
355
- end
356
- # end comments
357
- if str.name == 'p'
358
- v = str.content
359
- str_v += "\n" if para_count > 0
360
- para_count += 1
361
- if str.children.size > 1
362
- str_v += children_to_string(str.children)
363
- else
364
- str.children.each do |child|
365
- str_v += child.content #.text
366
- end
367
- end
368
- str_v.gsub!(/&apos;/,"'") # special case not supported by unescapeHTML
369
- str_v = CGI.unescapeHTML(str_v)
370
- end # == 'p'
371
- end
372
- when 'time'
373
- cell.children.each do |str|
374
- if str.name == 'p'
375
- v = str.content
376
- end
377
- end
378
- when '', nil
379
- #
380
- when 'date'
381
- #
382
- when 'percentage'
383
- #
384
- when 'float'
385
- #
386
- when 'boolean'
387
- v = attr(cell,'boolean-value').to_s
388
- else
389
- # raise "unknown type #{value_type}"
390
- end
391
- if skip_col
392
- if v != nil or cell.attributes['date-value']
393
- 0.upto(skip_col.to_i-1) do |i|
394
- set_cell_values(sheet,col,row,i,v,value_type,formula,cell,str_v,style_name)
395
- end
396
- end
397
- col += (skip_col.to_i - 1)
398
- end # if skip
399
- set_cell_values(sheet,col,row,0,v,value_type,formula,cell,str_v,style_name)
400
- col += 1
401
- end
402
- row += 1
403
- col = 1
404
- end
405
- end
406
- end
407
- end
408
- @doc.xpath("//*[local-name()='automatic-styles']").each do |style|
409
- read_styles(style)
410
- end
411
- if !sheet_found
412
- raise RangeError
413
- end
414
- @cells_read[sheet] = true
415
- @comments_read[sheet] = true
416
- end
417
-
418
- # Only calls read_cells because Roo::Base calls read_comments
419
- # whereas the reading of comments is done in read_cells for Roo::OpenOffice-objects
420
- def read_comments(sheet=nil)
421
- read_cells(sheet)
422
- end
423
-
424
- def read_labels
425
- @label ||= Hash[@doc.xpath("//table:named-range").map do |ne|
426
- #-
427
- # $Sheet1.$C$5
428
- #+
429
- name = attr(ne,'name').to_s
430
- sheetname,coords = attr(ne,'cell-range-address').to_s.split('.$')
431
- col, row = coords.split('$')
432
- sheetname = sheetname[1..-1] if sheetname[0,1] == '$'
433
- [name, [sheetname,row,col]]
434
- end]
435
- end
436
-
437
- def read_styles(style_elements)
438
- @style_definitions['Default'] = Roo::OpenOffice::Font.new
439
- style_elements.each do |style|
440
- next unless style.name == 'style'
441
- style_name = attr(style,'name')
442
- style.each do |properties|
443
- font = Roo::OpenOffice::Font.new
444
- font.bold = attr(properties,'font-weight')
445
- font.italic = attr(properties,'font-style')
446
- font.underline = attr(properties,'text-underline-style')
447
- @style_definitions[style_name] = font
448
- end
449
- end
450
- end
451
-
452
- A_ROO_TYPE = {
453
- "float" => :float,
454
- "string" => :string,
455
- "date" => :date,
456
- "percentage" => :percentage,
457
- "time" => :time,
458
- }
459
-
460
- def self.oo_type_2_roo_type(ootype)
461
- return A_ROO_TYPE[ootype]
462
- end
463
-
464
- # helper method to convert compressed spaces and other elements within
465
- # an text into a string
466
- def children_to_string(children)
467
- result = ''
468
- children.each {|child|
469
- if child.text?
470
- result = result + child.content
471
- else
472
- if child.name == 's'
473
- compressed_spaces = child.attributes['c'].to_s.to_i
474
- # no explicit number means a count of 1:
475
- if compressed_spaces == 0
476
- compressed_spaces = 1
477
- end
478
- result = result + " "*compressed_spaces
479
- else
480
- result = result + child.content
481
- end
482
- end
483
- }
484
- result
485
- end
486
-
487
- def attr(node, attr_name)
488
- if node.attributes[attr_name]
489
- node.attributes[attr_name].value
490
- end
491
- end
492
- end # class
493
-
494
- # LibreOffice is just an alias for Roo::OpenOffice class
495
- class Roo::LibreOffice < Roo::OpenOffice
496
- end
1
+ require 'date'
2
+ require 'nokogiri'
3
+ require 'cgi'
4
+ require 'zip/filesystem'
5
+ require 'roo/font'
6
+
7
+ class Roo::OpenOffice < Roo::Base
8
+ # initialization and opening of a spreadsheet file
9
+ # values for packed: :zip
10
+ def initialize(filename, options={})
11
+ packed = options[:packed]
12
+ file_warning = options[:file_warning] || :error
13
+
14
+ @only_visible_sheets = options[:only_visible_sheets]
15
+ file_type_check(filename,'.ods','an Roo::OpenOffice', file_warning, packed)
16
+ @tmpdir = make_tmpdir(File.basename(filename), options[:tmpdir_root])
17
+ @filename = local_filename(filename, @tmpdir, packed)
18
+ #TODO: @cells_read[:default] = false
19
+ Zip::File.open(@filename) do |zip_file|
20
+ if content_entry = zip_file.glob("content.xml").first
21
+ roo_content_xml_path = File.join(@tmpdir, 'roo_content.xml')
22
+ content_entry.extract(roo_content_xml_path)
23
+ decrypt_if_necessary(
24
+ zip_file,
25
+ content_entry,
26
+ roo_content_xml_path,
27
+ options
28
+ )
29
+ else
30
+ raise ArgumentError, 'file missing required content.xml'
31
+ end
32
+ end
33
+ super(filename, options)
34
+ @formula = Hash.new
35
+ @style = Hash.new
36
+ @style_defaults = Hash.new { |h,k| h[k] = [] }
37
+ @table_display = Hash.new { |h,k| h[k] = true }
38
+ @font_style_definitions = Hash.new
39
+ @comment = Hash.new
40
+ @comments_read = Hash.new
41
+ rescue => e # clean up any temp files, but only if an error was raised
42
+ close
43
+ raise e
44
+ end
45
+
46
+ def method_missing(m,*args)
47
+ read_labels
48
+ # is method name a label name
49
+ if @label.has_key?(m.to_s)
50
+ row,col = label(m.to_s)
51
+ cell(row,col)
52
+ else
53
+ # call super for methods like #a1
54
+ super
55
+ end
56
+ end
57
+
58
+ # Returns the content of a spreadsheet-cell.
59
+ # (1,1) is the upper left corner.
60
+ # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
61
+ # cell at the first line and first row.
62
+ def cell(row, col, sheet=nil)
63
+ sheet ||= default_sheet
64
+ read_cells(sheet)
65
+ row,col = normalize(row,col)
66
+ if celltype(row,col,sheet) == :date
67
+ yyyy,mm,dd = @cell[sheet][[row,col]].to_s.split('-')
68
+ return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
69
+ end
70
+ @cell[sheet][[row,col]]
71
+ end
72
+
73
+ # Returns the formula at (row,col).
74
+ # Returns nil if there is no formula.
75
+ # The method #formula? checks if there is a formula.
76
+ def formula(row,col,sheet=nil)
77
+ sheet ||= default_sheet
78
+ read_cells(sheet)
79
+ row,col = normalize(row,col)
80
+ @formula[sheet][[row,col]]
81
+ end
82
+
83
+ # Predicate methods really should return a boolean
84
+ # value. Hopefully no one was relying on the fact that this
85
+ # previously returned either nil/formula
86
+ def formula?(*args)
87
+ !!formula(*args)
88
+ end
89
+
90
+ # returns each formula in the selected sheet as an array of elements
91
+ # [row, col, formula]
92
+ def formulas(sheet=nil)
93
+ sheet ||= default_sheet
94
+ read_cells(sheet)
95
+ if @formula[sheet]
96
+ @formula[sheet].each.collect do |elem|
97
+ [elem[0][0], elem[0][1], elem[1]]
98
+ end
99
+ else
100
+ []
101
+ end
102
+ end
103
+
104
+ # Given a cell, return the cell's style
105
+ def font(row, col, sheet=nil)
106
+ sheet ||= default_sheet
107
+ read_cells(sheet)
108
+ row,col = normalize(row,col)
109
+ style_name = @style[sheet][[row,col]] || @style_defaults[sheet][col - 1] || 'Default'
110
+ @font_style_definitions[style_name]
111
+ end
112
+
113
+ # returns the type of a cell:
114
+ # * :float
115
+ # * :string
116
+ # * :date
117
+ # * :percentage
118
+ # * :formula
119
+ # * :time
120
+ # * :datetime
121
+ def celltype(row,col,sheet=nil)
122
+ sheet ||= default_sheet
123
+ read_cells(sheet)
124
+ row,col = normalize(row,col)
125
+ if @formula[sheet][[row,col]]
126
+ return :formula
127
+ else
128
+ @cell_type[sheet][[row,col]]
129
+ end
130
+ end
131
+
132
+ def sheets
133
+ unless @table_display.any?
134
+ doc.xpath("//*[local-name()='automatic-styles']").each do |style|
135
+ read_table_styles(style)
136
+ end
137
+ end
138
+ doc.xpath("//*[local-name()='table']").map do |sheet|
139
+ if !@only_visible_sheets || @table_display[attr(sheet,'style-name')]
140
+ sheet.attributes["name"].value
141
+ end
142
+ end.compact
143
+ end
144
+
145
+ # version of the Roo::OpenOffice document
146
+ # at 2007 this is always "1.0"
147
+ def officeversion
148
+ oo_version
149
+ @officeversion
150
+ end
151
+
152
+ # shows the internal representation of all cells
153
+ # mainly for debugging purposes
154
+ def to_s(sheet=nil)
155
+ sheet ||= default_sheet
156
+ read_cells(sheet)
157
+ @cell[sheet].inspect
158
+ end
159
+
160
+ # returns the row,col values of the labelled cell
161
+ # (nil,nil) if label is not defined
162
+ def label(labelname)
163
+ read_labels
164
+ unless @label.size > 0
165
+ return nil,nil,nil
166
+ end
167
+ if @label.has_key? labelname
168
+ return @label[labelname][1].to_i,
169
+ ::Roo::Utils.letter_to_number(@label[labelname][2]),
170
+ @label[labelname][0]
171
+ else
172
+ return nil,nil,nil
173
+ end
174
+ end
175
+
176
+ # Returns an array which all labels. Each element is an array with
177
+ # [labelname, [row,col,sheetname]]
178
+ def labels(sheet=nil)
179
+ read_labels
180
+ @label.map do |label|
181
+ [ label[0], # name
182
+ [ label[1][1].to_i, # row
183
+ ::Roo::Utils.letter_to_number(label[1][2]), # column
184
+ label[1][0], # sheet
185
+ ] ]
186
+ end
187
+ end
188
+
189
+ # returns the comment at (row/col)
190
+ # nil if there is no comment
191
+ def comment(row,col,sheet=nil)
192
+ sheet ||= default_sheet
193
+ read_cells(sheet)
194
+ row,col = normalize(row,col)
195
+ return nil unless @comment[sheet]
196
+ @comment[sheet][[row,col]]
197
+ end
198
+
199
+ # returns each comment in the selected sheet as an array of elements
200
+ # [row, col, comment]
201
+ def comments(sheet=nil)
202
+ sheet ||= default_sheet
203
+ read_comments(sheet) unless @comments_read[sheet]
204
+ if @comment[sheet]
205
+ @comment[sheet].each.collect do |elem|
206
+ [elem[0][0],elem[0][1],elem[1]]
207
+ end
208
+ else
209
+ []
210
+ end
211
+ end
212
+
213
+ private
214
+
215
+ # If the ODS file has an encryption-data element, then try to decrypt.
216
+ # If successful, the temporary content.xml will be overwritten with
217
+ # decrypted contents.
218
+ def decrypt_if_necessary(
219
+ zip_file,
220
+ content_entry,
221
+ roo_content_xml_path, options
222
+ )
223
+ # Check if content.xml is encrypted by extracting manifest.xml
224
+ # and searching for a manifest:encryption-data element
225
+
226
+ if manifest_entry = zip_file.glob("META-INF/manifest.xml").first
227
+ roo_manifest_xml_path = File.join(@tmpdir, "roo_manifest.xml")
228
+ manifest_entry.extract(roo_manifest_xml_path)
229
+ manifest = ::Roo::Utils.load_xml(roo_manifest_xml_path)
230
+
231
+ # XPath search for manifest:encryption-data only for the content.xml
232
+ # file
233
+
234
+ encryption_data = manifest.xpath(
235
+ "//manifest:file-entry[@manifest:full-path='content.xml']"\
236
+ "/manifest:encryption-data"
237
+ ).first
238
+
239
+ # If XPath returns a node, then we know content.xml is encrypted
240
+
241
+ if !encryption_data.nil?
242
+
243
+ # Since we know it's encrypted, we check for the password option
244
+ # and if it doesn't exist, raise an argument error
245
+
246
+ password = options[:password]
247
+ if !password.nil?
248
+ perform_decryption(
249
+ encryption_data,
250
+ password,
251
+ content_entry,
252
+ roo_content_xml_path
253
+ )
254
+ else
255
+ raise ArgumentError,
256
+ 'file is encrypted but password was not supplied'
257
+ end
258
+ end
259
+ else
260
+ raise ArgumentError, 'file missing required META-INF/manifest.xml'
261
+ end
262
+ end
263
+
264
+ # Process the ODS encryption manifest and perform the decryption
265
+ def perform_decryption(
266
+ encryption_data,
267
+ password,
268
+ content_entry,
269
+ roo_content_xml_path
270
+ )
271
+ # Extract various expected attributes from the manifest that
272
+ # describe the encryption
273
+
274
+ algorithm_node = encryption_data.xpath("manifest:algorithm").first
275
+ key_derivation_node =
276
+ encryption_data.xpath("manifest:key-derivation").first
277
+ start_key_generation_node =
278
+ encryption_data.xpath("manifest:start-key-generation").first
279
+
280
+ # If we have all the expected elements, then we can perform
281
+ # the decryption.
282
+
283
+ if !algorithm_node.nil? && !key_derivation_node.nil? &&
284
+ !start_key_generation_node.nil?
285
+
286
+ # The algorithm is a URI describing the algorithm used
287
+ algorithm = algorithm_node['manifest:algorithm-name']
288
+
289
+ # The initialization vector is base-64 encoded
290
+ iv = Base64.decode64(
291
+ algorithm_node['manifest:initialisation-vector']
292
+ )
293
+ key_derivation_name =
294
+ key_derivation_node['manifest:key-derivation-name']
295
+ key_size = key_derivation_node['manifest:key-size'].to_i
296
+ iteration_count =
297
+ key_derivation_node['manifest:iteration-count'].to_i
298
+ salt = Base64.decode64(key_derivation_node['manifest:salt'])
299
+
300
+ # The key is hashed with an algorithm represented by this URI
301
+ key_generation_name =
302
+ start_key_generation_node[
303
+ 'manifest:start-key-generation-name'
304
+ ]
305
+ key_generation_size =
306
+ start_key_generation_node['manifest:key-size'].to_i
307
+
308
+ hashed_password = password
309
+ key = nil
310
+
311
+ if key_generation_name.eql?(
312
+ "http://www.w3.org/2000/09/xmldsig#sha256"
313
+ )
314
+ hashed_password = Digest::SHA256.digest(password)
315
+ else
316
+ raise ArgumentError, 'Unknown key generation algorithm ' +
317
+ key_generation_name
318
+ end
319
+
320
+ cipher = find_cipher(
321
+ algorithm,
322
+ key_derivation_name,
323
+ hashed_password,
324
+ salt,
325
+ iteration_count,
326
+ iv
327
+ )
328
+
329
+ begin
330
+ decrypted = decrypt(content_entry, cipher)
331
+
332
+ # Finally, inflate the decrypted stream and overwrite
333
+ # content.xml
334
+ IO.binwrite(
335
+ roo_content_xml_path,
336
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(decrypted)
337
+ )
338
+ rescue StandardError => error
339
+ raise ArgumentError,
340
+ 'Invalid password or other data error: ' + error.to_s
341
+ end
342
+ else
343
+ raise ArgumentError,
344
+ 'manifest.xml missing encryption-data elements'
345
+ end
346
+ end
347
+
348
+ # Create a cipher based on an ODS algorithm URI from manifest.xml
349
+ def find_cipher(
350
+ algorithm,
351
+ key_derivation_name,
352
+ hashed_password,
353
+ salt,
354
+ iteration_count,
355
+ iv
356
+ )
357
+ cipher = nil
358
+ if algorithm.eql? "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
359
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
360
+ cipher.decrypt
361
+ cipher.padding = 0
362
+ cipher.key = find_cipher_key(
363
+ cipher,
364
+ key_derivation_name,
365
+ hashed_password,
366
+ salt,
367
+ iteration_count
368
+ )
369
+ cipher.iv = iv
370
+ else
371
+ raise ArgumentError, 'Unknown algorithm ' + algorithm
372
+ end
373
+ cipher
374
+ end
375
+
376
+ # Create a cipher key based on an ODS algorithm string from manifest.xml
377
+ def find_cipher_key(
378
+ cipher,
379
+ key_derivation_name,
380
+ hashed_password,
381
+ salt,
382
+ iteration_count
383
+ )
384
+ if key_derivation_name.eql? "PBKDF2"
385
+ key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
386
+ hashed_password,
387
+ salt,
388
+ iteration_count,
389
+ cipher.key_len
390
+ )
391
+ else
392
+ raise ArgumentError, 'Unknown key derivation name ' +
393
+ key_derivation_name
394
+ end
395
+ key
396
+ end
397
+
398
+ # Block decrypt raw bytes from the zip file based on the cipher
399
+ def decrypt(content_entry, cipher)
400
+ # Zip::Entry.extract writes a 0-length file when trying
401
+ # to extract an encrypted stream, so we read the
402
+ # raw bytes based on the offset and lengths
403
+ decrypted = ""
404
+ File.open(@filename, "rb") do |zipfile|
405
+ zipfile.seek(
406
+ content_entry.local_header_offset +
407
+ content_entry.calculate_local_header_size
408
+ )
409
+ total_to_read = content_entry.compressed_size
410
+
411
+ block_size = 4096
412
+ block_size = total_to_read if block_size > total_to_read
413
+
414
+ while buffer = zipfile.read(block_size)
415
+ decrypted += cipher.update(buffer)
416
+ total_to_read -= buffer.length
417
+
418
+ break if total_to_read == 0
419
+
420
+ block_size = total_to_read if block_size > total_to_read
421
+ end
422
+ end
423
+
424
+ decrypted + cipher.final
425
+ end
426
+
427
+ def doc
428
+ @doc ||= ::Roo::Utils.load_xml(File.join(@tmpdir, "roo_content.xml"))
429
+ end
430
+
431
+ # read the version of the OO-Version
432
+ def oo_version
433
+ doc.xpath("//*[local-name()='document-content']").each do |office|
434
+ @officeversion = attr(office,'version')
435
+ end
436
+ end
437
+
438
+ # helper function to set the internal representation of cells
439
+ def set_cell_values(sheet,x,y,i,v,value_type,formula,table_cell,str_v,style_name)
440
+ key = [y,x+i]
441
+ @cell_type[sheet] = {} unless @cell_type[sheet]
442
+ @cell_type[sheet][key] = Roo::OpenOffice.oo_type_2_roo_type(value_type)
443
+ @formula[sheet] = {} unless @formula[sheet]
444
+ if formula
445
+ ['of:', 'oooc:'].each do |prefix|
446
+ if formula[0,prefix.length] == prefix
447
+ formula = formula[prefix.length..-1]
448
+ end
449
+ end
450
+ @formula[sheet][key] = formula
451
+ end
452
+ @cell[sheet] = {} unless @cell[sheet]
453
+ @style[sheet] = {} unless @style[sheet]
454
+ @style[sheet][key] = style_name
455
+ case @cell_type[sheet][key]
456
+ when :float
457
+ @cell[sheet][key] = v.to_f
458
+ when :string
459
+ @cell[sheet][key] = str_v
460
+ when :date
461
+ #TODO: if table_cell.attributes['date-value'].size != "XXXX-XX-XX".size
462
+ if attr(table_cell,'date-value').size != "XXXX-XX-XX".size
463
+ #-- dann ist noch eine Uhrzeit vorhanden
464
+ #-- "1961-11-21T12:17:18"
465
+ @cell[sheet][key] = DateTime.parse(attr(table_cell,'date-value').to_s)
466
+ @cell_type[sheet][key] = :datetime
467
+ else
468
+ @cell[sheet][key] = table_cell.attributes['date-value']
469
+ end
470
+ when :percentage
471
+ @cell[sheet][key] = v.to_f
472
+ when :time
473
+ hms = v.split(':')
474
+ @cell[sheet][key] = hms[0].to_i*3600 + hms[1].to_i*60 + hms[2].to_i
475
+ else
476
+ @cell[sheet][key] = v
477
+ end
478
+ end
479
+
480
+ # read all cells in the selected sheet
481
+ #--
482
+ # the following construct means '4 blanks'
483
+ # some content <text:s text:c="3"/>
484
+ #++
485
+ def read_cells(sheet = default_sheet)
486
+ validate_sheet!(sheet)
487
+ return if @cells_read[sheet]
488
+
489
+ sheet_found = false
490
+ doc.xpath("//*[local-name()='table']").each do |ws|
491
+ if sheet == attr(ws,'name')
492
+ sheet_found = true
493
+ col = 1
494
+ row = 1
495
+ ws.children.each do |table_element|
496
+ case table_element.name
497
+ when 'table-column'
498
+ @style_defaults[sheet] << table_element.attributes['default-cell-style-name']
499
+ when 'table-row'
500
+ if table_element.attributes['number-rows-repeated']
501
+ skip_row = attr(table_element,'number-rows-repeated').to_s.to_i
502
+ row = row + skip_row - 1
503
+ end
504
+ table_element.children.each do |cell|
505
+ skip_col = attr(cell, 'number-columns-repeated')
506
+ formula = attr(cell,'formula')
507
+ value_type = attr(cell,'value-type')
508
+ v = attr(cell,'value')
509
+ style_name = attr(cell,'style-name')
510
+ case value_type
511
+ when 'string'
512
+ str_v = ''
513
+ # insert \n if there is more than one paragraph
514
+ para_count = 0
515
+ cell.children.each do |str|
516
+ # begin comments
517
+ =begin
518
+ - <table:table-cell office:value-type="string">
519
+ - <office:annotation office:display="true" draw:style-name="gr1" draw:text-style-name="P1" svg:width="1.1413in" svg:height="0.3902in" svg:x="2.0142in" svg:y="0in" draw:caption-point-x="-0.2402in" draw:caption-point-y="0.5661in">
520
+ <dc:date>2011-09-20T00:00:00</dc:date>
521
+ <text:p text:style-name="P1">Kommentar fuer B4</text:p>
522
+ </office:annotation>
523
+ <text:p>B4 (mit Kommentar)</text:p>
524
+ </table:table-cell>
525
+ =end
526
+ if str.name == 'annotation'
527
+ str.children.each do |annotation|
528
+ if annotation.name == 'p'
529
+ # @comment ist ein Hash mit Sheet als Key (wie bei @cell)
530
+ # innerhalb eines Elements besteht ein Eintrag aus einem
531
+ # weiteren Hash mit Key [row,col] und dem eigentlichen
532
+ # Kommentartext als Inhalt
533
+ @comment[sheet] = Hash.new unless @comment[sheet]
534
+ key = [row,col]
535
+ @comment[sheet][key] = annotation.text
536
+ end
537
+ end
538
+ end
539
+ # end comments
540
+ if str.name == 'p'
541
+ v = str.content
542
+ str_v += "\n" if para_count > 0
543
+ para_count += 1
544
+ if str.children.size > 1
545
+ str_v += children_to_string(str.children)
546
+ else
547
+ str.children.each do |child|
548
+ str_v += child.content #.text
549
+ end
550
+ end
551
+ str_v.gsub!(/&apos;/,"'") # special case not supported by unescapeHTML
552
+ str_v = CGI.unescapeHTML(str_v)
553
+ end # == 'p'
554
+ end
555
+ when 'time'
556
+ cell.children.each do |str|
557
+ if str.name == 'p'
558
+ v = str.content
559
+ end
560
+ end
561
+ when '', nil, 'date', 'percentage', 'float'
562
+ #
563
+ when 'boolean'
564
+ v = attr(cell,'boolean-value').to_s
565
+ else
566
+ # raise "unknown type #{value_type}"
567
+ end
568
+ if skip_col
569
+ if v != nil or cell.attributes['date-value']
570
+ 0.upto(skip_col.to_i-1) do |i|
571
+ set_cell_values(sheet,col,row,i,v,value_type,formula,cell,str_v,style_name)
572
+ end
573
+ end
574
+ col += (skip_col.to_i - 1)
575
+ end # if skip
576
+ set_cell_values(sheet,col,row,0,v,value_type,formula,cell,str_v,style_name)
577
+ col += 1
578
+ end
579
+ row += 1
580
+ col = 1
581
+ end
582
+ end
583
+ end
584
+ end
585
+ doc.xpath("//*[local-name()='automatic-styles']").each do |style|
586
+ read_styles(style)
587
+ end
588
+ if !sheet_found
589
+ raise RangeError
590
+ end
591
+ @cells_read[sheet] = true
592
+ @comments_read[sheet] = true
593
+ end
594
+
595
+ # Only calls read_cells because Roo::Base calls read_comments
596
+ # whereas the reading of comments is done in read_cells for Roo::OpenOffice-objects
597
+ def read_comments(sheet=nil)
598
+ read_cells(sheet)
599
+ end
600
+
601
+ def read_labels
602
+ @label ||= Hash[doc.xpath("//table:named-range").map do |ne|
603
+ #-
604
+ # $Sheet1.$C$5
605
+ #+
606
+ name = attr(ne,'name').to_s
607
+ sheetname,coords = attr(ne,'cell-range-address').to_s.split('.$')
608
+ col, row = coords.split('$')
609
+ sheetname = sheetname[1..-1] if sheetname[0,1] == '$'
610
+ [name, [sheetname,row,col]]
611
+ end]
612
+ end
613
+
614
+ def read_styles(style_elements)
615
+ @font_style_definitions['Default'] = Roo::Font.new
616
+ style_elements.each do |style|
617
+ next unless style.name == 'style'
618
+ style_name = attr(style,'name')
619
+ style.each do |properties|
620
+ font = Roo::OpenOffice::Font.new
621
+ font.bold = attr(properties,'font-weight')
622
+ font.italic = attr(properties,'font-style')
623
+ font.underline = attr(properties,'text-underline-style')
624
+ @font_style_definitions[style_name] = font
625
+ end
626
+ end
627
+ end
628
+
629
+ def read_table_styles(styles)
630
+ styles.children.each do |style|
631
+ next unless style.name == 'style'
632
+ style_name = attr(style,'name')
633
+ style.children.each do |properties|
634
+ display = attr(properties,'display')
635
+ next unless display
636
+ @table_display[style_name] = (display == 'true')
637
+ end
638
+ end
639
+ end
640
+
641
+ A_ROO_TYPE = {
642
+ "float" => :float,
643
+ "string" => :string,
644
+ "date" => :date,
645
+ "percentage" => :percentage,
646
+ "time" => :time,
647
+ }
648
+
649
+ def self.oo_type_2_roo_type(ootype)
650
+ return A_ROO_TYPE[ootype]
651
+ end
652
+
653
+ # helper method to convert compressed spaces and other elements within
654
+ # an text into a string
655
+ def children_to_string(children)
656
+ result = ''
657
+ children.each {|child|
658
+ if child.text?
659
+ result = result + child.content
660
+ else
661
+ if child.name == 's'
662
+ compressed_spaces = child.attributes['c'].to_s.to_i
663
+ # no explicit number means a count of 1:
664
+ if compressed_spaces == 0
665
+ compressed_spaces = 1
666
+ end
667
+ result = result + " "*compressed_spaces
668
+ else
669
+ result = result + child.content
670
+ end
671
+ end
672
+ }
673
+ result
674
+ end
675
+
676
+ def attr(node, attr_name)
677
+ if node.attributes[attr_name]
678
+ node.attributes[attr_name].value
679
+ end
680
+ end
681
+ end