unxls 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2651 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Unxls do
4
+ it 'Has a version number' do
5
+ expect(Unxls::VERSION).to_not eq nil
6
+ end
7
+
8
+ it 'Only opens BIFF8 files for now' do
9
+ expect {
10
+ Unxls.parse(testfile('other', 'biff2-empty.xls'))
11
+ }.to raise_error 'Sorry, BIFF2 is not supported yet'
12
+
13
+ expect {
14
+ Unxls.parse(testfile('other', 'biff3-empty.xls'))
15
+ }.to raise_error 'Sorry, BIFF3 is not supported yet'
16
+
17
+ expect {
18
+ Unxls.parse(testfile('other', 'biff4-empty.xls'))
19
+ }.to raise_error 'Sorry, BIFF4 is not supported yet'
20
+
21
+ expect {
22
+ Unxls.parse(testfile('other', 'biff5-empty.xls'))
23
+ }.to raise_error 'Sorry, BIFF5 is not supported yet'
24
+ end
25
+
26
+ context 'Workbook stream, Globals substream' do
27
+ context 'Single records' do
28
+ before(:context) do
29
+ @browser1 = Unxls::Biff8::Browser.new(testfile('biff8', 'empty.xls'))
30
+ @browser2 = Unxls::Biff8::Browser.new(testfile('biff8', 'preferences.xls'))
31
+ end
32
+
33
+ it 'Decodes BOF record' do
34
+ expect(@browser1.globals[:BOF][:dt_d]).to eq :globals
35
+ end
36
+
37
+ it 'Decodes CalcPrecision record' do
38
+ expect(@browser1.globals[:CalcPrecision][:fFullPrec]).to eq false
39
+ expect(@browser2.globals[:CalcPrecision][:fFullPrec]).to eq true
40
+ end
41
+
42
+ it 'Decodes CodePage record' do
43
+ expect(@browser1.globals[:CodePage][:cv_d]).to eq :'UTF-16LE'
44
+ end
45
+
46
+ it 'Decodes Country record' do
47
+ c1 = @browser1.globals[:Country]
48
+ expect(c1[:iCountryDef]).to eq 1
49
+ expect(c1[:iCountryDef_d]).to eq :"United States"
50
+ expect(c1[:iCountryWinIni]).to eq 1
51
+ expect(c1[:iCountryWinIni_d]).to eq :"United States"
52
+
53
+ c2 = @browser2.globals[:Country]
54
+ expect(c2[:iCountryDef]).to eq 81
55
+ expect(c2[:iCountryDef_d]).to eq :Japan
56
+ expect(c2[:iCountryWinIni]).to eq 7
57
+ expect(c2[:iCountryWinIni_d]).to eq :Russia
58
+ end
59
+
60
+ it 'Decodes Date1904 record' do
61
+ expect(@browser1.globals[:Date1904][:f1904DateSystem]).to eq false
62
+ expect(@browser2.globals[:Date1904][:f1904DateSystem]).to eq true
63
+ end
64
+
65
+ it 'Decodes Theme record' do
66
+ expect(@browser1.globals[:Theme]).to eq nil
67
+
68
+ browser = Unxls::Biff8::Browser.new(testfile('biff8', 'font.xls'))
69
+ theme = browser.globals[:Theme]
70
+
71
+ expect(theme[:rgb_d].size).to eq 5
72
+ filename = 'theme/theme/theme1.xml'
73
+ expect(theme[:rgb_d].keys).to include filename
74
+
75
+ require 'rexml/document'
76
+ parsed_theme = REXML::Document.new(theme[:rgb_d][filename])
77
+ expect(parsed_theme.root.elements['a:themeElements/a:clrScheme[@name="Office"]'].size).to eq 12
78
+ expect(parsed_theme.root.elements['a:themeElements/a:clrScheme/a:dk1/a:sysClr/@lastClr'].value).to eq '000000'
79
+ expect(parsed_theme.root.elements['a:themeElements/a:fontScheme[@name="Office"]'].size).to eq 2
80
+ expect(parsed_theme.root.elements['a:themeElements/a:fmtScheme[@name="Office"]'].size).to eq 4
81
+ end
82
+ end
83
+
84
+ context 'BoundSheet8 record' do
85
+ before(:context) do
86
+ @browser1 = Unxls::Biff8::Browser.new(testfile('biff8', 'empty.xls'))
87
+ @browser2 = Unxls::Biff8::Browser.new(testfile('biff8', 'preferences.xls'))
88
+ end
89
+
90
+ specify 'For worksheet "Worksheet"' do
91
+ bs = @browser2.globals[:BoundSheet8][0]
92
+ expect(bs[:lbPlyPos]).to eq(@browser2.wbs[1][:BOF][:_record][:pos])
93
+ expect(bs[:hsState]).to eq 0
94
+ expect(bs[:hsState_d]).to eq :visible
95
+ expect(bs[:dt]).to eq 0
96
+ expect(bs[:dt_d]).to eq :dialog_or_work_sheet
97
+ expect(bs[:dt_d]).to eq(@browser2.wbs[1][:BOF][:dt_d])
98
+ expect(bs[:stName]).to eq 'Worksheet'
99
+ end
100
+
101
+ specify 'For dialog sheet "Dialog"' do
102
+ bs = @browser2.globals[:BoundSheet8][1]
103
+ expect(bs[:lbPlyPos]).to eq @browser2.wbs[2][:BOF][:_record][:pos]
104
+ expect(bs[:hsState]).to eq 0
105
+ expect(bs[:hsState_d]).to eq :visible
106
+ expect(bs[:dt]).to eq 0
107
+ expect(bs[:dt_d]).to eq :dialog_or_work_sheet
108
+ expect(bs[:dt_d]).to eq @browser2.wbs[2][:BOF][:dt_d]
109
+ expect(@browser2.wbs[2][:WsBool][:fDialog]).to eq true
110
+ expect(bs[:stName]).to eq 'Dialog'
111
+ end
112
+
113
+ # https://superuser.com/questions/1253212/what-is-macro-worksheet-in-excel
114
+ specify 'For macro sheet "Excel 4 macro"' do
115
+ bs = @browser2.globals[:BoundSheet8][2]
116
+ expect(bs[:lbPlyPos]).to eq @browser2.wbs[3][:BOF][:_record][:pos]
117
+ expect(bs[:hsState]).to eq 0
118
+ expect(bs[:hsState_d]).to eq :visible
119
+ expect(bs[:dt]).to eq 1
120
+ expect(bs[:dt_d]).to eq :macro
121
+ expect(bs[:dt_d]).to eq @browser2.wbs[3][:BOF][:dt_d]
122
+ expect(bs[:stName]).to eq 'Excel 4 macro'
123
+ end
124
+
125
+ specify 'For macro sheet "International macro"' do
126
+ bs = @browser2.globals[:BoundSheet8][3]
127
+ expect(bs[:lbPlyPos]).to eq @browser2.wbs[4][:BOF][:_record][:pos]
128
+ expect(bs[:hsState]).to eq 0
129
+ expect(bs[:hsState_d]).to eq :visible
130
+ expect(bs[:dt]).to eq 1
131
+ expect(bs[:dt_d]).to eq :macro
132
+ expect(bs[:dt_d]).to eq @browser2.wbs[4][:BOF][:dt_d]
133
+ expect(bs[:stName]).to eq 'International macro'
134
+ end
135
+
136
+ specify 'For chart sheet "Chart"' do
137
+ bs = @browser2.globals[:BoundSheet8][4]
138
+ expect(bs[:lbPlyPos]).to eq @browser2.wbs[5][:BOF][:_record][:pos]
139
+ expect(bs[:hsState]).to eq 0
140
+ expect(bs[:hsState_d]).to eq :visible
141
+ expect(bs[:dt]).to eq 2
142
+ expect(bs[:dt_d]).to eq :chart
143
+ expect(bs[:dt_d]).to eq @browser2.wbs[5][:BOF][:dt_d]
144
+ expect(bs[:stName]).to eq 'Chart'
145
+ end
146
+
147
+ specify 'For a hidden worksheet' do
148
+ bs = @browser2.globals[:BoundSheet8][5]
149
+ expect(bs[:hsState_d]).to eq :hidden
150
+ expect(bs[:stName]).to eq 'Hidden'
151
+ end
152
+
153
+ specify 'For a very hidden worksheet' do
154
+ bs = @browser2.globals[:BoundSheet8][6]
155
+ expect(bs[:hsState_d]).to eq :very_hidden
156
+ expect(bs[:stName]).to eq 'Very hidden'
157
+ end
158
+ end
159
+
160
+ context 'DXF record' do
161
+ before(:context) do
162
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'tablestyle.xls'))
163
+ end
164
+
165
+ let(:dxf) { @browser.get_tse_dxf('Custom Table Style', :whole_table) }
166
+
167
+ it 'Decodes basic fields' do
168
+ expect(dxf[:frtHeader]).to be
169
+ expect(dxf[:fNewBorder]).to eq true
170
+ expect(dxf[:cprops]).to eq 14
171
+ end
172
+
173
+ it 'Decodes structure of xfPropType 0x00 (FillPattern)' do
174
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0 }
175
+ expect(prop[:cb]).to eq 5
176
+ expect(prop[:_description]).to eq :fill_pattern
177
+ expect(prop[:_structure]).to eq :FillPattern
178
+ expect(prop[:xfPropDataBlob_d]).to eq :FLSGRAY0625
179
+ end
180
+
181
+ # Fully tested in StyleExt
182
+ it 'Decodes structures already tested in StyleExt' do
183
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 1 } # foreground_color
184
+ expect(prop[:xfPropDataBlob_d][:dwRgba]).to eq :'2A8EF2FF'
185
+
186
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 2 } # background_color
187
+ expect(prop[:xfPropDataBlob_d][:dwRgba]).to eq :'7030A0FF'
188
+
189
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 5 } # text_color
190
+ expect(prop[:xfPropDataBlob_d][:dwRgba]).to eq :'2A8EF2FF'
191
+
192
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 6 } # top_border
193
+ expect(prop[:xfPropDataBlob_d][:dgBorder_d]).to eq :DOTTED
194
+
195
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 7 } # bottom_border
196
+ expect(prop[:xfPropDataBlob_d][:dgBorder_d]).to eq :DASHDOT
197
+
198
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 8 } # left_border
199
+ expect(prop[:xfPropDataBlob_d][:dgBorder_d]).to eq :HAIR
200
+
201
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 9 } # right_border
202
+ expect(prop[:xfPropDataBlob_d][:dgBorder_d]).to eq :DASHDOTDOT
203
+ end
204
+
205
+ it 'Decodes structure of xfPropType 0x19 (Bold, font weight)' do
206
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0x19 }
207
+ expect(prop[:cb]).to eq 6
208
+ expect(prop[:_description]).to eq :font_weight
209
+ expect(prop[:_structure]).to eq :Bold
210
+ expect(prop[:xfPropDataBlob_d]).to eq :BLSBOLD
211
+ end
212
+
213
+ it 'Decodes structure of xfPropType 0x1A (Underline, underline style)' do
214
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0x1A }
215
+ expect(prop[:cb]).to eq 6
216
+ expect(prop[:_description]).to eq :underline_style
217
+ expect(prop[:_structure]).to eq :Underline
218
+ expect(prop[:xfPropDataBlob_d]).to eq :ULSSINGLE
219
+ end
220
+
221
+ it 'Decodes structure of xfPropType 0x1C (_bool1b, text is italicized)' do
222
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0x1C }
223
+ expect(prop[:cb]).to eq 5
224
+ expect(prop[:_description]).to eq :italic
225
+ expect(prop[:_structure]).to eq :_bool1b
226
+ expect(prop[:xfPropDataBlob_d]).to eq true
227
+ end
228
+
229
+ it 'Decodes structure of xfPropType 0x1D (_bool1b, text has strikethrough formatting)' do
230
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0x1D }
231
+ expect(prop[:cb]).to eq 5
232
+ expect(prop[:_description]).to eq :strikethrough
233
+ expect(prop[:_structure]).to eq :_bool1b
234
+ expect(prop[:xfPropDataBlob_d]).to eq true
235
+ end
236
+
237
+ it 'Decodes structure of xfPropType 0x0B (XFPropBorder, vertical border formatting)' do
238
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0x0B }
239
+ expect(prop[:cb]).to eq 14
240
+ expect(prop[:_description]).to eq :vertical_border
241
+ expect(prop[:_structure]).to eq :XFPropBorder
242
+
243
+ prop_data = prop[:xfPropDataBlob_d]
244
+ expect(prop_data[:dgBorder]).to eq 1
245
+ expect(prop_data[:dgBorder_d]).to eq :THIN
246
+
247
+ color = prop_data[:color]
248
+ expect(color[:fValidRGBA]).to eq true
249
+ expect(color[:xclrType]).to eq 2
250
+ expect(color[:xclrType_d]).to eq :XCLRRGB
251
+ expect(color[:icv]).to eq 0xFF
252
+ expect(color[:nTintShade]).to eq 0
253
+ expect(color[:nTintShade_d]).to eq 0.0
254
+ expect(color[:dwRgba]).to eq :'2A8EF2FF'
255
+ end
256
+
257
+ it 'Decodes structure of xfPropType 0x0C (XFPropBorder, horizontal border formatting)' do
258
+ prop = dxf[:xfPropArray].find { |e| e[:xfPropType] == 0x0C }
259
+ expect(prop[:cb]).to eq 14
260
+ expect(prop[:_description]).to eq :horizontal_border
261
+ expect(prop[:_structure]).to eq :XFPropBorder
262
+
263
+ prop_data = prop[:xfPropDataBlob_d]
264
+ expect(prop_data[:dgBorder]).to eq 3
265
+ expect(prop_data[:dgBorder_d]).to eq :DASHED
266
+
267
+ color = prop_data[:color]
268
+ expect(color[:fValidRGBA]).to eq true
269
+ expect(color[:xclrType]).to eq 2
270
+ expect(color[:xclrType_d]).to eq :XCLRRGB
271
+ expect(color[:icv]).to eq 0xFF
272
+ expect(color[:nTintShade]).to eq 0
273
+ expect(color[:nTintShade_d]).to eq 0.0
274
+ expect(color[:dwRgba]).to eq :'92D050FF'
275
+ end
276
+ end
277
+
278
+ context 'FilePass record' do
279
+ it 'Decodes from files encrypted using XOR obfuscation' do
280
+ file = testfile('biff8', 'filepass', 'filepass_xor-password.xls')
281
+ browser = Unxls::Biff8::Browser.new(file, password: 'password')
282
+
283
+ fp = browser.globals[:FilePass]
284
+ expect(fp[:wEncryptionType]).to eq 0
285
+ expect(fp[:_type]).to eq :XOR
286
+ expect(fp[:key]).to eq 5242
287
+ expect(fp[:verificationBytes]).to eq 33711
288
+
289
+ expect(browser.globals[:Font][0][:fontName]).to eq 'Arial'
290
+ end
291
+
292
+ it 'Decodes from files encrypted using RC4 encryption, RC4 header structure' do
293
+ file = testfile('biff8', 'filepass', 'filepass_rc4_97_2000-password.xls')
294
+ browser = Unxls::Biff8::Browser.new(file, password: 'password')
295
+
296
+ fp = browser.globals[:FilePass]
297
+ expect(fp[:wEncryptionType]).to eq 1
298
+ expect(fp[:_type]).to eq :RC4
299
+ expect(fp[:EncryptionVersionInfo]).to eq({ vMajor: 1, vMinor: 1 })
300
+ %i(Salt EncryptedVerifier EncryptedVerifierHash).each do |attr|
301
+ expect(fp[attr]).to be_an_instance_of String
302
+ expect(fp[attr].size).to eq 16
303
+ expect(fp[attr].encoding.name).to eq 'ASCII-8BIT'
304
+ end
305
+
306
+ expect(browser.globals[:Font][0][:fontName]).to eq 'Arial'
307
+ end
308
+
309
+ [
310
+ { file: 'filepass_cryptoapi_40_basecrypto-password.xls', CSPName: 'Microsoft Base Cryptographic Provider v1.0', KeySize: 40 },
311
+ { file: 'filepass_cryptoapi_40_dhsc-password.xls', CSPName: 'Microsoft DH SChannel Cryptographic Provider', KeySize: 40 },
312
+ { file: 'filepass_cryptoapi_40_dss-password.xls', CSPName: 'Microsoft Base DSS and Diffie-Hellman Cryptographic Provider', KeySize: 40 },
313
+ { file: 'filepass_cryptoapi_128_enhanced-password.xls', CSPName: 'Microsoft Enhanced Cryptographic Provider v1.0', KeySize: 128 },
314
+ { file: 'filepass_cryptoapi_128_enhdss-password.xls', CSPName: 'Microsoft Enhanced DSS and Diffie-Hellman Cryptographic Provider', KeySize: 128 },
315
+ { file: 'filepass_cryptoapi_128_enhrsa-password.xls', CSPName: 'Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)', KeySize: 128 },
316
+ { file: 'filepass_cryptoapi_128_enhrsasc-password.xls', CSPName: 'Microsoft RSA SChannel Cryptographic Provider', KeySize: 128 },
317
+ { file: 'filepass_cryptoapi_128_strong-password.xls', CSPName: 'Microsoft Strong Cryptographic Provider', KeySize: 128 },
318
+ ].each do |params|
319
+ it "Decodes from #{params[:file]} encrypted using RC4 encryption, RC4 CryptoAPI header structure" do
320
+ file = testfile('biff8', 'filepass', params[:file])
321
+ browser = Unxls::Biff8::Browser.new(file, password: 'password')
322
+ fp = browser.globals[:FilePass]
323
+ expect(fp[:wEncryptionType]).to eq 1
324
+ expect(fp[:_type]).to eq :CryptoAPI
325
+ expect(fp[:EncryptionVersionInfo]).to eq({ vMajor: 2, vMinor: 2 })
326
+ flags = { fCryptoAPI: true, fDocProps: false, fExternal: false, fAES: false }
327
+ expect(fp[:EncryptionHeaderFlags]).to eq flags
328
+ expect(fp[:EncryptionHeaderSize]).to be > 0
329
+
330
+ eh = fp[:EncryptionHeader]
331
+ expect(eh[:Flags]).to eq flags
332
+ expect(eh[:SizeExtra]).to eq 0
333
+ expect(eh[:AlgID]).to eq 0x6801
334
+ expect(eh[:AlgID_d]).to eq :RC4
335
+ expect(eh[:KeySize]).to eq params[:KeySize]
336
+ expect(eh[:ProviderType]).to be_an_kind_of Integer
337
+ expect(eh[:CSPName]).to eq params[:CSPName]
338
+
339
+ ev = fp[:EncryptionVerifier]
340
+ %i(Salt EncryptedVerifier).each do |attr|
341
+ expect(ev[attr]).to be_an_instance_of String
342
+ expect(ev[attr].size).to eq ev[:SaltSize]
343
+ expect(ev[attr].encoding.name).to eq 'ASCII-8BIT'
344
+ end
345
+ expect(ev[:VerifierHashSize]).to eq 20
346
+ expect(ev[:EncryptedVerifierHash].size).to eq ev[:VerifierHashSize]
347
+ expect(ev[:EncryptedVerifierHash]).to be_an_instance_of String
348
+ expect(ev[:EncryptedVerifierHash].encoding.name).to eq 'ASCII-8BIT'
349
+
350
+ expect(fp[:_encryption_algorithm]).to eq :RC4
351
+ expect(fp[:_hashing_algorithm]).to eq :SHA1
352
+
353
+ expect(browser.globals[:Font][0][:fontName]).to eq 'Arial'
354
+ end
355
+ end
356
+ end
357
+
358
+ context 'Font record' do
359
+ before(:context) do
360
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'font.xls'))
361
+ end
362
+
363
+ def get_font(stream, row, col)
364
+ font_index = @browser.get_xf(stream, row, col)[:ifnt]
365
+ @browser.get_font(font_index)
366
+ end
367
+
368
+ # Size
369
+ it 'Decodes dyHeight field' do
370
+ expect(get_font(1, 2, 0)[:dyHeight]).to eq 200
371
+ expect(get_font(1, 3, 0)[:dyHeight]).to eq 240
372
+ expect(get_font(1, 4, 0)[:dyHeight]).to eq 300
373
+ end
374
+
375
+ it 'Decodes fItalic field' do
376
+ expect(get_font(1, 2, 1)[:fItalic]).to eq true
377
+ end
378
+
379
+ it 'Decodes fStrikeOut field' do
380
+ expect(get_font(1, 2, 2)[:fStrikeOut]).to eq true
381
+ end
382
+
383
+ # Seems like fOutline, fShadow are only used in old (~2) versions of Excel for Mac
384
+ # Seems like fCondense, fExtend came from Word but can't be found in the interface of Excel
385
+
386
+ # Color
387
+ it 'Decodes icv field' do
388
+ expect(get_font(1, 2, 3)[:icv]).to eq 0x7FFF # Font automatic color
389
+ expect(get_font(1, 3, 3)[:icv]).to eq 0x000A # rgColor[2] of Palette, FF0000
390
+ end
391
+
392
+ # Bold
393
+ it 'Decodes bls field' do
394
+ f = get_font(1, 2, 4)
395
+ expect(f[:bls]).to eq 400
396
+ expect(f[:bls_d]).to eq :BLSNORMAL
397
+
398
+ f = get_font(1, 3, 4)
399
+ expect(f[:bls]).to eq 700
400
+ expect(f[:bls_d]).to eq :BLSBOLD
401
+ end
402
+
403
+ # Superscript, subscript
404
+ it 'Decodes sss field' do
405
+ f = get_font(1, 2, 5)
406
+ expect(f[:sss]).to eq 0
407
+ expect(f[:sss_d]).to eq :SSSNONE
408
+
409
+ f = get_font(1, 3, 5)
410
+ expect(f[:sss]).to eq 1
411
+ expect(f[:sss_d]).to eq :SSSSUPER
412
+
413
+ f = get_font(1, 4, 5)
414
+ expect(f[:sss]).to eq 2
415
+ expect(f[:sss_d]).to eq :SSSSUB
416
+ end
417
+
418
+ # Underline
419
+ it 'Decodes uls field' do
420
+ f = get_font(1, 2, 6)
421
+ expect(f[:uls]).to eq 0
422
+ expect(f[:uls_d]).to eq :ULSNONE
423
+
424
+ f = get_font(1, 3, 6)
425
+ expect(f[:uls]).to eq 1
426
+ expect(f[:uls_d]).to eq :ULSSINGLE
427
+
428
+ f = get_font(1, 4, 6)
429
+ expect(f[:uls]).to eq 2
430
+ expect(f[:uls_d]).to eq :ULSDOUBLE
431
+
432
+ f = get_font(1, 5, 6)
433
+ expect(f[:uls]).to eq 33
434
+ expect(f[:uls_d]).to eq :ULSSINGLEACCOUNTANT
435
+
436
+ f = get_font(1, 6, 6)
437
+ expect(f[:uls]).to eq 34
438
+ expect(f[:uls_d]).to eq :ULSDOUBLEACCOUNTANT
439
+ end
440
+
441
+ # Font family (Mac versions seem to not to write this field)
442
+ it 'Decodes bFamily field' do
443
+ f = get_font(1, 2, 7)
444
+ expect(f[:bFamily]).to eq 0
445
+ expect(f[:bFamily_d]).to eq :'Not applicable'
446
+
447
+ f = get_font(1, 3, 7)
448
+ expect(f[:bFamily]).to eq 1
449
+ expect(f[:bFamily_d]).to eq :Roman
450
+
451
+ f = get_font(1, 4, 7)
452
+ expect(f[:bFamily]).to eq 2
453
+ expect(f[:bFamily_d]).to eq :Swiss
454
+
455
+ f = get_font(1, 5, 7)
456
+ expect(f[:bFamily]).to eq 3
457
+ expect(f[:bFamily_d]).to eq :Modern
458
+
459
+ f = get_font(1, 6, 7)
460
+ expect(f[:bFamily]).to eq 4
461
+ expect(f[:bFamily_d]).to eq :Script
462
+ end
463
+
464
+ # Font charset (Mac versions seem to not to write this field)
465
+ it 'Decodes bCharSet field' do
466
+ f = get_font(1, 2, 8)
467
+ expect(f[:bCharSet]).to eq 128
468
+ expect(f[:bCharSet_d]).to eq :ShiftJIS
469
+
470
+ f = get_font(1, 3, 8)
471
+ expect(f[:bCharSet]).to eq 129
472
+ expect(f[:bCharSet_d]).to eq :Jangul
473
+
474
+ f = get_font(1, 4, 8)
475
+ expect(f[:bCharSet]).to eq 136
476
+ expect(f[:bCharSet_d]).to eq :ChineseBIG5
477
+
478
+ f = get_font(1, 5, 8)
479
+ expect(f[:bCharSet]).to eq 134
480
+ expect(f[:bCharSet_d]).to eq :GB2312
481
+
482
+ f = get_font(1, 6, 8)
483
+ expect(f[:bCharSet]).to eq 0
484
+ expect(f[:bCharSet_d]).to eq :ANSI
485
+ end
486
+
487
+ it 'Decodes fontName field' do
488
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'font-fontname.xls'))
489
+
490
+ expect(get_font(1, 2, 0)[:fontName]).to eq 'HGP創英角ゴシックUB'
491
+ expect(get_font(1, 3, 0)[:fontName]).to eq 'メイリオ'
492
+ expect(get_font(1, 4, 0)[:fontName]).to eq '나눔고딕 ExtraBold'
493
+ expect(get_font(1, 5, 0)[:fontName]).to eq 'مِصحفي'
494
+ expect(get_font(1, 6, 0)[:fontName]).to eq 'Цветные эмодзи Apple'
495
+ expect(get_font(1, 7, 0)[:fontName]).to eq 'Arial'
496
+ end
497
+ end
498
+
499
+ context 'Format record' do
500
+ before(:context) do
501
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'format.xls'))
502
+ end
503
+
504
+ # Record id
505
+ it 'Decodes ifmt field' do
506
+ 1.upto(5).each do |row|
507
+ expect(@browser.get_format(1, row, 0)[:ifmt]).to be_an_kind_of Integer
508
+ end
509
+ end
510
+
511
+ # Format string
512
+ it 'Decodes stFormat field' do
513
+ expect(@browser.get_format(1, 1, 0)[:stFormat]).to eq '"Format フォーマット"'
514
+ expect(@browser.get_format(1, 2, 0)[:stFormat]).to eq '[$-409]d\-mmm\-yy;@'
515
+ expect(@browser.get_format(1, 3, 0)[:stFormat]).to eq 'm"月"d"日";@'
516
+ expect(@browser.get_format(1, 4, 0)[:stFormat]).to eq 'dd/mm/yy;@'
517
+ expect(@browser.get_format(1, 5, 0)[:stFormat]).to eq '_-[$₩-412]* #,##0.00_-;\-[$₩-412]* #,##0.00_-;_-[$₩-412]* "-"??_-;_-@_-'
518
+ end
519
+ end
520
+
521
+ context 'Palette record' do
522
+ before(:context) do
523
+ @browser1 = Unxls::Biff8::Browser.new(testfile('biff8', 'empty.xls'))
524
+ @browser2 = Unxls::Biff8::Browser.new(testfile('biff8', 'palette.xls'))
525
+ end
526
+
527
+ it 'Decodes' do
528
+ expect(@browser1.globals[:Palette]).to eq nil # Uses standard palette
529
+
530
+ p = @browser2.globals[:Palette]
531
+ expect(p[:ccv]).to eq 56
532
+ expect(p[:rgColor].size).to eq p[:ccv]
533
+ expect(p[:rgColor][0]).to eq :"2A8EF200" # Black changed to RGB 42, 142, 242
534
+ expect(p[:rgColor][1]).to eq :"F28E2A00" # White changed to RGB 242, 142, 142
535
+ expect(p[:rgColor][2]).to eq :"8E2AF200" # Red changed to RGB 142, 42, 242
536
+ expect(p[:rgColor][3]).to eq :"00FF0000" # Green unchanged
537
+ end
538
+ end
539
+
540
+ context 'SST record' do
541
+ before(:context) do
542
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'sst.xls'))
543
+ end
544
+
545
+ it 'Decodes cstTotal field' do
546
+ expect(@browser.globals[:SST][:cstTotal]).to eq 24
547
+ end
548
+
549
+ it 'Decodes cstUnique field' do
550
+ expect(@browser.globals[:SST][:cstUnique]).to eq 20
551
+ end
552
+
553
+ describe 'String-related fields' do
554
+ specify 'For short ASCII string' do
555
+ sst = @browser.get_sst(1, 3, 0)
556
+ expect(sst[:fHighByte]).to eq false
557
+ expect(sst[:fRichSt]).to eq false
558
+ expect(sst[:cch]).to eq 35
559
+ expect(sst[:rgb]).to eq 'String with single-byte characters.'
560
+ expect(sst[:rgb].size).to eq sst[:cch]
561
+ end
562
+
563
+ specify 'For short string with single and double-byte characters' do
564
+ sst = @browser.get_sst(1, 4, 0)
565
+ expect(sst[:fHighByte]).to eq true
566
+ expect(sst[:fRichSt]).to eq false
567
+ expect(sst[:cch]).to eq 49
568
+ expect(sst[:rgb]).to eq 'String with double-byte characters: АБВГД, アイウエオ。'
569
+ expect(sst[:rgb].size).to eq sst[:cch]
570
+ end
571
+
572
+ specify 'For very long string with surrogate pair characters' do
573
+ sst = @browser.get_sst(1, 7, 0)
574
+ expect(sst[:fHighByte]).to eq true
575
+ expect(sst[:fRichSt]).to eq false
576
+ expect(sst[:rgb].size).to eq 17000
577
+ expect(sst[:cch] - sst[:rgb].size).to eq 5 # 1 surrogate pair character (emojis, etc.) is counted as 2
578
+ expect(sst[:rgb][0..9]).to eq 'Very long '
579
+ expect(sst[:rgb][-9..-1]).to eq ' W W END.'
580
+ end
581
+ end
582
+
583
+ describe 'Formatting runs-related fields' do
584
+ let(:sst) do
585
+ @browser.get_sst(1, 3, 1)
586
+ end
587
+
588
+ it 'Decodes basic fields' do
589
+ expect(sst[:fRichSt]).to eq true
590
+ expect(sst[:cRun]).to eq 10
591
+ expect(sst[:rgb][0..10]).to eq 'Lorem ipsum'
592
+ end
593
+
594
+ it 'Decodes first run ("ipsum")' do
595
+ expect(sst[:rgRun][0][:ich]).to eq 6 # Run's start character position
596
+ expect(sst[:rgRun][1][:ich]).to eq 11 # Run's end character position + 1
597
+ frun_ifnt = sst[:rgRun][0][:ifnt] - 1 # See 2.5.129 FontIndex
598
+ font = @browser.globals[:Font][frun_ifnt]
599
+ expect(font[:fontName]).to eq 'Meiryo'
600
+ expect(sst[:rgRun][1][:ifnt]).to eq 0
601
+ end
602
+
603
+ it 'Decodes second run ("amet")' do
604
+ expect(sst[:rgRun][2][:ich]).to eq 22
605
+ expect(sst[:rgRun][3][:ich]).to eq 26
606
+ frun_ifnt = sst[:rgRun][2][:ifnt] - 1
607
+ font = @browser.globals[:Font][frun_ifnt]
608
+ expect(font[:fontName]).to eq 'Osaka'
609
+ expect(sst[:rgRun][3][:ifnt]).to eq 0
610
+ end
611
+
612
+ it 'Decodes third run ("adipiscing")' do
613
+ expect(sst[:rgRun][4][:ich]).to eq 40
614
+ expect(sst[:rgRun][5][:ich]).to eq 50
615
+ frun_ifnt = sst[:rgRun][4][:ifnt] - 1
616
+ font = @browser.globals[:Font][frun_ifnt]
617
+ expect(font[:fontName]).to eq 'FangSong'
618
+ expect(sst[:rgRun][5][:ifnt]).to eq 0
619
+ end
620
+
621
+ # etc
622
+ end
623
+
624
+ context 'Phonetic text runs-related fields' do
625
+ it 'Decodes basic fields' do
626
+ sst = @browser.get_sst(1, 3, 2)
627
+ expect(sst[:fExtSt]).to eq true
628
+ expect(sst[:cbExtRst]).to eq 92
629
+ expect(sst[:rgb]).to eq '世界選手権でメダルを取った選手が池江選手にメッセージ'
630
+ expect(sst[:ExtRst][:cb]).to eq 88
631
+ end
632
+
633
+ it 'Decodes rphssub field' do
634
+ rphssub = @browser.get_sst(1, 3, 2)[:ExtRst][:rphssub]
635
+ expect(rphssub[:crun]).to eq 6
636
+ expect(rphssub[:cch]).to eq 21
637
+ expect(rphssub[:cch]).to eq rphssub[:st].size
638
+ expect(rphssub[:st]).to eq 'セカイセンシュケントセンシュイケエセンシュ'
639
+ end
640
+
641
+ it 'Decodes rgphruns field' do
642
+ rgphruns = @browser.get_sst(1, 3, 2)[:ExtRst][:rgphruns]
643
+
644
+ # First run: "セカイ" (0-) over "世界" (0-1)
645
+ expect(rgphruns[0][:ichFirst]).to eq 0 # furigana (rphssub.st) start char
646
+ expect(rgphruns[0][:ichMom]).to eq 0 # text (SST.rgb[].rgb) start char
647
+ expect(rgphruns[0][:cchMom]).to eq 2 # text char count
648
+
649
+ # Second run: "センシュケン" (3-) over "選手権" (2-4)
650
+ expect(rgphruns[1][:ichFirst]).to eq 3
651
+ expect(rgphruns[1][:ichMom]).to eq 2
652
+ expect(rgphruns[1][:cchMom]).to eq 3
653
+
654
+ # Third run: "ト" (9-) over "取" (10)
655
+ expect(rgphruns[2][:ichFirst]).to eq 9
656
+ expect(rgphruns[2][:ichMom]).to eq 10
657
+ expect(rgphruns[2][:cchMom]).to eq 1
658
+
659
+ # 4th run: "センシュ" (10-) over "選手" (14-15)
660
+ expect(rgphruns[3][:ichFirst]).to eq 10
661
+ expect(rgphruns[3][:ichMom]).to eq 13
662
+ expect(rgphruns[3][:cchMom]).to eq 2
663
+
664
+ # etc
665
+ end
666
+
667
+ it 'Decodes phs field' do
668
+ phs = @browser.get_sst(1, 3, 2)[:ExtRst][:phs]
669
+ expect(phs[:phType]).to eq 0
670
+ expect(phs[:phType_d]).to eq :narrow_katakana
671
+
672
+ phs = @browser.get_sst(1, 4, 2)[:ExtRst][:phs]
673
+ expect(phs[:phType]).to eq 1
674
+ expect(phs[:phType_d]).to eq :wide_katakana
675
+
676
+ phs = @browser.get_sst(1, 5, 2)[:ExtRst][:phs]
677
+ expect(phs[:phType]).to eq 2
678
+ expect(phs[:phType_d]).to eq :hiragana
679
+
680
+ phs = @browser.get_sst(1, 3, 3)[:ExtRst][:phs]
681
+ expect(phs[:alcH]).to eq 0
682
+ expect(phs[:alcH_d]).to eq :general
683
+
684
+ phs = @browser.get_sst(1, 4, 3)[:ExtRst][:phs]
685
+ expect(phs[:alcH]).to eq 1
686
+ expect(phs[:alcH_d]).to eq :left
687
+
688
+ phs = @browser.get_sst(1, 5, 3)[:ExtRst][:phs]
689
+ expect(phs[:alcH]).to eq 2
690
+ expect(phs[:alcH_d]).to eq :center
691
+
692
+ phs = @browser.get_sst(1, 6, 3)[:ExtRst][:phs]
693
+ expect(phs[:alcH]).to eq 3
694
+ expect(phs[:alcH_d]).to eq :distributed
695
+
696
+ phs = @browser.get_sst(1, 3, 4)[:ExtRst][:phs]
697
+ phs_ifnt = phs[:ifnt] - 1
698
+ font = @browser.globals[:Font][phs_ifnt]
699
+ expect(font[:fontName]).to eq 'HGPMinchoE'
700
+
701
+ phs = @browser.get_sst(1, 4, 4)[:ExtRst][:phs]
702
+ phs_ifnt = phs[:ifnt] - 1
703
+ font = @browser.globals[:Font][phs_ifnt]
704
+ expect(font[:fontName]).to eq 'Hiragino Kaku Gothic StdN W8'
705
+ end
706
+ end
707
+ end
708
+
709
+ context 'Style record' do
710
+ before(:context) do
711
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'style-styleext.xls'))
712
+ end
713
+
714
+ it 'Decodes fields for built-in styles' do
715
+ s = @browser.get_style(1, 2, 0)
716
+ expect(s[:ixfe]).to eq 0
717
+ expect(s[:fBuiltIn]).to eq true
718
+ expect(s[:builtInData][:istyBuiltIn]).to eq 0
719
+ expect(s[:builtInData][:iLevel]).to eq 0xFF
720
+ expect(s[:builtInData][:istyBuiltIn_d]).to eq :Normal
721
+
722
+ s = @browser.get_style(1, 3, 0)
723
+ expect(s[:ixfe]).to eq 43
724
+ expect(s[:fBuiltIn]).to eq true
725
+ expect(s[:builtInData][:istyBuiltIn]).to eq 3
726
+ expect(s[:builtInData][:iLevel]).to eq 0xFF
727
+ expect(s[:builtInData][:istyBuiltIn_d]).to eq :Comma
728
+ end
729
+
730
+ it 'Decodes fields for basic styles written by Excel' do
731
+ s = @browser.get_style(1, 2, 1)
732
+ expect(s[:ixfe]).to eq 40
733
+ expect(s[:fBuiltIn]).to eq false
734
+ expect(s[:user]).to eq :Bad
735
+
736
+ s = @browser.get_style(1, 3, 1)
737
+ expect(s[:ixfe]).to eq 41
738
+ expect(s[:fBuiltIn]).to eq false
739
+ expect(s[:user]).to eq :Calculation
740
+ end
741
+
742
+ it 'Decodes custom styles' do
743
+ s = @browser.get_style(1, 4, 2)
744
+ expect(s[:ixfe]).to eq 47
745
+ expect(s[:fBuiltIn]).to eq false
746
+ expect(s[:user]).to eq :'Custom 1'
747
+ end
748
+ end
749
+
750
+ context 'StyleExt record' do
751
+ before(:context) do
752
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'style-styleext.xls'))
753
+ end
754
+
755
+ it 'Decodes basic fields' do
756
+ s = @browser.get_styleext(1, 2, 2) # Modified built-in style
757
+ expect(s[:fBuiltIn]).to eq true
758
+ expect(s[:fHidden]).to eq false
759
+ expect(s[:fCustom]).to eq true
760
+ expect(s[:iCategory]).to eq 5
761
+ expect(s[:iCategory_d]).to eq :'Number format style'
762
+ expect(s[:builtInData]).to eq @browser.get_style(1, 2, 2)[:builtInData]
763
+ expect(s[:stName]).to eq :Currency
764
+ expect(s[:cprops]).to eq 3
765
+
766
+ s = @browser.get_styleext(1, 3, 2) # Modified basic style
767
+ expect(s[:fBuiltIn]).to eq true # fBuiltIn in corresponding Style is false
768
+ expect(s[:fHidden]).to eq false
769
+ expect(s[:fCustom]).to eq true
770
+ expect(s[:iCategory]).to eq 3
771
+ expect(s[:iCategory_d]).to eq :'Title and heading style'
772
+ expect(s[:stName]).to eq :Total
773
+ expect(s[:cprops]).to eq 3
774
+
775
+ s = @browser.get_styleext(1, 4, 2) # Custom style
776
+ expect(s[:fBuiltIn]).to eq false
777
+ expect(s[:fHidden]).to eq false
778
+ expect(s[:fCustom]).to eq false
779
+ expect(s[:iCategory]).to eq 0
780
+ expect(s[:iCategory_d]).to eq :'Custom style'
781
+ expect(s[:stName]).to eq :'Custom 1'
782
+ expect(s[:cprops]).to eq 10
783
+ end
784
+
785
+ context 'Decodes xfPropArray field' do
786
+ let(:custom1) { @browser.get_styleext(1, 4, 2)[:xfPropArray] }
787
+ let(:custom2) { @browser.get_styleext(1, 5, 2)[:xfPropArray] }
788
+
789
+ it 'Decodes structure of xfPropType 0x01 (XFPropColor, foreground color)' do
790
+ prop = custom1.find { |e| e[:xfPropType] == 1 }
791
+ expect(prop[:cb]).to eq 12
792
+ expect(prop[:_description]).to eq :foreground_color
793
+ expect(prop[:_structure]).to eq :XFPropColor
794
+
795
+ prop_data = prop[:xfPropDataBlob_d]
796
+ expect(prop_data[:fValidRGBA]).to eq true
797
+ expect(prop_data[:xclrType]).to eq 3
798
+ expect(prop_data[:xclrType_d]).to eq :XCLRTHEMED
799
+ expect(prop_data[:icv]).to eq 5
800
+ expect(prop_data[:nTintShade]).to eq 0
801
+ expect(prop_data[:nTintShade_d]).to eq 0.0
802
+ expect(prop_data[:dwRgba]).to eq :ED7D31FF
803
+ end
804
+
805
+ it 'Decodes structure of xfPropType 0x02 (XFPropColor, background color)' do
806
+ prop = custom1.find { |e| e[:xfPropType] == 2 }
807
+ expect(prop[:cb]).to eq 12
808
+ expect(prop[:_description]).to eq :background_color
809
+ expect(prop[:_structure]).to eq :XFPropColor
810
+
811
+ prop_data = prop[:xfPropDataBlob_d]
812
+ expect(prop_data[:fValidRGBA]).to eq true
813
+ expect(prop_data[:xclrType]).to eq 2
814
+ expect(prop_data[:xclrType_d]).to eq :XCLRRGB
815
+ expect(prop_data[:icv]).to eq 0xFF
816
+ expect(prop_data[:nTintShade]).to eq 0
817
+ expect(prop_data[:nTintShade_d]).to eq 0.0
818
+ expect(prop_data[:dwRgba]).to eq :'2A8EF2FF' # rgb(42, 142, 242)
819
+ end
820
+
821
+ it 'Decodes structure of xfPropType 0x03 (XFPropGradient, gradient fill)' do
822
+ prop = custom2.find { |e| e[:xfPropType] == 3 }
823
+ expect(prop[:cb]).to eq 48
824
+ expect(prop[:_description]).to eq :gradient_fill
825
+ expect(prop[:_structure]).to eq :XFPropGradient
826
+
827
+ prop_data = prop[:xfPropDataBlob_d]
828
+ expect(prop_data[:type]).to eq 0
829
+ expect(prop_data[:type_d]).to eq :linear
830
+ expect(prop_data[:numDegree]).to eq 45.0
831
+ expect(prop_data[:numFillToLeft]).to eq 0.0
832
+ expect(prop_data[:numFillToRight]).to eq 0.0
833
+ expect(prop_data[:numFillToTop]).to eq 0.0
834
+ expect(prop_data[:numFillToBottom]).to eq 0.0
835
+ end
836
+
837
+ it 'Decodes structure of xfPropType 0x04 (XFPropGradientStop, gradient stop)' do
838
+ props = custom2.select { |e| e[:xfPropType] == 4 }
839
+
840
+ {
841
+ 0 => 0.0,
842
+ 2 => 1.0
843
+ }.each do |prop_index, num_position|
844
+ prop = props[prop_index]
845
+ expect(prop[:cb]).to eq 22
846
+ expect(prop[:_description]).to eq :gradient_stop
847
+ expect(prop[:_structure]).to eq :XFPropGradientStop
848
+
849
+ prop_data = prop[:xfPropDataBlob_d]
850
+ expect(prop_data[:numPosition]).to eq num_position
851
+
852
+ color = prop_data[:color]
853
+ expect(color[:fValidRGBA]).to eq true
854
+ expect(color[:xclrType]).to eq 3
855
+ expect(color[:xclrType_d]).to eq :XCLRTHEMED
856
+ expect(color[:icv]).to eq 7
857
+ expect(color[:nTintShade]).to eq -8224
858
+ expect(color[:nTintShade_d]).to eq -0.25
859
+ expect(color[:dwRgba]).to eq :BD8E00FF
860
+ end
861
+
862
+ prop = props[1]
863
+ expect(prop[:cb]).to eq 22
864
+ expect(prop[:_description]).to eq :gradient_stop
865
+ expect(prop[:_structure]).to eq :XFPropGradientStop
866
+
867
+ prop_data = prop[:xfPropDataBlob_d]
868
+ expect(prop_data[:numPosition]).to eq 0.5
869
+
870
+ color = prop_data[:color]
871
+ expect(color[:fValidRGBA]).to eq true
872
+ expect(color[:xclrType]).to eq 3
873
+ expect(color[:xclrType_d]).to eq :XCLRTHEMED
874
+ expect(color[:icv]).to eq 9
875
+ expect(color[:nTintShade]).to eq 13107
876
+ expect(color[:nTintShade_d]).to eq 0.4
877
+ expect(color[:dwRgba]).to eq :A9D08EFF
878
+ end
879
+
880
+ it 'Decodes structure of xfPropType 0x05 (XFPropColor, text color)' do
881
+ prop = custom1.find { |e| e[:xfPropType] == 5 }
882
+ expect(prop[:cb]).to eq 12
883
+ expect(prop[:_description]).to eq :text_color
884
+ expect(prop[:_structure]).to eq :XFPropColor
885
+
886
+ prop_data = prop[:xfPropDataBlob_d]
887
+ expect(prop_data[:fValidRGBA]).to eq true
888
+ expect(prop_data[:xclrType]).to eq 3
889
+ expect(prop_data[:xclrType_d]).to eq :XCLRTHEMED
890
+ expect(prop_data[:icv]).to eq 5
891
+ expect(prop_data[:nTintShade]).to eq -8190
892
+ expect(prop_data[:nTintShade_d]).to eq -0.25
893
+ expect(prop_data[:dwRgba]).to eq :C65911FF
894
+ end
895
+
896
+ it 'Decodes structure of xfPropType 0x06 (XFPropBorder, top border formatting)' do
897
+ prop = custom1.find { |e| e[:xfPropType] == 6 }
898
+ expect(prop[:cb]).to eq 14
899
+ expect(prop[:_description]).to eq :top_border
900
+ expect(prop[:_structure]).to eq :XFPropBorder
901
+
902
+ prop_data = prop[:xfPropDataBlob_d]
903
+ expect(prop_data[:dgBorder]).to eq 8
904
+ expect(prop_data[:dgBorder_d]).to eq :MEDIUMDASHED
905
+
906
+ color = prop_data[:color]
907
+ expect(color[:fValidRGBA]).to eq true
908
+ expect(color[:xclrType]).to eq 3
909
+ expect(color[:xclrType_d]).to eq :XCLRTHEMED
910
+ expect(color[:icv]).to eq 9
911
+ expect(color[:nTintShade]).to eq 0
912
+ expect(color[:nTintShade_d]).to eq 0.0
913
+ expect(color[:dwRgba]).to eq :'70AD47FF'
914
+ end
915
+
916
+ it 'Decodes structure of xfPropType 0x07 (XFPropBorder, bottom border formatting)' do
917
+ prop = custom1.find { |e| e[:xfPropType] == 7 }
918
+ expect(prop[:cb]).to eq 14
919
+ expect(prop[:_description]).to eq :bottom_border
920
+ expect(prop[:_structure]).to eq :XFPropBorder
921
+
922
+ prop_data = prop[:xfPropDataBlob_d]
923
+ expect(prop_data[:dgBorder]).to eq 1
924
+ expect(prop_data[:dgBorder_d]).to eq :THIN
925
+
926
+ color = prop_data[:color]
927
+ expect(color[:fValidRGBA]).to eq true
928
+ expect(color[:xclrType]).to eq 2
929
+ expect(color[:xclrType_d]).to eq :XCLRRGB
930
+ expect(color[:icv]).to eq 0xFF
931
+ expect(color[:nTintShade]).to eq 0
932
+ expect(color[:nTintShade_d]).to eq 0.0
933
+ expect(color[:dwRgba]).to eq :'7030A0FF'
934
+ end
935
+
936
+ it 'Decodes structure of xfPropType 0x08 (XFPropBorder, left border formatting)' do
937
+ prop = custom1.find { |e| e[:xfPropType] == 8 }
938
+ expect(prop[:cb]).to eq 14
939
+ expect(prop[:_description]).to eq :left_border
940
+ expect(prop[:_structure]).to eq :XFPropBorder
941
+
942
+ prop_data = prop[:xfPropDataBlob_d]
943
+ expect(prop_data[:dgBorder]).to eq 5
944
+ expect(prop_data[:dgBorder_d]).to eq :THICK
945
+
946
+ color = prop_data[:color]
947
+ expect(color[:fValidRGBA]).to eq true
948
+ expect(color[:xclrType]).to eq 2
949
+ expect(color[:xclrType_d]).to eq :XCLRRGB
950
+ expect(color[:icv]).to eq 0xFF
951
+ expect(color[:nTintShade]).to eq 0
952
+ expect(color[:nTintShade_d]).to eq 0.0
953
+ expect(color[:dwRgba]).to eq :'2A8EF2FF'
954
+ end
955
+
956
+ it 'Decodes structure of xfPropType 0x09 (XFPropBorder, right border formatting)' do
957
+ prop = custom1.find { |e| e[:xfPropType] == 9 }
958
+ expect(prop[:cb]).to eq 14
959
+ expect(prop[:_description]).to eq :right_border
960
+ expect(prop[:_structure]).to eq :XFPropBorder
961
+
962
+ prop_data = prop[:xfPropDataBlob_d]
963
+ expect(prop_data[:dgBorder]).to eq 3
964
+ expect(prop_data[:dgBorder_d]).to eq :DASHED
965
+
966
+ color = prop_data[:color]
967
+ expect(color[:fValidRGBA]).to eq true
968
+ expect(color[:xclrType]).to eq 2
969
+ expect(color[:xclrType_d]).to eq :XCLRRGB
970
+ expect(color[:icv]).to eq 0xFF
971
+ expect(color[:nTintShade]).to eq 0
972
+ expect(color[:nTintShade_d]).to eq 0.0
973
+ expect(color[:dwRgba]).to eq :FF0000FF
974
+ end
975
+
976
+ it 'Decodes structure of xfPropType 0x0A (XFPropBorder, diagonal border formatting)' do
977
+ prop = custom1.find { |e| e[:xfPropType] == 10 }
978
+ expect(prop[:cb]).to eq 14
979
+ expect(prop[:_description]).to eq :diagonal_border
980
+ expect(prop[:_structure]).to eq :XFPropBorder
981
+
982
+ prop_data = prop[:xfPropDataBlob_d]
983
+ expect(prop_data[:dgBorder]).to eq 9
984
+ expect(prop_data[:dgBorder_d]).to eq :DASHDOT
985
+
986
+ color = prop_data[:color]
987
+ expect(color[:fValidRGBA]).to eq true
988
+ expect(color[:xclrType]).to eq 3
989
+ expect(color[:xclrType_d]).to eq :XCLRTHEMED
990
+ expect(color[:icv]).to eq 7
991
+ expect(color[:nTintShade]).to eq 0
992
+ expect(color[:nTintShade_d]).to eq 0.0
993
+ expect(color[:dwRgba]).to eq :FFC000FF
994
+ end
995
+
996
+ it 'Decodes structure of 0x0D (diagonal up border)' do
997
+ prop = custom1.find { |e| e[:xfPropType] == 13 }
998
+ expect(prop[:cb]).to eq 5
999
+ expect(prop[:_description]).to eq :diagonal_up
1000
+ expect(prop[:_structure]).to eq :_bool1b
1001
+ expect(prop[:xfPropDataBlob_d]).to eq true
1002
+ end
1003
+
1004
+ it 'Decodes structure of 0x0E (diagonal down)' do
1005
+ prop = custom1.find { |e| e[:xfPropType] == 14 }
1006
+ expect(prop[:cb]).to eq 5
1007
+ expect(prop[:_description]).to eq :diagonal_down
1008
+ expect(prop[:_structure]).to eq :_bool1b
1009
+ expect(prop[:xfPropDataBlob_d]).to eq true
1010
+ end
1011
+ end
1012
+
1013
+ # It seems that in StyleExt record Excel uses only the xfPropTypes that specify
1014
+ # color, to render the resuling RGB values (e.g. themed color with applied tint,
1015
+ # or RGB color) to the dwRgba field. Other properties, even though they are
1016
+ # specified for the style, are written to style XFs. At any rate, there seems
1017
+ # no way to make Excel to write out these properties to a StyleExt record using
1018
+ # the UI.
1019
+ context 'Seemingly unused xfPropTypes' do
1020
+ # it 'Decodes structure of xfPropType 0x00 (FillPattern)' # used by TableStyleElement/DXF
1021
+ # it 'Decodes structure of xfPropType 0x0B (XFPropBorder, vertical border formatting)' # used by TableStyleElement/DXF
1022
+ # it 'Decodes structure of xfPropType 0x0C (XFPropBorder, horizontal border formatting)' # used by TableStyleElement/DXF
1023
+ it 'Decodes structure of 0x0F (HorizAlign, horizontal alignment)'
1024
+ it 'Decodes structure of 0x10 (VertAlign, vertical alignment)'
1025
+ it 'Decodes structure of 0x11 (XFPropTextRotation, text rotation)'
1026
+ it 'Decodes structure of 0x12 (indentation level)'
1027
+ it 'Decodes structure of 0x13 (ReadingOrder, reading order)'
1028
+ it 'Decodes structure of 0x14 (text wrap)'
1029
+ it 'Decodes structure of 0x15 (justify distributed)'
1030
+ it 'Decodes structure of 0x16 (shrink to fit)'
1031
+ it 'Decodes structure of 0x17 (cell is merged)'
1032
+ it 'Decodes structure of 0x18 (LPWideString, font name)'
1033
+ # it 'Decodes structure of 0x19 (Bold, font_weight)' # used by TableStyleElement/DXF
1034
+ # it 'Decodes structure of 0x1A (Underline, underline style)' # used by TableStyleElement/DXF
1035
+ it 'Decodes structure of 0x1B (Script, script style)'
1036
+ # it 'Decodes structure of 0x1C (text is italicized)' # used by TableStyleElement/DXF
1037
+ # it 'Decodes structure of 0x1D (text has strikethrough formatting)' # used by TableStyleElement/DXF
1038
+ it 'Decodes structure of 0x1E (text has an outline style)'
1039
+ it 'Decodes structure of 0x1F (text has a shadow style)'
1040
+ it 'Decodes structure of 0x20 (text is condensed)'
1041
+ it 'Decodes structure of 0x21 (text is extended)'
1042
+ it 'Decodes structure of 0x22 (font character set)'
1043
+ it 'Decodes structure of 0x23 (font family)'
1044
+ it 'Decodes structure of 0x24 (text size)'
1045
+ it 'Decodes structure of 0x25 (FontScheme, font scheme)'
1046
+ it 'Decodes structure of 0x26 (XLUnicodeString, number format string)'
1047
+ it 'Decodes structure of 0x29 (IFmt, number format identifier)'
1048
+ it 'Decodes structure of 0x2A (text relative indentation)'
1049
+ it 'Decodes structure of 0x2B (locked)'
1050
+ it 'Decodes structure of 0x2C (hidden)'
1051
+ end
1052
+ end
1053
+
1054
+ context 'TableStyles, TableStyle, TableStyleElement records' do
1055
+ before(:context) do
1056
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'tablestyle.xls'))
1057
+ end
1058
+
1059
+ let(:ts_name) { 'Custom Table Style' }
1060
+ let(:pts_name) { 'Custom PivotTable Style' }
1061
+
1062
+ it 'Decodes TableStyles record' do
1063
+ record = @browser.globals[:TableStyles]
1064
+ expect(record[:frtHeader]).to be
1065
+ expect(record[:cts]).to eq 146
1066
+ expect(record[:cchDefTableStyle]).to eq 18
1067
+ expect(record[:cchDefPivotStyle]).to eq 23
1068
+ expect(record[:rgchDefTableStyle]).to eq ts_name
1069
+ expect(record[:rgchDefPivotStyle]).to eq pts_name
1070
+ end
1071
+
1072
+ it 'Decodes TableStyle record' do
1073
+ record = @browser.get_tablestyle(ts_name)
1074
+ expect(record[:rgchName]).to eq ts_name
1075
+ expect(record[:fIsPivot]).to eq false
1076
+ expect(record[:fIsTable]).to eq true
1077
+ expect(record[:ctse]).to be_between(1, 28)
1078
+ expect(record[:cchName]).to eq 18
1079
+
1080
+ record = @browser.get_tablestyle(pts_name)
1081
+ expect(record[:rgchName]).to eq pts_name
1082
+ expect(record[:fIsPivot]).to eq true
1083
+ expect(record[:fIsTable]).to eq false
1084
+ expect(record[:ctse]).to be_between(1, 28)
1085
+ expect(record[:cchName]).to eq 23
1086
+ end
1087
+
1088
+ context 'TableStyleElement record' do
1089
+ [
1090
+ { tseType: 0x00, tseType_d: :whole_table }, # Whole Table
1091
+ { tseType: 0x01, tseType_d: :header_row }, # Header Row
1092
+ { tseType: 0x02, tseType_d: :total_row }, # Grand Total Row
1093
+ { tseType: 0x03, tseType_d: :first_column }, # First Column
1094
+ { tseType: 0x04, tseType_d: :last_column }, # Grand Total Column
1095
+ { tseType: 0x05, tseType_d: :row_stripe_1, size: 4 }, # First Row Stripe
1096
+ { tseType: 0x06, tseType_d: :row_stripe_2, size: 5 }, # Second Row Stripe
1097
+ { tseType: 0x07, tseType_d: :column_stripe_1, size: 2 }, # First Column Stripe
1098
+ { tseType: 0x08, tseType_d: :column_stripe_2, size: 3 }, # Second Column Stripe
1099
+ { tseType: 0x09, tseType_d: :first_cell_header }, # First Header Cell
1100
+ { tseType: 0x0A, tseType_d: :last_cell_header }, # Last Header Cell
1101
+ { tseType: 0x0B, tseType_d: :first_cell_total }, # First Total Cell
1102
+ { tseType: 0x0C, tseType_d: :last_cell_total }, # Last Total Cell
1103
+ { tseType: 0x0D, tseType_d: :pt_outermost_subtotal_columns }, # Subtotal Column 1
1104
+ { tseType: 0x0E, tseType_d: :pt_alternating_even_subtotal_columns }, # Subtotal Column 2
1105
+ { tseType: 0x0F, tseType_d: :pt_alternating_odd_subtotal_columns }, # Subtotal Column 3
1106
+ { tseType: 0x10, tseType_d: :pt_outermost_subtotal_rows }, # Subtotal Row 1
1107
+ { tseType: 0x11, tseType_d: :pt_alternating_even_subtotal_rows }, # Subtotal Row 2
1108
+ { tseType: 0x12, tseType_d: :pt_alternating_odd_subtotal_rows }, # Subtotal Row 3
1109
+ { tseType: 0x13, tseType_d: :pt_empty_rows_after_each_subtotal_row }, # Blank Row
1110
+ { tseType: 0x14, tseType_d: :pt_outermost_column_subheadings }, # Column Subheading 1
1111
+ { tseType: 0x15, tseType_d: :pt_alternating_even_column_subheadings }, # Column Subheading 2
1112
+ { tseType: 0x16, tseType_d: :pt_alternating_odd_column_subheadings }, # Column Subheading 3
1113
+ { tseType: 0x17, tseType_d: :pt_outermost_row_subheadings }, # Row Subheading 1
1114
+ { tseType: 0x18, tseType_d: :pt_alternating_even_row_subheadings }, # Row Subheading 2
1115
+ { tseType: 0x19, tseType_d: :pt_alternating_odd_row_subheadings }, # Row Subheading 3
1116
+ { tseType: 0x1A, tseType_d: :pt_page_field_captions }, # Report Filter Labels
1117
+ { tseType: 0x1B, tseType_d: :pt_page_item_captions }, # Report Filter Values
1118
+ ].each do |params|
1119
+ it "Decodes records of type #{params[:tseType_d]}" do
1120
+ style_name = params[:tseType_d].to_s.start_with?('pt_') ? pts_name : ts_name
1121
+ element = @browser.get_tablestyleelement(style_name, params[:tseType_d])
1122
+ expect(element[:tseType]).to eq params[:tseType]
1123
+ expect(element[:tseType_d]).to eq params[:tseType_d]
1124
+ if params[:size]
1125
+ expect(element[:size]).to eq params[:size]
1126
+ else
1127
+ expect(element[:size]).to be_between(1, 9)
1128
+ end
1129
+ expect(element[:index]).to be_an_kind_of Integer
1130
+ end
1131
+ end
1132
+
1133
+ it 'Adds custom :_tsi field to reference parent TableStyle record' do
1134
+ styles = @browser.globals[:TableStyle]
1135
+ styles.each do |style|
1136
+ index = style[:_record][:index]
1137
+ number_of_elements = @browser.globals[:TableStyleElement].select { |r| r[:_tsi] == index }.size
1138
+ expect(style[:ctse]).to eq number_of_elements
1139
+ end
1140
+ end
1141
+ end
1142
+ end
1143
+
1144
+ context 'XF record' do
1145
+ before(:context) do
1146
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'xf.xls'))
1147
+ end
1148
+
1149
+ # Relation to Font record
1150
+ it 'Decodes ifnt field' do
1151
+ ifnt = @browser.get_xf(1, 2, 0)[:ifnt]
1152
+ ifnt -= 1 if ifnt >= 4 # See p. 677
1153
+ expect(@browser.globals[:Font][ifnt][:fontName]).to eq 'Mongolian Baiti'
1154
+ end
1155
+
1156
+ # Relation to Format record
1157
+ it 'Decodes ifmt field' do
1158
+ expect(@browser.get_xf(1, 2, 1)[:ifmt]).to eq 0
1159
+
1160
+ ifmt = @browser.get_xf(1, 3, 1)[:ifmt]
1161
+ format = @browser.globals[:Format].find { |r| r[:ifmt] == ifmt }
1162
+ expect(format[:stFormat]).to eq '"Format フォーマット"'
1163
+ end
1164
+
1165
+ # Format - Cells - Protection - Locked
1166
+ it 'Decodes fLocked field' do
1167
+ expect(@browser.get_xf(1, 2, 2)[:fLocked]).to eq true
1168
+ expect(@browser.get_xf(1, 3, 2)[:fLocked]).to eq false
1169
+ end
1170
+
1171
+ # Format - Cells - Protection - Hidden
1172
+ it 'Decodes fHidden field' do
1173
+ expect(@browser.get_xf(1, 2, 3)[:fHidden]).to eq true
1174
+ expect(@browser.get_xf(1, 3, 3)[:fHidden]).to eq false
1175
+ end
1176
+
1177
+ # Lotus 1-2-3 prefixes for text alignment. Enable (Win-only): Tools - Options - Transition - Transition navigation keys
1178
+ # Mac version seems to keep these fields between saves
1179
+ it 'Decodes f123Prefix field' do
1180
+ expect(@browser.get_xf(1, 2, 4)[:f123Prefix]).to eq false # none
1181
+ expect(@browser.get_xf(1, 3, 4)[:f123Prefix]).to eq true # ' single quote 0x27 (left-aligned)
1182
+ expect(@browser.get_xf(1, 4, 4)[:f123Prefix]).to eq true # " double quote 0x22 (right-aligned)
1183
+ expect(@browser.get_xf(1, 5, 4)[:f123Prefix]).to eq true # ^ caret 0x5E (centered)
1184
+ expect(@browser.get_xf(1, 6, 4)[:f123Prefix]).to eq true # \ backslash 0x5C (justified)
1185
+ end
1186
+
1187
+ # Specifies whether XF is a style
1188
+ it 'Decodes fStyle field' do
1189
+ 14.times do |i|
1190
+ expect(@browser.globals[:XF][i][:fStyle]).to eq true # Built-in style XFs
1191
+ end
1192
+
1193
+ expect(@browser.globals[:XF][15][:fStyle]).to eq false # Default cell format XF
1194
+
1195
+ user_style1_xf_index = @browser.get_xf(1, 5, 5)[:ixfParent] # "User style 1" XF
1196
+ expect(@browser.globals[:XF][user_style1_xf_index][:fStyle]).to eq true
1197
+ end
1198
+
1199
+ # Relation of cell XF to its style XF
1200
+ it 'Decodes ixfParent field' do
1201
+ expect(@browser.globals[:XF][0][:ixfParent]).to eq 0xFFF # Builtin Normal style XF
1202
+ expect(@browser.globals[:XF][15][:ixfParent]).to eq 0 # Default cell format XF
1203
+ expect(@browser.get_xf(1, 2, 5)[:ixfParent]).to eq 0 # Cell set to Normal style
1204
+ expect(@browser.get_xf(1, 3, 5)[:ixfParent]).to eq 16 # Set to 20% - Accent1 style
1205
+ expect(@browser.get_xf(1, 4, 5)[:ixfParent]).to eq 40 # Set to Bad style
1206
+ end
1207
+
1208
+ # Names of built-in style XFs
1209
+ it 'Adds _description field for 16 default XFs' do
1210
+ expect(@browser.globals[:XF][0][:_description]).to eq :'Normal style'
1211
+ expect(@browser.globals[:XF][1][:_description]).to eq :'Row outline level 1'
1212
+ expect(@browser.globals[:XF][8][:_description]).to eq :'Column outline level 1'
1213
+ expect(@browser.globals[:XF][15][:_description]).to eq :'Default cell format'
1214
+ expect(@browser.globals[:XF][16][:_description]).to eq nil
1215
+ end
1216
+
1217
+ # Decoded fStyle field
1218
+ it 'Adds _type field' do
1219
+ expect(@browser.globals[:XF][0][:_type]).to eq :stylexf
1220
+ expect(@browser.globals[:XF][15][:_type]).to eq :cellxf
1221
+ expect(@browser.get_xf(1, 5, 5)[:_type]).to eq :cellxf
1222
+ end
1223
+
1224
+ # Text alignment
1225
+ it 'Decodes alc field and adds alc_d field' do
1226
+ # Can't find usage of alc == 0xFF (:ALCNIL, Alignment not specified) in cell XFs and style XFs
1227
+
1228
+ f = @browser.get_xf(1, 2, 6)
1229
+ expect(f[:alc]).to eq 0
1230
+ expect(f[:alc_d]).to eq :ALCGEN
1231
+
1232
+ f = @browser.get_xf(1, 3, 6)
1233
+ expect(f[:alc]).to eq 1
1234
+ expect(f[:alc_d]).to eq :ALCLEFT
1235
+
1236
+ f = @browser.get_xf(1, 4, 6)
1237
+ expect(f[:alc]).to eq 2
1238
+ expect(f[:alc_d]).to eq :ALCCTR
1239
+
1240
+ f = @browser.get_xf(1, 5, 6)
1241
+ expect(f[:alc]).to eq 3
1242
+ expect(f[:alc_d]).to eq :ALCRIGHT
1243
+
1244
+ f = @browser.get_xf(1, 6, 6)
1245
+ expect(f[:alc]).to eq 4
1246
+ expect(f[:alc_d]).to eq :ALCFILL
1247
+
1248
+ f = @browser.get_xf(1, 7, 6)
1249
+ expect(f[:alc]).to eq 5
1250
+ expect(f[:alc_d]).to eq :ALCJUST
1251
+
1252
+ f = @browser.get_xf(1, 8, 6)
1253
+ expect(f[:alc]).to eq 6
1254
+ expect(f[:alc_d]).to eq :ALCCONTCTR
1255
+
1256
+ f = @browser.get_xf(1, 9, 6)
1257
+ expect(f[:alc]).to eq 7
1258
+ expect(f[:alc_d]).to eq :ALCDIST
1259
+ end
1260
+
1261
+ # Alignment - Text control - Wrap text
1262
+ it 'Decodes fWrap field' do
1263
+ expect(@browser.get_xf(1, 2, 7)[:fWrap]).to eq true
1264
+ expect(@browser.get_xf(1, 3, 7)[:fWrap]).to eq false
1265
+ end
1266
+
1267
+ # Vertical alignment
1268
+ it 'Decodes alcV field and adds alcV_d field' do
1269
+ f = @browser.get_xf(1, 2, 8)
1270
+ expect(f[:alcV]).to eq 0
1271
+ expect(f[:alcV_d]).to eq :ALCVTOP
1272
+
1273
+ f = @browser.get_xf(1, 3, 8)
1274
+ expect(f[:alcV]).to eq 1
1275
+ expect(f[:alcV_d]).to eq :ALCVCTR
1276
+
1277
+ f = @browser.get_xf(1, 4, 8)
1278
+ expect(f[:alcV]).to eq 2
1279
+ expect(f[:alcV_d]).to eq :ALCVBOT
1280
+
1281
+ f = @browser.get_xf(1, 5, 8)
1282
+ expect(f[:alcV]).to eq 3
1283
+ expect(f[:alcV_d]).to eq :ALCVJUST
1284
+
1285
+ f = @browser.get_xf(1, 6, 8)
1286
+ expect(f[:alcV]).to eq 4
1287
+ expect(f[:alcV_d]).to eq :ALCVDIST
1288
+ end
1289
+
1290
+ # Alignment - Text alignment – Justify distributed
1291
+ it 'Decodes fJustLast field' do
1292
+ expect(@browser.get_xf(1, 2, 9)[:fJustLast]).to eq true # 'Justify distributed' on
1293
+ expect(@browser.get_xf(1, 3, 9)[:fJustLast]).to eq false
1294
+ end
1295
+
1296
+ # Alignment – Orientation
1297
+ it 'Decodes trot field and adds trot_d field' do
1298
+ f = @browser.get_xf(1, 2, 10) # normal
1299
+ expect(f[:trot]).to eq 0
1300
+ expect(f[:trot_d]).to eq :counterclockwise
1301
+
1302
+ f = @browser.get_xf(1, 3, 10) # -45
1303
+ expect(f[:trot]).to eq 45
1304
+ expect(f[:trot_d]).to eq :counterclockwise
1305
+
1306
+ f = @browser.get_xf(1, 4, 10) # +45
1307
+ expect(f[:trot]).to eq 135
1308
+ expect(f[:trot_d]).to eq :clockwise
1309
+
1310
+ f = @browser.get_xf(1, 5, 10) # vertical
1311
+ expect(f[:trot]).to eq 255
1312
+ expect(f[:trot_d]).to eq :vertical
1313
+ end
1314
+
1315
+ # Alignment - Text alignment – Indent
1316
+ it 'Decodes cIndent field' do
1317
+ expect(@browser.get_xf(1, 2, 11)[:cIndent]).to eq 0
1318
+ expect(@browser.get_xf(1, 3, 11)[:cIndent]).to eq 5
1319
+ expect(@browser.get_xf(1, 4, 11)[:cIndent]).to eq 15
1320
+ end
1321
+
1322
+ # Alignment - Text control - Shrink to fit
1323
+ it 'Decodes fShrinkToFit field' do
1324
+ expect(@browser.get_xf(1, 2, 12)[:fShrinkToFit]).to eq true
1325
+ expect(@browser.get_xf(1, 3, 13)[:fShrinkToFit]).to eq false
1326
+ end
1327
+
1328
+ # Alignment - Right-to-left - Text direction
1329
+ it 'Decodes iReadingOrder field and adds iReadingOrder_d field' do
1330
+ f = @browser.get_xf(1, 2, 13)
1331
+ expect(f[:iReadingOrder]).to eq 0
1332
+ expect(f[:iReadingOrder_d]).to eq :READING_ORDER_CONTEXT
1333
+
1334
+ f = @browser.get_xf(1, 3, 13)
1335
+ expect(f[:iReadingOrder]).to eq 1
1336
+ expect(f[:iReadingOrder_d]).to eq :READING_ORDER_LTR
1337
+
1338
+ f = @browser.get_xf(1, 4, 13)
1339
+ expect(f[:iReadingOrder]).to eq 2
1340
+ expect(f[:iReadingOrder_d]).to eq :READING_ORDER_RTL
1341
+ end
1342
+
1343
+ # Flags specifying whether changes in parent XFs are not reflected in this XF
1344
+ # Hoping that there are records in the test file with this bit set to true:
1345
+ %i(fAtrNum fAtrFnt fAtrAlc fAtrBdr fAtrPat fAtrProt).each do |attr|
1346
+ it "Decodes #{attr} field" do
1347
+ [true, false].each do |val|
1348
+ xf = @browser.mapif(:XF, first: true) { |r, _, _, ssi| r if ssi == 0 && r[attr] == val }
1349
+ expect(xf[attr]).to eq val
1350
+ end
1351
+ end
1352
+ end
1353
+
1354
+ let(:border_styles) do
1355
+ [
1356
+ { row: 2, num_val: 0x00, decoded_val: :NONE },
1357
+ { row: 3, num_val: 0x01, decoded_val: :THIN },
1358
+ { row: 4, num_val: 0x02, decoded_val: :MEDIUM },
1359
+ { row: 5, num_val: 0x03, decoded_val: :DASHED },
1360
+ { row: 6, num_val: 0x04, decoded_val: :DOTTED },
1361
+ { row: 7, num_val: 0x05, decoded_val: :THICK },
1362
+ { row: 8, num_val: 0x06, decoded_val: :DOUBLE },
1363
+ { row: 9, num_val: 0x07, decoded_val: :HAIR },
1364
+ { row: 10, num_val: 0x08, decoded_val: :MEDIUMDASHED },
1365
+ { row: 11, num_val: 0x09, decoded_val: :DASHDOT },
1366
+ { row: 12, num_val: 0x0A, decoded_val: :MEDIUMDASHDOT },
1367
+ { row: 13, num_val: 0x0B, decoded_val: :DASHDOTDOT },
1368
+ { row: 14, num_val: 0x0C, decoded_val: :MEDIUMDASHDOTDOT },
1369
+ { row: 15, num_val: 0x0D, decoded_val: :SLANTEDDASHDOTDOT },
1370
+ ]
1371
+ end
1372
+
1373
+ [
1374
+ { col: 14, num_prop: :dgLeft, decoded_prop: :dgLeft_d },
1375
+ { col: 15, num_prop: :dgRight, decoded_prop: :dgRight_d },
1376
+ { col: 17, num_prop: :dgTop, decoded_prop: :dgTop_d },
1377
+ { col: 18, num_prop: :dgBottom, decoded_prop: :dgBottom_d },
1378
+ { col: 26, num_prop: :dgDiag, decoded_prop: :dgDiag_d },
1379
+ ].each do |attrs|
1380
+ it "Decodes #{attrs[:num_prop]} and adds #{attrs[:decoded_prop]} field" do
1381
+ border_styles.each do |bs|
1382
+ f = @browser.get_xf(1, bs[:row], attrs[:col])
1383
+ expect(f[attrs[:decoded_prop]]).to eq bs[:decoded_val]
1384
+ expect(f[attrs[:num_prop]]).to eq bs[:num_val]
1385
+ end
1386
+ end
1387
+ end
1388
+
1389
+ # Border colors (see 2.5.161 Icv)
1390
+ let(:border_icvs) do
1391
+ {
1392
+ 2 => 0x40, # default text color
1393
+ 3 => 0x23, # cyan
1394
+ 4 => 0x22, # yellow
1395
+ }
1396
+ end
1397
+
1398
+ {
1399
+ icvLeft: 20,
1400
+ icvRight: 21,
1401
+ icvTop: 23,
1402
+ icvBottom: 24,
1403
+ icvDiag: 25,
1404
+ }.each do |field, col|
1405
+ it "Decodes #{field} field" do
1406
+ border_icvs.each do |row, icv|
1407
+ expect(@browser.get_xf(1, row, col)[field]).to eq icv
1408
+ end
1409
+ end
1410
+ end
1411
+
1412
+ let(:diagonals) do
1413
+ [
1414
+ { row: 2, grbitDiag: 0, grbitDiag_d: :'No diagonal border' },
1415
+ { row: 3, grbitDiag: 1, grbitDiag_d: :'Diagonal-down border' },
1416
+ { row: 4, grbitDiag: 2, grbitDiag_d: :'Diagonal-up border' },
1417
+ { row: 5, grbitDiag: 3, grbitDiag_d: :'Both diagonal-down and diagonal-up' },
1418
+ ]
1419
+ end
1420
+
1421
+ # Diagonal lines (borders)
1422
+ it 'Decodes grbitDiag and adds grbitDiag_d field' do
1423
+ diagonals.each do |attrs|
1424
+ f = @browser.get_xf(1, attrs[:row], 22)
1425
+ expect(f[:grbitDiag]).to eq(attrs[:grbitDiag])
1426
+ expect(f[:grbitDiag_d]).to eq(attrs[:grbitDiag_d])
1427
+ end
1428
+ end
1429
+
1430
+ it 'Decodes fHasXFExt field' do
1431
+ expect(@browser.get_xf(1, 2, 27)[:fHasXFExt]).to eq true # Uses advanced styling (RGB font color) specified in XFExt
1432
+ expect(@browser.get_xf(1, 3, 27)[:fHasXFExt]).to eq false
1433
+ end
1434
+
1435
+ let(:fill_patterns) do
1436
+ [
1437
+ { row: 2, fls: 0x00, fls_d: :FLSNULL }, # No fill pattern
1438
+ { row: 3, fls: 0x01, fls_d: :FLSSOLID }, # Solid
1439
+ { row: 4, fls: 0x02, fls_d: :FLSMEDGRAY }, # 50% gray
1440
+ { row: 5, fls: 0x03, fls_d: :FLSDKGRAY }, # 75% gray
1441
+ { row: 6, fls: 0x04, fls_d: :FLSLTGRAY }, # 25% gray
1442
+ { row: 7, fls: 0x05, fls_d: :FLSDKHOR }, # Horizontal stripe
1443
+ { row: 8, fls: 0x06, fls_d: :FLSDKVER }, # Vertical stripe
1444
+ { row: 9, fls: 0x07, fls_d: :FLSDKDOWN }, # Reverse diagonal stripe
1445
+ { row: 10, fls: 0x08, fls_d: :FLSDKUP }, # Diagonal stripe
1446
+ { row: 11, fls: 0x09, fls_d: :FLSDKGRID }, # Diagonal crosshatch
1447
+ { row: 12, fls: 0x0A, fls_d: :FLSDKTRELLIS }, # Thick diagonal crosshatch
1448
+ { row: 13, fls: 0x0B, fls_d: :FLSLTHOR }, # Thin horizontal stripe
1449
+ { row: 14, fls: 0x0C, fls_d: :FLSLTVER }, # Thin vertical stripe
1450
+ { row: 15, fls: 0x0D, fls_d: :FLSLTDOWN }, # Thin reverse diagonal stripe
1451
+ { row: 16, fls: 0x0E, fls_d: :FLSLTUP }, # Thin diagonal stripe
1452
+ { row: 17, fls: 0x0F, fls_d: :FLSLTGRID }, # Thin horizontal crosshatch
1453
+ { row: 18, fls: 0x10, fls_d: :FLSLTTRELLIS }, # Thin diagonal crosshatch
1454
+ { row: 19, fls: 0x11, fls_d: :FLSGRAY125 }, # 12.5% gray
1455
+ { row: 20, fls: 0x12, fls_d: :FLSGRAY0625 }, # 6.25% gray
1456
+ ]
1457
+ end
1458
+
1459
+ # Cell pattern type
1460
+ it 'Decodes fls and adds fls_d field' do
1461
+ fill_patterns.each do |attrs|
1462
+ f = @browser.get_xf(1, attrs[:row], 28)
1463
+ expect(f[:fls]).to eq attrs[:fls]
1464
+ expect(f[:fls_d]).to eq attrs[:fls_d]
1465
+ end
1466
+ end
1467
+
1468
+ # Cell pattern color (see 2.5.161 Icv)
1469
+ it 'Decodes icvFore field' do
1470
+ expect(@browser.get_xf(1, 2, 29)[:icvFore]).to eq 0x40 # default foreground color
1471
+ expect(@browser.get_xf(1, 3, 29)[:icvFore]).to eq 0x23 # cyan
1472
+ end
1473
+
1474
+ # Cell background color (see 2.5.161 Icv)
1475
+ it 'Decodes icvBack field' do
1476
+ expect(@browser.get_xf(1, 2, 30)[:icvBack]).to eq 0x41 # default background color
1477
+ expect(@browser.get_xf(1, 3, 30)[:icvBack]).to eq 0x23 # cyan
1478
+ end
1479
+
1480
+ # Cell is a PivotTable button
1481
+ it 'Decodes fsxButton field' do
1482
+ expect(@browser.get_xf(1, 2, 31)[:fsxButton]).to eq true
1483
+ expect(@browser.get_xf(1, 3, 31)[:fsxButton]).to eq true
1484
+ expect(@browser.get_xf(1, 4, 31)[:fsxButton]).to eq false
1485
+ end
1486
+ end
1487
+
1488
+ context 'XFExt record' do
1489
+ before(:context) do
1490
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'xfext.xls'))
1491
+ end
1492
+
1493
+ it 'Decodes frtHeader field' do
1494
+ f = @browser.get_xfext(1, 3, 0)
1495
+ expect(f[:frtHeader][:rt]).to eq f[:_record][:id]
1496
+ expect(f[:frtHeader][:grBitFrt]).to eq({ fFrtRef: false, fFrtAlert: false })
1497
+ end
1498
+
1499
+ it 'Decodes ixfe field' do
1500
+ expect(@browser.get_xfext(1, 3, 0)[:ixfe]).to be_an_kind_of Integer
1501
+ expect(@browser.get_xf(1, 3, 0)[:fHasXFExt]).to eq true
1502
+ end
1503
+
1504
+ it 'Decodes cexts field' do
1505
+ expect(@browser.get_xfext(1, 3, 1)[:cexts]).to eq 2
1506
+ end
1507
+
1508
+ context 'Decodes rgExt field' do
1509
+ context 'ExtProp type 0x04 (FullColorExt) for foreground (pattern) color' do
1510
+ it 'With xclrType 0 (XCLRAUTO)' # cannot find an example
1511
+
1512
+ it 'With xclrType 1 (XCLRINDEXED)' # cannot find an example
1513
+
1514
+ it 'With xclrType 2 (XCLRRGB)' do
1515
+ extprop = @browser.get_xfext(1, 3, 0)[:rgExt].find { |h| h[:_property] == :'cell interior foreground color' }
1516
+
1517
+ expect(extprop[:extType]).to eq 4
1518
+ expect(extprop[:extType_d]).to eq :FullColorExt
1519
+ expect(extprop[:cb]).to eq 20
1520
+
1521
+ propdata = extprop[:extPropData]
1522
+ expect(propdata[:xclrType]).to eq 2
1523
+ expect(propdata[:xclrType_d]).to eq :XCLRRGB
1524
+ expect(propdata[:nTintShade]).to eq 0
1525
+ expect(propdata[:nTintShade_d]).to eq 0.0
1526
+ expect(propdata[:xclrValue]).to eq :'2A8EF2FF' # rgb(42, 142, 242)
1527
+ end
1528
+
1529
+ it 'With xclrType 3 (XCLRTHEMED)' do
1530
+ extprop = @browser.get_xfext(1, 4, 0)[:rgExt].find { |h| h[:_property] == :'cell interior foreground color' }
1531
+
1532
+ expect(extprop[:extType]).to eq 4
1533
+ expect(extprop[:extType_d]).to eq :FullColorExt
1534
+ expect(extprop[:cb]).to eq 20
1535
+
1536
+ propdata = extprop[:extPropData]
1537
+ expect(propdata[:xclrType]).to eq 3
1538
+ expect(propdata[:xclrType_d]).to eq :XCLRTHEMED # Blue, Accent 5, lighter 40%
1539
+ expect(propdata[:nTintShade]).to eq 13105
1540
+ expect(propdata[:nTintShade_d]).to eq 0.4
1541
+ expect(propdata[:xclrValue]).to eq 8
1542
+ end
1543
+
1544
+ it 'With xclrType 4 (XCLRNINCHED)' # cannot find an example
1545
+ end
1546
+
1547
+ context 'ExtProp type 0x05 (FullColorExt) for background color' do
1548
+ it 'With xclrType 2 (XCLRRGB)' do
1549
+ extprop = @browser.get_xfext(1, 3, 1)[:rgExt].find { |h| h[:_property] == :'cell interior background color' }
1550
+ propdata = extprop[:extPropData]
1551
+ expect(propdata[:xclrType_d]).to eq :XCLRRGB
1552
+ expect(propdata[:xclrValue]).to eq :'2A8EF2FF'
1553
+ end
1554
+
1555
+ it 'With xclrType 3 (XCLRTHEMED)' do
1556
+ extprop = @browser.get_xfext(1, 4, 1)[:rgExt].find { |h| h[:_property] == :'cell interior background color' }
1557
+ propdata = extprop[:extPropData]
1558
+ expect(propdata[:xclrType_d]).to eq :XCLRTHEMED
1559
+ expect(propdata[:nTintShade_d]).to eq 0.4
1560
+ expect(propdata[:xclrValue]).to eq 8
1561
+ end
1562
+ end
1563
+
1564
+ context 'ExtProp type 0x06 (XFExtGradient) for cell interior gradient' do
1565
+ def get_gradient_rgext(substream_index, row, column)
1566
+ @browser.get_xfext(substream_index, row, column)[:rgExt].find do |h|
1567
+ h[:_property] == :'cell interior gradient fill'
1568
+ end
1569
+ end
1570
+
1571
+ def check_type(extprop)
1572
+ expect(extprop[:extType]).to eq 6
1573
+ expect(extprop[:extType_d]).to eq :XFExtGradient
1574
+ end
1575
+
1576
+ def check_linear_grad_props(propdata)
1577
+ expect(propdata[:type]).to eq 0
1578
+ expect(propdata[:type_d]).to eq :linear
1579
+ %i(numFillToLeft numFillToRight numFillToTop numFillToBottom).each do |prop|
1580
+ expect(propdata[prop]).to eql 0.0
1581
+ end
1582
+ end
1583
+
1584
+ def check_rectangular_grad_props(propdata)
1585
+ expect(propdata[:type]).to eq 1
1586
+ expect(propdata[:type_d]).to eq :rectangular
1587
+ expect(propdata[:numDegree]).to eql 0.0
1588
+ end
1589
+
1590
+ def check_gradstops_2_themed(gradstops)
1591
+ (0..1).each do |i|
1592
+ expect(gradstops[i][:xclrType]).to eq 3
1593
+ expect(gradstops[i][:xclrType_d]).to eq :XCLRTHEMED
1594
+ expect(gradstops[i][:numTint]).to eql 0.0
1595
+ end
1596
+
1597
+ expect(gradstops[0][:xclrValue]).to eq 0
1598
+ expect(gradstops[0][:numPosition]).to eql 0.0
1599
+
1600
+ expect(gradstops[1][:xclrValue]).to eq 4
1601
+ expect(gradstops[1][:numPosition]).to eql 1.0
1602
+ end
1603
+
1604
+ def check_gradstops_3_themed(gradstops)
1605
+ (0..2).each do |i|
1606
+ expect(gradstops[i][:xclrType]).to eq 3
1607
+ expect(gradstops[i][:xclrType_d]).to eq :XCLRTHEMED
1608
+ expect(gradstops[i][:numTint]).to eql 0.0
1609
+ end
1610
+
1611
+ expect(gradstops[0][:xclrValue]).to eq 0
1612
+ expect(gradstops[0][:numPosition]).to eql 0.0
1613
+
1614
+ expect(gradstops[1][:xclrValue]).to eq 4
1615
+ expect(gradstops[1][:numPosition]).to eql 0.5
1616
+
1617
+ expect(gradstops[2][:xclrValue]).to eq 0
1618
+ expect(gradstops[2][:numPosition]).to eql 1.0
1619
+ end
1620
+
1621
+ it 'With XFExtGradient type 0 (linear), 0 deg, 2 stops, themed color' do
1622
+ extprop = get_gradient_rgext(1, 3, 2)
1623
+
1624
+ check_type(extprop)
1625
+ expect(extprop[:cb]).to eq 96
1626
+
1627
+ propdata = extprop[:extPropData]
1628
+ check_linear_grad_props(propdata)
1629
+ expect(propdata[:numDegree]).to eql 0.0
1630
+ expect(propdata[:cGradStops]).to eq 2
1631
+
1632
+ check_gradstops_2_themed(propdata[:rgGradStops])
1633
+ end
1634
+
1635
+ it 'With XFExtGradient type 0 (linear), 45 deg, 2 stops, themed color' do
1636
+ extprop = get_gradient_rgext(1, 4, 2)
1637
+
1638
+ check_type(extprop)
1639
+ expect(extprop[:cb]).to eq 96
1640
+
1641
+ propdata = extprop[:extPropData]
1642
+ check_linear_grad_props(propdata)
1643
+ expect(propdata[:numDegree]).to eql 45.0
1644
+ expect(propdata[:cGradStops]).to eq 2
1645
+
1646
+ check_gradstops_2_themed(propdata[:rgGradStops])
1647
+ end
1648
+
1649
+ it 'With XFExtGradient type 0 (linear), 90 deg, 3 stops, themed color' do
1650
+ extprop = get_gradient_rgext(1, 5, 2)
1651
+
1652
+ check_type(extprop)
1653
+ expect(extprop[:cb]).to eq 118
1654
+
1655
+ propdata = extprop[:extPropData]
1656
+ check_linear_grad_props(propdata)
1657
+ expect(propdata[:numDegree]).to eql 90.0
1658
+ expect(propdata[:cGradStops]).to eq 3
1659
+
1660
+ check_gradstops_3_themed(propdata[:rgGradStops])
1661
+ end
1662
+
1663
+ it 'With XFExtGradient type 1 (rectangular), left-top aligned, 2 stops, themed color' do
1664
+ extprop = get_gradient_rgext(1, 6, 2)
1665
+
1666
+ check_type(extprop)
1667
+ expect(extprop[:cb]).to eq 96
1668
+
1669
+ propdata = extprop[:extPropData]
1670
+ check_rectangular_grad_props(propdata)
1671
+ %i(numFillToLeft numFillToRight numFillToTop numFillToBottom).each do |prop|
1672
+ expect(propdata[prop]).to eql 0.0
1673
+ end
1674
+ expect(propdata[:cGradStops]).to eq 2
1675
+
1676
+ check_gradstops_2_themed(propdata[:rgGradStops])
1677
+ end
1678
+
1679
+ it 'With XFExtGradient type 1 (rectangular), right-top aligned, 2 stops, themed color' do
1680
+ extprop = get_gradient_rgext(1, 7, 2)
1681
+
1682
+ check_type(extprop)
1683
+ expect(extprop[:cb]).to eq 96
1684
+
1685
+ propdata = extprop[:extPropData]
1686
+ check_rectangular_grad_props(propdata)
1687
+ %i(numFillToLeft numFillToRight).each { |prop| expect(propdata[prop]).to eql 1.0 }
1688
+ %i(numFillToTop numFillToBottom).each { |prop| expect(propdata[prop]).to eql 0.0 }
1689
+ expect(propdata[:cGradStops]).to eq 2
1690
+
1691
+ check_gradstops_2_themed(propdata[:rgGradStops])
1692
+ end
1693
+
1694
+ it 'With XFExtGradient type 1 (rectangular), right-bottom aligned, 2 stops, themed color' do
1695
+ extprop = get_gradient_rgext(1, 8, 2)
1696
+
1697
+ check_type(extprop)
1698
+ expect(extprop[:cb]).to eq 96
1699
+
1700
+ propdata = extprop[:extPropData]
1701
+ check_rectangular_grad_props(propdata)
1702
+ %i(numFillToLeft numFillToRight numFillToTop numFillToBottom).each do |prop|
1703
+ expect(propdata[prop]).to eql 1.0
1704
+ end
1705
+ expect(propdata[:cGradStops]).to eq 2
1706
+
1707
+ check_gradstops_2_themed(propdata[:rgGradStops])
1708
+ end
1709
+
1710
+ it 'With XFExtGradient type 1 (rectangular), centered, 2 stops, themed color' do
1711
+ extprop = get_gradient_rgext(1, 9, 2)
1712
+
1713
+ check_type(extprop)
1714
+ expect(extprop[:cb]).to eq 96
1715
+
1716
+ propdata = extprop[:extPropData]
1717
+ check_rectangular_grad_props(propdata)
1718
+ %i(numFillToLeft numFillToRight numFillToTop numFillToBottom).each do |prop|
1719
+ expect(propdata[prop]).to eql 0.5
1720
+ end
1721
+ expect(propdata[:cGradStops]).to eq 2
1722
+
1723
+ check_gradstops_2_themed(propdata[:rgGradStops])
1724
+ end
1725
+
1726
+ it 'With XFExtGradient type 0 (linear), 0 deg, 2 stops, custom colors' do
1727
+ extprop = get_gradient_rgext(1, 10, 2)
1728
+
1729
+ check_type(extprop)
1730
+ expect(extprop[:cb]).to eq 96
1731
+
1732
+ propdata = extprop[:extPropData]
1733
+ check_linear_grad_props(propdata)
1734
+ expect(propdata[:numDegree]).to eql 0.0
1735
+ expect(propdata[:cGradStops]).to eq 2
1736
+
1737
+ gradstops = propdata[:rgGradStops]
1738
+ expect(gradstops[0][:xclrType]).to eq 3
1739
+ expect(gradstops[0][:xclrType_d]).to eq :XCLRTHEMED
1740
+ expect(gradstops[0][:xclrValue]).to eq 8
1741
+ expect(gradstops[0][:numPosition]).to eql 0.0
1742
+ expect(gradstops[0][:numTint]).to eql 0.4
1743
+
1744
+ expect(gradstops[1][:xclrType]).to eq 2
1745
+ expect(gradstops[1][:xclrType_d]).to eq :XCLRRGB
1746
+ expect(gradstops[1][:xclrValue]).to eq :'2A8EF2FF'
1747
+ expect(gradstops[1][:numPosition]).to eql 1.0
1748
+ expect(gradstops[1][:numTint]).to eql 0.0
1749
+ end
1750
+ end
1751
+
1752
+ let(:border_colors) do
1753
+ [
1754
+ { row: 3, propdata: { xclrType: 2, xclrType_d: :XCLRRGB, nTintShade_d: 0.0, xclrValue: :'2A8EF2FF' } },
1755
+ { row: 4, propdata: { xclrType: 3, xclrType_d: :XCLRTHEMED, nTintShade_d: 0.4, xclrValue: 8 } },
1756
+ ]
1757
+ end
1758
+
1759
+ [
1760
+ { column: 3, type: 0x07, property: :'top cell border color' },
1761
+ { column: 4, type: 0x08, property: :'bottom cell border color' },
1762
+ { column: 5, type: 0x09, property: :'left cell border color' },
1763
+ { column: 6, type: 0x0A, property: :'right cell border color' },
1764
+ { column: 7, type: 0x0B, property: :'diagonal cell border color' },
1765
+ ].each do |attrs|
1766
+ it "ExtProp type #{attrs[:type]} (FullColorExt) for #{attrs[:property]}" do
1767
+ border_colors.each do |bc|
1768
+ extprop = @browser.get_xfext(1, bc[:row], attrs[:column])[:rgExt].find do |h|
1769
+ h[:_property] == attrs[:property]
1770
+ end
1771
+
1772
+ expect(extprop[:extType]).to eq(attrs[:type])
1773
+ expect(extprop[:extType_d]).to eq(:FullColorExt)
1774
+
1775
+ # nTintShade written by Excel seems to differ a bit for exactly same colors
1776
+ propdata = extprop[:extPropData]
1777
+ case extprop[:extPropData][:xclrType_d]
1778
+ when :XCLRRGB then expect(propdata[:nTintShade]).to eq 0
1779
+ else expect(propdata[:nTintShade]).to be_within(2).of(13103) # :XCLRTHEMED
1780
+ end
1781
+ propdata.delete(:nTintShade)
1782
+
1783
+ expect(propdata).to eq(bc[:propdata])
1784
+ end
1785
+ end
1786
+ end
1787
+
1788
+ context 'ExtProp type 0x0D (FullColorExt) for cell text color' do
1789
+ it 'With xclrType 2 (XCLRRGB)' do
1790
+ extprop = @browser.get_xfext(1, 3, 8)[:rgExt].find { |h| h[:_property] == :'cell text color' }
1791
+ propdata = extprop[:extPropData]
1792
+ expect(propdata[:xclrType_d]).to eq :XCLRRGB
1793
+ expect(propdata[:xclrValue]).to eq :'2A8EF2FF'
1794
+ end
1795
+
1796
+ it 'With xclrType 3 (XCLRTHEMED)' do
1797
+ extprop = @browser.get_xfext(1, 4, 8)[:rgExt].find { |h| h[:_property] == :'cell text color' }
1798
+ propdata = extprop[:extPropData]
1799
+ expect(propdata[:xclrType_d]).to eq :XCLRTHEMED
1800
+ expect(propdata[:nTintShade_d]).to eq 0.4
1801
+ expect(propdata[:xclrValue]).to eq 8
1802
+ end
1803
+ end
1804
+
1805
+ it 'Type 0x0E (FontScheme) for font scheme' do
1806
+ xfext = @browser.globals[:XFExt][0]
1807
+ extprop = xfext[:rgExt].find { |h| h[:_property] == :'font scheme' }
1808
+ expect(extprop[:extType]).to eq 14
1809
+ expect(extprop[:extType_d]).to eq :FontScheme
1810
+ expect(extprop[:cb]).to eq 5
1811
+ expect(extprop[:extPropData][:FontScheme]).to eq 2
1812
+ expect(extprop[:extPropData][:FontScheme_d]).to eq :XFSMINOR
1813
+ end
1814
+
1815
+ it 'Type 0x0F (_int1b) for text indentation level' do
1816
+ extprop = @browser.get_xfext(1, 3, 10)[:rgExt].find { |h| h[:_property] == :'text indentation level' }
1817
+ expect(extprop[:extType]).to eq 15
1818
+ expect(extprop[:extType_d]).to eq :_int1b
1819
+ expect(extprop[:cb]).to eq 6
1820
+ expect(extprop[:extPropData]).to eq 16
1821
+ end
1822
+ end
1823
+ end
1824
+ end
1825
+
1826
+ context 'Workbook stream, Worksheet substream' do
1827
+ context 'Various single records' do
1828
+ before(:context) do
1829
+ @browser1 = Unxls::Biff8::Browser.new(testfile('biff8', 'empty.xls'))
1830
+ @browser2 = Unxls::Biff8::Browser.new(testfile('biff8', 'preferences.xls'))
1831
+ end
1832
+
1833
+ it 'Decodes WsBool record' do
1834
+ wsb1 = @browser1.wbs[1][:WsBool] # empty.xls / Sheet1
1835
+ expect(wsb1[:fShowAutoBreaks]).to eq true # Defaults
1836
+ expect(wsb1[:fDialog]).to eq false
1837
+ expect(wsb1[:fApplyStyles]).to eq false
1838
+ expect(wsb1[:fRowSumsBelow]).to eq true
1839
+ expect(wsb1[:fColSumsRight]).to eq true
1840
+ expect(wsb1[:fFitToPage]).to eq false
1841
+ expect(wsb1[:fSyncHoriz]).to eq false
1842
+ expect(wsb1[:fSyncVert]).to eq false
1843
+ expect(wsb1[:fAltExprEval]).to eq false
1844
+ expect(wsb1[:fFormulaEntry]).to eq false
1845
+
1846
+ wsb2 = @browser2.wbs[1][:WsBool] # preferences.xls / Worksheet
1847
+ expect(wsb2[:fShowAutoBreaks]).to eq false # Options - View - Window Options - Page breaks
1848
+ expect(wsb2[:fDialog]).to eq false # Sheet is a Dialog sheet
1849
+ expect(wsb2[:fApplyStyles]).to eq true # Data - Group and outline - Settings - Automatic styles
1850
+ expect(wsb2[:fRowSumsBelow]).to eq false # Data - Group and outline - Settings - Summary rows below detail
1851
+ expect(wsb2[:fColSumsRight]).to eq false # Data - Group and outline - Settings - Summary columns to right of detail
1852
+ expect(wsb2[:fFitToPage]).to eq true # Options - International - Printing - Allow A4/Letter paper resizing
1853
+ expect(wsb2[:fSyncHoriz]).to eq true # To set the attributes add and run this subroutine for the 'Worksheet' sheet:
1854
+ expect(wsb2[:fSyncVert]).to eq true # Sub Main(); Windows.Arrange xlArrangeStyleVertical, True, True, True; End Sub;
1855
+ expect(wsb2[:fAltExprEval]).to eq true # Options - Transition - Sheet options - Transition formula evaluation
1856
+ expect(wsb2[:fFormulaEntry]).to eq true # Options - Transition - Sheet options - Transition formula entry
1857
+
1858
+ wsb3 = @browser2.wbs[2][:WsBool] # preferences.xls / Dialog
1859
+ expect(wsb3[:fDialog]).to eq true
1860
+ end
1861
+
1862
+ context 'PhoneticInfo' do
1863
+ let(:record) { @browser2.wbs[1][:PhoneticInfo] }
1864
+
1865
+ it 'Decodes phs field' do
1866
+ phs = record[:phs]
1867
+ font = @browser2.get_font(phs[:ifnt])
1868
+ expect(font[:fontName]).to eq 'Hiragino Kaku Gothic Pro W3'
1869
+ expect(phs[:phType]).to eq 0
1870
+ expect(phs[:phType_d]).to eq :narrow_katakana
1871
+ expect(phs[:alcH]).to eq 1
1872
+ expect(phs[:alcH_d]).to eq :left
1873
+ end
1874
+
1875
+ it 'Decodes sqref field' do
1876
+ sqref = record[:sqref]
1877
+ expect(sqref[:cref]).to eq(2)
1878
+
1879
+ rgrefs = sqref[:rgrefs]
1880
+ expect(rgrefs[0][:rwFirst]).to eq 2
1881
+ expect(rgrefs[0][:rwLast]).to eq 3
1882
+ expect(rgrefs[0][:colFirst]).to eq 1
1883
+ expect(rgrefs[0][:colLast]).to eq 1
1884
+ expect(rgrefs[1][:rwFirst]).to eq 5
1885
+ expect(rgrefs[1][:rwLast]).to eq 5
1886
+ expect(rgrefs[1][:colFirst]).to eq 1
1887
+ expect(rgrefs[1][:colLast]).to eq 1
1888
+ end
1889
+
1890
+ end
1891
+ end
1892
+
1893
+ context 'CELLTABLE-related records' do
1894
+ before(:context) do
1895
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'cells.xls'))
1896
+ end
1897
+
1898
+ it 'Decodes Blank record' do
1899
+ record = @browser.get_cell(1, 4, 0)
1900
+ expect(record[:rw]).to eq 4
1901
+ expect(record[:col]).to eq 0
1902
+ expect(record[:ixfe]).to be_an_kind_of Integer
1903
+ expect(@browser.globals[:XF][record[:ixfe]][:fls_d]).to eq :FLSGRAY125
1904
+ end
1905
+
1906
+ context 'BoolErr record' do
1907
+ it 'Decodes basic fields' do
1908
+ record = @browser.get_cell(1, 25, 0)
1909
+
1910
+ expect(record[:rw]).to eq 25
1911
+ expect(record[:col]).to eq 0
1912
+ expect(record[:ixfe]).to be_an_kind_of Integer
1913
+ end
1914
+
1915
+ [
1916
+ { fError: 0, row: 25, col: 0, bBoolErr: 0x01, bBoolErr_d: :True },
1917
+ { fError: 0, row: 25, col: 1, bBoolErr: 0x00, bBoolErr_d: :False },
1918
+ { fError: 1, row: 29, col: 0, bBoolErr: 0x00, bBoolErr_d: :'#NULL!' },
1919
+ { fError: 1, row: 29, col: 1, bBoolErr: 0x07, bBoolErr_d: :'#DIV/0!' },
1920
+ { fError: 1, row: 29, col: 2, bBoolErr: 0x0F, bBoolErr_d: :'#VALUE!' },
1921
+ { fError: 1, row: 29, col: 3, bBoolErr: 0x17, bBoolErr_d: :'#REF!' },
1922
+ { fError: 1, row: 29, col: 4, bBoolErr: 0x1D, bBoolErr_d: :'#NAME?' },
1923
+ { fError: 1, row: 29, col: 5, bBoolErr: 0x24, bBoolErr_d: :'#NUM!' },
1924
+ { fError: 1, row: 29, col: 6, bBoolErr: 0x2A, bBoolErr_d: :'#N/A' },
1925
+ # { fError: 1, row: 29, col: 7, bBoolErr: 0x2B, bBoolErr_d: :'#GETTING_DATA' }, # not a permanent error
1926
+ ].each do |params|
1927
+ it "Decodes BoolErr containing #{params[:bBoolErr_d]}" do
1928
+ record = @browser.get_cell(1, params[:row], params[:col])
1929
+
1930
+ expect(record[:bes][:bBoolErr]).to eq params[:bBoolErr]
1931
+ expect(record[:bes][:bBoolErr_d]).to eq params[:bBoolErr_d]
1932
+ expect(record[:bes][:fError]).to eq params[:fError]
1933
+ end
1934
+ end
1935
+ end
1936
+
1937
+ context 'Formula record' do
1938
+ it 'Decodes basic fields' do
1939
+ record = @browser.get_cell(1, 33, 0)
1940
+
1941
+ expect(record[:rw]).to eq 33
1942
+ expect(record[:col]).to eq 0
1943
+ expect(record[:ixfe]).to be_an_kind_of Integer
1944
+ expect(record[:fAlwaysCalc]).to eq false
1945
+ expect(record[:fFill]).to eq false
1946
+ expect(record[:fShrFmla]).to eq false
1947
+ expect(record[:fClearErrors]).to eq false
1948
+ expect(record[:chn]).to eq :not_implemented
1949
+ expect(record[:formula]).to eq :not_implemented
1950
+ end
1951
+
1952
+ it 'Decodes Formula that returns a string' do
1953
+ record = @browser.get_cell(1, 33, 0)
1954
+ expect(record[:_value]).to eq nil
1955
+ expect(record[:_type]).to eq :string
1956
+
1957
+ value = @browser.get_cell(1, 33, 0, record_type: :String) # See 2.5.133 FormulaValue
1958
+ expect(value[:string]).to eq 'Text'
1959
+ end
1960
+
1961
+ it 'Decodes Formula that returns a float' do
1962
+ record = @browser.get_cell(1, 33, 1)
1963
+ expect(record[:_value]).to eq 2.0
1964
+ expect(record[:_type]).to eq :float
1965
+ end
1966
+
1967
+ it 'Decodes Formula that returns a boolean' do
1968
+ record = @browser.get_cell(1, 33, 2)
1969
+ expect(record[:_value]).to eq true
1970
+ expect(record[:_type]).to eq :boolean
1971
+ end
1972
+
1973
+ it 'Decodes Formula that returns an error' do
1974
+ record = @browser.get_cell(1, 33, 3)
1975
+ expect(record[:_value]).to eq :'N/A'
1976
+ expect(record[:_type]).to eq :error
1977
+ end
1978
+
1979
+ it 'Decodes Formula that returns an empty string' do
1980
+ record = @browser.get_cell(1, 33, 4)
1981
+ expect(record[:_value]).to eq ''
1982
+ expect(record[:_type]).to eq :blank_string
1983
+ end
1984
+ end
1985
+
1986
+ it 'Decodes LabelSst record' do
1987
+ [
1988
+ 'Shared',
1989
+ 'Text',
1990
+ ' Text ',
1991
+ "Line 1\nLine 2",
1992
+ " Line 1 \n Line 2 "
1993
+ ].each_with_index do |text, column|
1994
+ record = @browser.get_cell(1, 21, column)
1995
+ expect(record[:rw]).to eq 21
1996
+ expect(record[:col]).to eq column
1997
+ expect(record[:ixfe]).to be_an_kind_of Integer
1998
+
1999
+ expect(record[:isst]).to be_an_kind_of Integer
2000
+ sst = @browser.globals[:SST][:rgb][record[:isst]]
2001
+ expect(sst[:rgb]).to eq text
2002
+ end
2003
+ end
2004
+
2005
+ it 'Decodes MulBlank record' do
2006
+ record = @browser.get_cell(1, 0, 1)
2007
+
2008
+ expect(record[:rw]).to eq 0
2009
+ expect(record[:colFirst]).to eq 1
2010
+ expect(record[:colLast]).to eq 7
2011
+ expect(record[:rgixfe]).to be_an_instance_of Array
2012
+ expect(record[:rgixfe].size).to eq 7
2013
+ expect(record[:rgixfe].first).to eq 68
2014
+ end
2015
+
2016
+ it 'Decodes MulRk record' do
2017
+ record = @browser.get_cell(1, 16, 1)
2018
+
2019
+ expect(record[:rw]).to eq 16
2020
+ expect(record[:colFirst]).to eq 1
2021
+ expect(record[:colLast]).to eq 5
2022
+ expect(record[:rgrkrec]).to be_an_instance_of Array
2023
+ expect(record[:rgrkrec].size).to eq 5
2024
+
2025
+ [1.0, -1.0, 0.01, -0.01, 0.0,].each_with_index do |rk, index|
2026
+ rkrec = record[:rgrkrec][index]
2027
+ expect(rkrec[:ixfe]).to eq 15
2028
+ expect(rkrec[:RK]).to eq rk
2029
+ end
2030
+ end
2031
+
2032
+ it 'Decodes Number record' do
2033
+ [
2034
+ { col: 1, num: 1.2345 },
2035
+ { col: 2, num: -1.2345 },
2036
+ ].each do |params|
2037
+ record = @browser.get_cell(1, 7, params[:col])
2038
+
2039
+ expect(record[:rw]).to eq 7
2040
+ expect(record[:col]).to eq params[:col]
2041
+ expect(record[:ixfe]).to eq 15
2042
+ expect(record[:num]).to eq params[:num]
2043
+ end
2044
+ end
2045
+
2046
+ it 'Decodes RK record' do
2047
+ [
2048
+ { col: 1, RK: 1.0 },
2049
+ { col: 3, RK: -1.0 },
2050
+ { col: 5, RK: 0.01 },
2051
+ { col: 7, RK: -0.01 },
2052
+ ].each do |params|
2053
+ record = @browser.get_cell(1, 10, params[:col])
2054
+
2055
+ expect(record[:rw]).to eq 10
2056
+ expect(record[:col]).to eq params[:col]
2057
+ expect(record[:ixfe]).to eq 15
2058
+ expect(record[:RK]).to eq params[:RK]
2059
+ end
2060
+ end
2061
+ end
2062
+
2063
+ context 'Row, ColInfo, Dimensions, MergeCells records' do
2064
+ before(:context) do
2065
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'row-colinfo-dimensions-mergecells.xls'))
2066
+ end
2067
+
2068
+ context 'ColInfo record' do
2069
+ let(:default_width) { 2742 } # 10 points
2070
+ let(:default_ixfe) { 15 }
2071
+ let(:sheet_index) { 2 }
2072
+
2073
+ it 'Decodes for column with custom width' do
2074
+ colinfo = @browser.get_colinfo(sheet_index, 1)
2075
+
2076
+ expect(colinfo[:colFirst]).to eq 1
2077
+ expect(colinfo[:colLast]).to eq 1
2078
+ expect(colinfo[:coldx]).to eq 2486 # custom width (9 points)
2079
+ expect(colinfo[:ixfe]).to eq default_ixfe
2080
+ expect(colinfo[:fHidden]).to eq false
2081
+ expect(colinfo[:fUserSet]).to eq true
2082
+ expect(colinfo[:fBestFit]).to eq false
2083
+ expect(colinfo[:fPhonetic]).to eq false
2084
+ expect(colinfo[:iOutLevel]).to eq 0
2085
+ expect(colinfo[:fCollapsed]).to eq false
2086
+ end
2087
+
2088
+ it 'Decodes for column with custom styling' do
2089
+ colinfo = @browser.get_colinfo(sheet_index, 2)
2090
+ expect(colinfo[:colFirst]).to eq 2
2091
+ expect(colinfo[:colLast]).to eq 2
2092
+ expect(colinfo[:coldx]).to eq default_width
2093
+
2094
+ expect(colinfo[:ixfe]).to be_an_kind_of Integer # custom XF
2095
+ xfext = @browser.globals[:XFExt].find { |r| r[:ixfe] == colinfo[:ixfe] }
2096
+ extprop = xfext[:rgExt].find { |x| x[:_property] == :'cell interior foreground color' }
2097
+ expect(extprop[:extPropData][:xclrValue]).to eq :'2A8EF2FF'
2098
+
2099
+ expect(colinfo[:fHidden]).to eq false
2100
+ expect(colinfo[:fUserSet]).to eq false
2101
+ expect(colinfo[:fBestFit]).to eq false
2102
+ expect(colinfo[:fPhonetic]).to eq false
2103
+ expect(colinfo[:iOutLevel]).to eq 0
2104
+ expect(colinfo[:fCollapsed]).to eq false
2105
+ end
2106
+
2107
+ it 'Decodes for hidden column' do
2108
+ colinfo = @browser.get_colinfo(sheet_index, 3)
2109
+ expect(colinfo[:colFirst]).to eq 3
2110
+ expect(colinfo[:colLast]).to eq 3
2111
+ expect(colinfo[:coldx]).to eq 0
2112
+ expect(colinfo[:ixfe]).to eq default_ixfe
2113
+ expect(colinfo[:fHidden]).to eq true
2114
+ expect(colinfo[:fUserSet]).to eq true
2115
+ expect(colinfo[:fBestFit]).to eq false
2116
+ expect(colinfo[:fPhonetic]).to eq false
2117
+ expect(colinfo[:iOutLevel]).to eq 0
2118
+ expect(colinfo[:fCollapsed]).to eq false
2119
+ end
2120
+
2121
+ it 'Decodes for column with AutoFit Selection' do
2122
+ colinfo = @browser.get_colinfo(sheet_index, 4)
2123
+ expect(colinfo[:colFirst]).to eq 4
2124
+ expect(colinfo[:colLast]).to eq 4
2125
+ expect(colinfo[:coldx]).to eq 3254
2126
+ expect(colinfo[:ixfe]).to eq default_ixfe
2127
+ expect(colinfo[:fHidden]).to eq false
2128
+ expect(colinfo[:fUserSet]).to eq true
2129
+ expect(colinfo[:fBestFit]).to eq true
2130
+ expect(colinfo[:fPhonetic]).to eq false
2131
+ expect(colinfo[:iOutLevel]).to eq 0
2132
+ expect(colinfo[:fCollapsed]).to eq false
2133
+ end
2134
+
2135
+ it 'Decodes for column with phonetic fields displayed' do
2136
+ colinfo = @browser.get_colinfo(sheet_index, 5)
2137
+ expect(colinfo[:colFirst]).to eq 5
2138
+ expect(colinfo[:colLast]).to eq 5
2139
+ expect(colinfo[:coldx]).to eq default_width
2140
+ expect(colinfo[:ixfe]).to eq default_ixfe
2141
+ expect(colinfo[:fHidden]).to eq false
2142
+ expect(colinfo[:fUserSet]).to eq false
2143
+ expect(colinfo[:fBestFit]).to eq false
2144
+ expect(colinfo[:fPhonetic]).to eq true
2145
+ expect(colinfo[:iOutLevel]).to eq 0
2146
+ expect(colinfo[:fCollapsed]).to eq false
2147
+ end
2148
+
2149
+ # Columns G and H are actually collapsed, but the flag fCollapsed is not set,
2150
+ # whereas flags fHidden and fUserSet *are* set.
2151
+ # Seems like flag fCollapsed is only set to the columns
2152
+ # that follow a set of collapsed columns.
2153
+ it 'Decodes for collapsed columns' do
2154
+ # Column G, inner outline, collapsed
2155
+ colinfo = @browser.get_colinfo(sheet_index, 6)
2156
+ expect(colinfo[:colFirst]).to eq 6
2157
+ expect(colinfo[:colLast]).to eq 6
2158
+ expect(colinfo[:coldx]).to eq default_width
2159
+ expect(colinfo[:ixfe]).to eq default_ixfe
2160
+ expect(colinfo[:fHidden]).to eq true
2161
+ expect(colinfo[:fUserSet]).to eq true
2162
+ expect(colinfo[:fBestFit]).to eq false
2163
+ expect(colinfo[:fPhonetic]).to eq false
2164
+ expect(colinfo[:iOutLevel]).to eq 2
2165
+ expect(colinfo[:fCollapsed]).to eq false
2166
+
2167
+ # Column G, outer outline, collapsed
2168
+ colinfo = @browser.get_colinfo(sheet_index, 7)
2169
+ expect(colinfo[:colFirst]).to eq 7
2170
+ expect(colinfo[:colLast]).to eq 7
2171
+ expect(colinfo[:coldx]).to eq default_width
2172
+ expect(colinfo[:ixfe]).to eq default_ixfe
2173
+ expect(colinfo[:fHidden]).to eq true
2174
+ expect(colinfo[:fUserSet]).to eq true
2175
+ expect(colinfo[:fBestFit]).to eq false
2176
+ expect(colinfo[:fPhonetic]).to eq false
2177
+ expect(colinfo[:iOutLevel]).to eq 1
2178
+ expect(colinfo[:fCollapsed]).to eq true
2179
+
2180
+ # Column I with default settings, directly follows collapsed column G.
2181
+ colinfo = @browser.get_colinfo(sheet_index, 8)
2182
+ expect(colinfo[:colFirst]).to eq 8
2183
+ expect(colinfo[:colLast]).to eq 8
2184
+ expect(colinfo[:coldx]).to eq default_width
2185
+ expect(colinfo[:ixfe]).to eq default_ixfe
2186
+ expect(colinfo[:fHidden]).to eq false
2187
+ expect(colinfo[:fUserSet]).to eq false
2188
+ expect(colinfo[:fBestFit]).to eq false
2189
+ expect(colinfo[:fPhonetic]).to eq false
2190
+ expect(colinfo[:iOutLevel]).to eq 0
2191
+ expect(colinfo[:fCollapsed]).to eq true
2192
+ end
2193
+ end
2194
+
2195
+ context 'Row record' do
2196
+ let(:sheet_index) { 1 }
2197
+
2198
+ specify 'Row record not written if row has no content' do
2199
+ expect(@browser.get_row(sheet_index, 2)).to eq nil
2200
+ end
2201
+
2202
+ # colMic and colMac are the same for row groups of 16 records, e.g. rows 0-15, then 16-31 etc.
2203
+ # So if row 0 has cells in cols 1 and 3, and row 15 has cell in cols 5 and 10, both Row records
2204
+ # will have colMic = 1 and colMac = 10, but rows 16…31 will have their own "bounding box".
2205
+ it 'Decodes colMic and colMac fields' do
2206
+ (3..5).each do |row_index|
2207
+ row = @browser.get_row(sheet_index, row_index)
2208
+ expect(row[:colMic]).to eq 0
2209
+ expect(row[:colMac]).to eq 6
2210
+ end
2211
+ end
2212
+
2213
+ it 'Decodes miyRw and fUnsynced fields' do
2214
+ row = @browser.get_row(sheet_index, 7)
2215
+ expect(row[:miyRw]).to eq 400
2216
+ expect(row[:fUnsynced]).to eq false
2217
+
2218
+ row = @browser.get_row(sheet_index, 8)
2219
+ expect(row[:miyRw]).to eq 600
2220
+ expect(row[:fUnsynced]).to eq true
2221
+ end
2222
+
2223
+ it 'Decodes iOutLevel, fCollapsed and fDyZero fields' do
2224
+ [
2225
+ { row_index: 10, iOutLevel: 4, fCollapsed: false, fDyZero: true },
2226
+ { row_index: 11, iOutLevel: 3, fCollapsed: true, fDyZero: true },
2227
+ { row_index: 12, iOutLevel: 2, fCollapsed: false, fDyZero: true },
2228
+ { row_index: 13, iOutLevel: 1, fCollapsed: true, fDyZero: false },
2229
+ ].each do |params|
2230
+ row = @browser.get_row(sheet_index, params[:row_index])
2231
+ expect(row[:iOutLevel]).to eq params[:iOutLevel]
2232
+ expect(row[:fCollapsed]).to eq params[:fCollapsed]
2233
+ expect(row[:fDyZero]).to eq params[:fDyZero]
2234
+ end
2235
+
2236
+ expect(@browser.get_row(sheet_index, 15)[:fDyZero]).to eq true
2237
+ end
2238
+
2239
+ it 'Decodes fGhostDirty and ixfe fields' do
2240
+ row = @browser.get_row(sheet_index, 17)
2241
+ expect(row[:fGhostDirty]).to eq true
2242
+ expect(row[:ixfe]).to be_an_kind_of Integer
2243
+
2244
+ row_xfext = @browser.mapif(:XFExt, first: true) { |r| r if r[:ixfe] == row[:ixfe] }
2245
+ color_ext = row_xfext[:rgExt].find { |p| p[:_property] == :'cell interior foreground color' }
2246
+ expect(color_ext[:extPropData][:xclrValue]).to eq :'187CE0FF'
2247
+ end
2248
+
2249
+ it 'Decodes fExAsc field' do
2250
+ {
2251
+ 18 => false,
2252
+ 19 => false,
2253
+ 20 => true,
2254
+ 21 => false,
2255
+ 22 => true,
2256
+ 23 => false,
2257
+ 24 => true,
2258
+ 25 => false,
2259
+ 26 => true,
2260
+ }.each do |row_index, f_ex_asc|
2261
+ expect(@browser.get_row(sheet_index, row_index)[:fExAsc]).to eq f_ex_asc
2262
+ end
2263
+ end
2264
+
2265
+ it 'Decodes fExDes' do
2266
+ {
2267
+ 27 => false,
2268
+ 28 => true,
2269
+ 29 => true,
2270
+ 30 => false,
2271
+ 31 => true,
2272
+ 32 => true,
2273
+ 33 => false,
2274
+ 34 => true,
2275
+ 35 => true,
2276
+ 36 => false,
2277
+ 37 => true,
2278
+ 38 => true,
2279
+ 39 => false,
2280
+ 40 => true,
2281
+ 41 => true,
2282
+ 42 => false,
2283
+ 43 => true,
2284
+ 44 => true,
2285
+ 45 => false,
2286
+ 46 => true,
2287
+ 47 => true,
2288
+ 48 => false,
2289
+ }.each do |row_index, f_ex_des|
2290
+ expect(@browser.get_row(sheet_index, row_index)[:fExDes]).to eq f_ex_des
2291
+ end
2292
+ end
2293
+
2294
+ it 'Decodes fPhonetic field' do
2295
+ expect(@browser.get_row(sheet_index, 50)[:fPhonetic]).to eq false
2296
+ expect(@browser.get_row(sheet_index, 51)[:fPhonetic]).to eq true
2297
+ end
2298
+ end
2299
+
2300
+ it 'Decodes Dimensions record' do
2301
+ record = @browser.wbs[3][:Dimensions]
2302
+ expect(record[:rwMic]).to eq 1
2303
+ expect(record[:rwMac]).to eq 8
2304
+ expect(record[:colMic]).to eq 1
2305
+ expect(record[:colMac]).to eq 6
2306
+ end
2307
+
2308
+ it 'Decodes MergeCells record' do
2309
+ record = @browser.wbs[4][:MergeCells].first
2310
+ expect(record[:cmcs]).to eq 3
2311
+ expect(record[:rgref][0]).to eq({ rwFirst: 1, rwLast: 4, colFirst: 1, colLast: 1 })
2312
+ expect(record[:rgref][1]).to eq({ rwFirst: 1, rwLast: 1, colFirst: 3, colLast: 6 })
2313
+ expect(record[:rgref][2]).to eq({ rwFirst: 3, rwLast: 6, colFirst: 3, colLast: 6 })
2314
+ end
2315
+ end
2316
+
2317
+ context 'Note, Obj, TxO records' do
2318
+ before(:context) do
2319
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'note-obj-txo.xls'))
2320
+ end
2321
+
2322
+ context 'Note record' do
2323
+ specify 'For cell with normal comment' do
2324
+ record = @browser.get_note(1, 1, 0)
2325
+ expect(record[:rw]).to eq 1
2326
+ expect(record[:col]).to eq 0
2327
+ expect(record[:fShow]).to eq false
2328
+ expect(record[:fRwHidden]).to eq false
2329
+ expect(record[:fColHidden]).to eq false
2330
+ expect(record[:idObj]).to be_an_kind_of Integer
2331
+ expect(record[:stAuthor]).to eq 'Microsoft Office User'
2332
+ end
2333
+
2334
+ specify 'For cell with comment always shown' do
2335
+ record = @browser.get_note(1, 2, 0)
2336
+ expect(record[:rw]).to eq 2
2337
+ expect(record[:col]).to eq 0
2338
+ expect(record[:fShow]).to eq true
2339
+ expect(record[:fRwHidden]).to eq false
2340
+ expect(record[:fColHidden]).to eq false
2341
+ expect(record[:idObj]).to be_an_kind_of Integer
2342
+ expect(record[:stAuthor]).to eq 'Microsoft Office User'
2343
+ end
2344
+
2345
+ specify 'For cell in hidden row' do
2346
+ record = @browser.get_note(1, 3, 0)
2347
+ expect(record[:rw]).to eq 3
2348
+ expect(record[:col]).to eq 0
2349
+ expect(record[:fShow]).to eq false
2350
+ expect(record[:fRwHidden]).to eq true
2351
+ expect(record[:fColHidden]).to eq false
2352
+ expect(record[:idObj]).to be_an_kind_of Integer
2353
+ expect(record[:stAuthor]).to eq 'Microsoft Office User'
2354
+ end
2355
+
2356
+ specify 'For cell in hidden column' do
2357
+ record = @browser.get_note(1, 1, 1)
2358
+ expect(record[:rw]).to eq 1
2359
+ expect(record[:col]).to eq 1
2360
+ expect(record[:fShow]).to eq false
2361
+ expect(record[:fRwHidden]).to eq false
2362
+ expect(record[:fColHidden]).to eq true
2363
+ expect(record[:idObj]).to be_an_kind_of Integer
2364
+ expect(record[:stAuthor]).to eq 'Microsoft Office User'
2365
+ end
2366
+ end
2367
+
2368
+ context 'Obj record' do
2369
+ [
2370
+ [1, 0],
2371
+ [2, 0],
2372
+ [3, 0],
2373
+ [1, 1],
2374
+ ].each do |rc|
2375
+ it 'Decodes cmo field' do
2376
+ row, column = rc
2377
+ note_obj_id = @browser.get_note(1, row, column)[:idObj]
2378
+ record = @browser.get_obj(1, note_obj_id)
2379
+
2380
+ cmo = record[:cmo]
2381
+ expect(cmo[:ft]).to eq 0x15
2382
+ expect(cmo[:cb]).to eq 0x12
2383
+ expect(cmo[:ot]).to eq 0x19
2384
+ expect(cmo[:ot_d]).to eq :Note
2385
+ expect(cmo[:fLocked]).to eq true
2386
+ expect(cmo[:fPrint]).to eq true
2387
+ %i(fDefaultSize fPublished fDisabled fUIObj fRecalcObj fRecalcObjAlways).each do |field_name|
2388
+ expect(cmo[field_name]).to eq false
2389
+ end
2390
+
2391
+ expect(record[:_other_fields]).to eq :not_implemented
2392
+ end
2393
+ end
2394
+ end
2395
+
2396
+ context 'TxO record' do
2397
+ def get_note_txo(stream, row, col)
2398
+ note_obj_id = @browser.get_note(stream, row, col)[:idObj]
2399
+ note_obj_index = @browser.get_obj(stream, note_obj_id)[:_record][:index]
2400
+ @browser.wbs[stream][:TxO][note_obj_index]
2401
+ end
2402
+
2403
+ specify 'For note with default formatting' do
2404
+ record = get_note_txo(2, 1, 0)
2405
+ expect(record[:hAlignment]).to eq 1
2406
+ expect(record[:hAlignment_d]).to eq :left
2407
+ expect(record[:vAlignment]).to eq 1
2408
+ expect(record[:vAlignment_d]).to eq :top
2409
+ expect(record[:fLockText]).to eq true
2410
+ expect(record[:fJustLast]).to eq false
2411
+ expect(record[:fSecretEdit]).to eq false
2412
+ expect(record[:rot]).to eq 0
2413
+ expect(record[:rot_d]).to eq :none
2414
+ expect(record[:ifntEmpty]).to eq 0
2415
+ expect(record[:fmla]).to eq :not_implemented
2416
+ expect(record[:text_string]).to eq 'default style, halign left, valign top, rot none'
2417
+ expect(record[:text_string].size).to eq record[:cchText]
2418
+ expect(record[:formatting_runs].size).to eq 1
2419
+ expect(record[:formatting_runs].first[:ich]).to be_an_kind_of Integer
2420
+ expect(record[:formatting_runs].first[:ifnt]).to be_an_kind_of Integer
2421
+ end
2422
+
2423
+ specify 'For note with center-middle aligned, unlocked text' do
2424
+ record = get_note_txo(2, 3, 0)
2425
+ expect(record[:hAlignment]).to eq 2
2426
+ expect(record[:hAlignment_d]).to eq :centered
2427
+ expect(record[:vAlignment]).to eq 2
2428
+ expect(record[:vAlignment_d]).to eq :middle
2429
+ expect(record[:fLockText]).to eq false
2430
+ expect(record[:text_string]).to eq 'halign center, valign middle, lock text false'
2431
+ end
2432
+
2433
+ specify 'For note with right-bottom aligned text' do
2434
+ record = get_note_txo(2, 5, 0)
2435
+ expect(record[:hAlignment]).to eq 3
2436
+ expect(record[:hAlignment_d]).to eq :right
2437
+ expect(record[:vAlignment]).to eq 3
2438
+ expect(record[:vAlignment_d]).to eq :bottom
2439
+ expect(record[:text_string]).to eq 'halign right, valign bottom'
2440
+ end
2441
+
2442
+ specify 'For note with horizontally and vertically justified text' do
2443
+ record = get_note_txo(2, 7, 0)
2444
+ expect(record[:hAlignment]).to eq 4
2445
+ expect(record[:hAlignment_d]).to eq :justify
2446
+ expect(record[:vAlignment]).to eq 4
2447
+ expect(record[:vAlignment_d]).to eq :justify
2448
+ expect(record[:text_string]).to eq 'halign justify, valign justify'
2449
+ end
2450
+
2451
+ specify 'For note with horizontally and vertically distributed text' do
2452
+ record = get_note_txo(2, 9, 0)
2453
+ expect(record[:hAlignment]).to eq 7
2454
+ expect(record[:hAlignment_d]).to eq :justify_distributed
2455
+ expect(record[:vAlignment]).to eq 7
2456
+ expect(record[:vAlignment_d]).to eq :justify_distributed
2457
+ expect(record[:text_string]).to eq 'halign distributed, valign distributed'
2458
+ end
2459
+
2460
+ specify 'For note with stacked orientation' do
2461
+ record = get_note_txo(2, 1, 5)
2462
+ expect(record[:rot]).to eq 1
2463
+ expect(record[:rot_d]).to eq :stacked
2464
+ expect(record[:text_string]).to eq 'rot stacked'
2465
+ end
2466
+
2467
+ specify 'For note with text rotated 90 ccw' do
2468
+ record = get_note_txo(2, 3, 5)
2469
+ expect(record[:rot]).to eq 2
2470
+ expect(record[:rot_d]).to eq :ccw_90
2471
+ expect(record[:text_string]).to eq 'rot 90 ccw'
2472
+ end
2473
+
2474
+ specify 'For note with text rotated 90 cw' do
2475
+ record = get_note_txo(2, 5, 5)
2476
+ expect(record[:rot]).to eq 3
2477
+ expect(record[:rot_d]).to eq :cw_90
2478
+ expect(record[:text_string]).to eq 'rot 90 cw'
2479
+ end
2480
+
2481
+ specify 'For note with no text' do
2482
+ record = get_note_txo(2, 7, 5)
2483
+ expect(@browser.get_font(record[:ifntEmpty])[:fontName]).to eq 'Tahoma'
2484
+ end
2485
+
2486
+ specify 'For note with text split across several Continue records' do
2487
+ record = get_note_txo(2, 11, 0)
2488
+ expect(record[:text_string].size).to eq 16440 # characters
2489
+ expect(record[:cchText]).to eq 16445 # bytes. 5 extra bytes for 5 emojis
2490
+ expect(record[:text_string][4105..4107]).to eq 'Ж>W'
2491
+ expect(record[:text_string][12327..12329]).to eq 'W>Ж'
2492
+ expect(record[:text_string][-5..-1]).to eq 'Ж END'
2493
+ end
2494
+
2495
+ specify 'For note with text with formatting runs' do
2496
+ record = get_note_txo(2, 13, 0)
2497
+ expect(record[:formatting_runs].size).to eq 5
2498
+
2499
+ run = record[:formatting_runs][0]
2500
+ expect(run[:ich]).to eq 0
2501
+ expect(@browser.get_font(run[:ifnt])[:fontName]).to eq 'Candara'
2502
+
2503
+ run = record[:formatting_runs][1]
2504
+ expect(run[:ich]).to eq 23
2505
+ expect(@browser.get_font(run[:ifnt])[:fontName]).to eq 'Baskerville Old Face'
2506
+
2507
+ run = record[:formatting_runs][2]
2508
+ expect(run[:ich]).to eq 33
2509
+ expect(@browser.get_font(run[:ifnt])[:fontName]).to eq 'Candara'
2510
+
2511
+ run = record[:formatting_runs][3]
2512
+ expect(run[:ich]).to eq 34
2513
+ expect(@browser.get_font(run[:ifnt])[:fontName]).to eq 'Corbel'
2514
+
2515
+ run = record[:formatting_runs][4]
2516
+ expect(run[:ich]).to eq 38
2517
+ expect(@browser.get_font(run[:ifnt])[:fontName]).to eq 'Candara'
2518
+ end
2519
+ end
2520
+ end
2521
+
2522
+ context 'HLink, HLinkTooltip records' do
2523
+ before(:context) do
2524
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'hyperlinks.xls'))
2525
+ end
2526
+
2527
+ context 'HLink record' do
2528
+ it 'Decodes basic fields' do
2529
+ record = @browser.get_hlink(1, 16, 1)
2530
+
2531
+ expect(record[:rwFirst]).to eq 16
2532
+ expect(record[:rwLast]).to eq 16
2533
+ expect(record[:colFirst]).to eq 1
2534
+ expect(record[:colLast]).to eq 1
2535
+ expect(record[:hlinkClsid]).to eq :'79eac9d0-baf9-11ce-8c82-00aa004ba90b'
2536
+ end
2537
+
2538
+ it 'Decodes hyperlink field for an internal link' do
2539
+ field = @browser.get_hlink(1, 16, 1)[:hyperlink]
2540
+
2541
+ expect(field[:streamVersion]).to eq 2
2542
+
2543
+ expect(field[:hlstmfHasMoniker]).to eq false
2544
+ expect(field[:hlstmfIsAbsolute]).to eq false
2545
+ expect(field[:hlstmfSiteGaveDisplayName]).to eq true
2546
+ expect(field[:hlstmfHasLocationStr]).to eq true
2547
+ expect(field[:hlstmfHasDisplayName]).to eq true
2548
+ expect(field[:hlstmfHasGUID]).to eq false
2549
+ expect(field[:hlstmfHasCreationTime]).to eq false
2550
+ expect(field[:hlstmfHasFrameName]).to eq false
2551
+ expect(field[:hlstmfMonikerSavedAsStr]).to eq false
2552
+ expect(field[:hlstmfAbsFromGetdataRel]).to eq false
2553
+
2554
+ expect(field[:displayName]).to eq "#Sheet2!localname"
2555
+ expect(field[:location]).to eq "Sheet2!localname"
2556
+ end
2557
+
2558
+ it 'Decodes hyperlink field for local file link' do
2559
+ field = @browser.get_hlink(1, 27, 1)[:hyperlink]
2560
+
2561
+ expect(field[:hlstmfHasMoniker]).to eq true
2562
+ expect(field[:hlstmfIsAbsolute]).to eq false
2563
+ expect(field[:hlstmfSiteGaveDisplayName]).to eq true
2564
+ expect(field[:hlstmfHasLocationStr]).to eq true
2565
+ expect(field[:hlstmfHasDisplayName]).to eq true
2566
+ expect(field[:hlstmfHasGUID]).to eq false
2567
+ expect(field[:hlstmfHasCreationTime]).to eq false
2568
+ expect(field[:hlstmfHasFrameName]).to eq false
2569
+ expect(field[:hlstmfMonikerSavedAsStr]).to eq false
2570
+ expect(field[:hlstmfAbsFromGetdataRel]).to eq false
2571
+ expect(field[:displayName]).to eq 'long_unicode_filename_€.xls#A1'
2572
+ expect(field[:location]).to eq 'A1'
2573
+
2574
+ ole_moniker = field[:oleMoniker]
2575
+ expect(ole_moniker[:monikerClsid]).to eq :'00000303-0000-0000-c000-000000000046'
2576
+ expect(ole_moniker[:monikerClsid_d]).to eq :FileMoniker
2577
+
2578
+ moniker_data = ole_moniker[:data]
2579
+ expect(moniker_data[:cAnti]).to eq 0
2580
+ expect(moniker_data[:ansiLength]).to eq 28
2581
+ expect(moniker_data[:ansiPath]).to eq 'long_unicode_filename_?.xls'
2582
+ expect(moniker_data[:versionNumber]).to eq 57005
2583
+ expect(moniker_data[:cbUnicodePathSize]).to eq 60
2584
+ expect(moniker_data[:cbUnicodePathBytes]).to eq 54
2585
+ expect(moniker_data[:unicodePath]).to eq 'long_unicode_filename_€.xls'
2586
+ end
2587
+
2588
+ it 'Decodes hyperlink field for a UNC path link' do
2589
+ field = @browser.get_hlink(1, 37, 1)[:hyperlink]
2590
+
2591
+ expect(field[:hlstmfHasMoniker]).to eq true
2592
+ expect(field[:hlstmfIsAbsolute]).to eq true
2593
+ expect(field[:hlstmfSiteGaveDisplayName]).to eq true
2594
+ expect(field[:hlstmfHasLocationStr]).to eq false
2595
+ expect(field[:hlstmfHasDisplayName]).to eq true
2596
+ expect(field[:hlstmfHasGUID]).to eq false
2597
+ expect(field[:hlstmfHasCreationTime]).to eq false
2598
+ expect(field[:hlstmfHasFrameName]).to eq false
2599
+ expect(field[:hlstmfMonikerSavedAsStr]).to eq true
2600
+ expect(field[:hlstmfAbsFromGetdataRel]).to eq false
2601
+ expect(field[:displayName]).to eq "\\\\127.0.0.1\\PATH\\EXTP8_4.XLS"
2602
+ expect(field[:moniker]).to eq "\\\\127.0.0.1\\PATH\\EXTP8_4.XLS"
2603
+ end
2604
+
2605
+ it 'Decodes hyperlink field for a HTTP link'
2606
+ it 'Decodes hyperlink field for a mail link'
2607
+ end
2608
+
2609
+ context 'HLinkTooltip record'
2610
+ end
2611
+ end
2612
+
2613
+ context 'Optimizations' do
2614
+ before(:context) do
2615
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'row-colinfo-dimensions-mergecells.xls'))
2616
+ end
2617
+
2618
+ it 'Adds cell value dimensions' do
2619
+ expect(@browser.cell_index[:dimensions][3]).to eq({ rmin: 1, rmax: 5, cmin: 1, cmax: 4 })
2620
+ end
2621
+
2622
+ it 'Adds cell index for quick lookup' do
2623
+ expect(@browser.cell_index[:'3_5_4']).to eq :Formula_0
2624
+ end
2625
+
2626
+ context 'Note, HLink, HLinkTooltip' do
2627
+ before(:context) do
2628
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'note-obj-txo.xls'))
2629
+ end
2630
+
2631
+ it 'Adds Note index for quick lookup' do
2632
+ expect(@browser.cell_index[:notes][:'1_1_0']).to eq 0
2633
+ end
2634
+ end
2635
+
2636
+ context 'Note, HLink, HLinkTooltip' do
2637
+ before(:context) do
2638
+ @browser = Unxls::Biff8::Browser.new(testfile('biff8', 'hyperlinks.xls'))
2639
+ end
2640
+
2641
+ it 'Adds HLink index for quick lookup' do
2642
+ expect(@browser.cell_index[:hlinks][:'1_6_1']).to eq 0
2643
+ end
2644
+
2645
+ it 'Adds HLinkTooltip index for quick lookup' do
2646
+ expect(@browser.cell_index[:hlinktooltips][:'1_57_1']).to eq 0
2647
+ end
2648
+ end
2649
+ end
2650
+
2651
+ end