unxls 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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