Failed Conditions
Push — master ( 92389c...96e843 )
by Adrien
26:46 queued 17:47
created

Xls   F

Complexity

Total Complexity 1135

Size/Duplication

Total Lines 7903
Duplicated Lines 0 %

Test Coverage

Coverage 53.35%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 3994
c 2
b 0
f 0
dl 0
loc 7903
ccs 2101
cts 3938
cp 0.5335
rs 0.8
wmc 1135

112 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A canRead() 0 14 2
A readScenProtect() 0 18 2
A readObjectProtect() 0 18 2
A readProtect() 0 17 2
A readPassword() 0 12 2
A readDefColWidth() 0 12 2
A readSharedFmla() 0 22 1
A readString() 0 17 2
A readDefault() 0 6 1
F readDocumentSummaryInformation() 0 131 27
A getUInt2d() 0 3 1
A readScl() 0 16 1
A readObj() 0 35 3
F readXf() 0 294 48
F load() 0 671 135
A readRow() 0 54 5
B verifyPassword() 0 73 8
A readPrintGridlines() 0 12 3
B readColInfo() 0 45 6
A readCodepage() 0 12 1
F readSst() 0 172 23
A readDateMode() 0 12 2
A readStyle() 0 28 4
A readPalette() 0 16 3
A readDefinedName() 0 47 3
F readFormula() 0 124 25
C listWorksheetInfo() 0 103 16
A readLeftMargin() 0 11 2
B readBof() 0 33 7
A setCodepage() 0 7 2
B readRecordData() 0 45 9
A readMsoDrawing() 0 9 1
A readExternName() 0 27 2
A readMulRk() 0 42 6
A readBlank() 0 23 6
A readHeader() 0 20 4
A loadOLE() 0 12 1
A readDefaultRowHeight() 0 12 1
A readFormat() 0 20 3
B readLabel() 0 36 8
A readExternSheet() 0 20 3
F readSummaryInformation() 0 140 28
A makeKey() 0 22 2
B readExternalBook() 0 55 6
A readHcenter() 0 13 2
A readFooter() 0 19 4
B readMulBlank() 0 25 7
A readVerticalPageBreaks() 0 20 4
A readPageSetup() 0 51 5
A readTopMargin() 0 11 2
A readBottomMargin() 0 11 2
D readFont() 0 91 13
A readSheetPr() 0 21 1
A readNumber() 0 30 5
B readSheet() 0 49 6
A readNote() 0 45 5
A readRk() 0 32 5
A readFilepass() 0 21 3
B readBoolErr() 0 47 7
D readXfExt() 0 169 27
D readLabelSst() 0 72 18
A readTextObject() 0 41 3
B listWorksheetNames() 0 49 7
A readMsoDrawingGroup() 0 9 1
A readVcenter() 0 13 2
A readHorizontalPageBreaks() 0 20 4
A readRightMargin() 0 11 2
A getSplicedRecordData() 0 26 2
A readByteStringLong() 0 12 1
A readContinue() 0 41 4
A readBIFF8CellRangeAddress() 0 42 5
A getFormulaFromStructure() 0 16 2
A readUnicodeStringLong() 0 13 1
A uncompressByteString() 0 9 2
F readHyperLink() 0 166 20
A getIEEE754() 0 24 4
A readRGB() 0 15 1
A includeCellRangeFiltered() 0 19 5
A readPane() 0 26 3
A readUnicodeStringShort() 0 13 1
A encodeUTF16() 0 7 2
A extractNumber() 0 21 3
A readBIFF5CellRangeAddressFixed() 0 28 5
B readBIFF8CellAddressB() 0 36 6
B readBIFF8CellRangeAddressB() 0 75 11
A readBIFF8CellRangeAddressList() 0 17 2
F createFormulaFromTokens() 0 165 57
F readDataValidation() 0 205 30
A readDataValidations() 0 7 1
A getFormulaFromData() 0 13 3
C readWindow2() 0 75 11
A readBIFF8CellAddress() 0 19 3
A readSheetProtection() 0 91 3
A readSheetLayout() 0 33 4
A UTF8toExcelDoubleQuoted() 0 3 1
A readBIFF5CellRangeAddressList() 0 17 2
B readBIFF8Constant() 0 45 7
F getNextToken() 0 1580 333
A readPageLayoutView() 0 30 2
A decodeCodepage() 0 3 1
A parseRichText() 0 6 1
A readUnicodeString() 0 22 3
A readMergedCells() 0 16 6
A readBIFF8ConstantArray() 0 27 3
A getInt2d() 0 3 1
A getInt4d() 0 14 2
B readSheetRangeByRefIndex() 0 46 7
A readBIFF8CellRangeAddressFixed() 0 28 5
A readByteStringShort() 0 11 1
A readSelection() 0 44 5
B readRangeProtection() 0 53 6

How to fix   Complexity   

Complex Class

Complex classes like Xls often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Xls, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6
use PhpOffice\PhpSpreadsheet\Cell\DataType;
7
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
8
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
9
use PhpOffice\PhpSpreadsheet\NamedRange;
10
use PhpOffice\PhpSpreadsheet\RichText\RichText;
11
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
12
use PhpOffice\PhpSpreadsheet\Shared\Date;
13
use PhpOffice\PhpSpreadsheet\Shared\Escher;
14
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
15
use PhpOffice\PhpSpreadsheet\Shared\File;
16
use PhpOffice\PhpSpreadsheet\Shared\OLE;
17
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
18
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
19
use PhpOffice\PhpSpreadsheet\Spreadsheet;
20
use PhpOffice\PhpSpreadsheet\Style\Alignment;
21
use PhpOffice\PhpSpreadsheet\Style\Borders;
22
use PhpOffice\PhpSpreadsheet\Style\Font;
23
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
24
use PhpOffice\PhpSpreadsheet\Style\Protection;
25
use PhpOffice\PhpSpreadsheet\Style\Style;
26
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
27
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
28
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
29
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
30
31
// Original file header of ParseXL (used as the base for this class):
32
// --------------------------------------------------------------------------------
33
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
34
// trex005, and mmp11 (SourceForge.net)
35
// https://sourceforge.net/projects/phpexcelreader/
36
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
37
//     Modelled moreso after Perl Excel Parse/Write modules
38
//     Added Parse_Excel_Spreadsheet object
39
//         Reads a whole worksheet or tab as row,column array or as
40
//         associated hash of indexed rows and named column fields
41
//     Added variables for worksheet (tab) indexes and names
42
//     Added an object call for loading individual woorksheets
43
//     Changed default indexing defaults to 0 based arrays
44
//     Fixed date/time and percent formats
45
//     Includes patches found at SourceForge...
46
//         unicode patch by nobody
47
//         unpack("d") machine depedency patch by matchy
48
//         boundsheet utf16 patch by bjaenichen
49
//     Renamed functions for shorter names
50
//     General code cleanup and rigor, including <80 column width
51
//     Included a testcase Excel file and PHP example calls
52
//     Code works for PHP 5.x
53
54
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
55
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
56
//     Decoding of formula conditions, results, and tokens.
57
//     Support for user-defined named cells added as an array "namedcells"
58
//         Patch code for user-defined named cells supports single cells only.
59
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
60
//         external sheet reference structure
61
class Xls extends BaseReader
62
{
63
    // ParseXL definitions
64
    const XLS_BIFF8 = 0x0600;
65
    const XLS_BIFF7 = 0x0500;
66
    const XLS_WORKBOOKGLOBALS = 0x0005;
67
    const XLS_WORKSHEET = 0x0010;
68
69
    // record identifiers
70
    const XLS_TYPE_FORMULA = 0x0006;
71
    const XLS_TYPE_EOF = 0x000a;
72
    const XLS_TYPE_PROTECT = 0x0012;
73
    const XLS_TYPE_OBJECTPROTECT = 0x0063;
74
    const XLS_TYPE_SCENPROTECT = 0x00dd;
75
    const XLS_TYPE_PASSWORD = 0x0013;
76
    const XLS_TYPE_HEADER = 0x0014;
77
    const XLS_TYPE_FOOTER = 0x0015;
78
    const XLS_TYPE_EXTERNSHEET = 0x0017;
79
    const XLS_TYPE_DEFINEDNAME = 0x0018;
80
    const XLS_TYPE_VERTICALPAGEBREAKS = 0x001a;
81
    const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001b;
82
    const XLS_TYPE_NOTE = 0x001c;
83
    const XLS_TYPE_SELECTION = 0x001d;
84
    const XLS_TYPE_DATEMODE = 0x0022;
85
    const XLS_TYPE_EXTERNNAME = 0x0023;
86
    const XLS_TYPE_LEFTMARGIN = 0x0026;
87
    const XLS_TYPE_RIGHTMARGIN = 0x0027;
88
    const XLS_TYPE_TOPMARGIN = 0x0028;
89
    const XLS_TYPE_BOTTOMMARGIN = 0x0029;
90
    const XLS_TYPE_PRINTGRIDLINES = 0x002b;
91
    const XLS_TYPE_FILEPASS = 0x002f;
92
    const XLS_TYPE_FONT = 0x0031;
93
    const XLS_TYPE_CONTINUE = 0x003c;
94
    const XLS_TYPE_PANE = 0x0041;
95
    const XLS_TYPE_CODEPAGE = 0x0042;
96
    const XLS_TYPE_DEFCOLWIDTH = 0x0055;
97
    const XLS_TYPE_OBJ = 0x005d;
98
    const XLS_TYPE_COLINFO = 0x007d;
99
    const XLS_TYPE_IMDATA = 0x007f;
100
    const XLS_TYPE_SHEETPR = 0x0081;
101
    const XLS_TYPE_HCENTER = 0x0083;
102
    const XLS_TYPE_VCENTER = 0x0084;
103
    const XLS_TYPE_SHEET = 0x0085;
104
    const XLS_TYPE_PALETTE = 0x0092;
105
    const XLS_TYPE_SCL = 0x00a0;
106
    const XLS_TYPE_PAGESETUP = 0x00a1;
107
    const XLS_TYPE_MULRK = 0x00bd;
108
    const XLS_TYPE_MULBLANK = 0x00be;
109
    const XLS_TYPE_DBCELL = 0x00d7;
110
    const XLS_TYPE_XF = 0x00e0;
111
    const XLS_TYPE_MERGEDCELLS = 0x00e5;
112
    const XLS_TYPE_MSODRAWINGGROUP = 0x00eb;
113
    const XLS_TYPE_MSODRAWING = 0x00ec;
114
    const XLS_TYPE_SST = 0x00fc;
115
    const XLS_TYPE_LABELSST = 0x00fd;
116
    const XLS_TYPE_EXTSST = 0x00ff;
117
    const XLS_TYPE_EXTERNALBOOK = 0x01ae;
118
    const XLS_TYPE_DATAVALIDATIONS = 0x01b2;
119
    const XLS_TYPE_TXO = 0x01b6;
120
    const XLS_TYPE_HYPERLINK = 0x01b8;
121
    const XLS_TYPE_DATAVALIDATION = 0x01be;
122
    const XLS_TYPE_DIMENSION = 0x0200;
123
    const XLS_TYPE_BLANK = 0x0201;
124
    const XLS_TYPE_NUMBER = 0x0203;
125
    const XLS_TYPE_LABEL = 0x0204;
126
    const XLS_TYPE_BOOLERR = 0x0205;
127
    const XLS_TYPE_STRING = 0x0207;
128
    const XLS_TYPE_ROW = 0x0208;
129
    const XLS_TYPE_INDEX = 0x020b;
130
    const XLS_TYPE_ARRAY = 0x0221;
131
    const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225;
132
    const XLS_TYPE_WINDOW2 = 0x023e;
133
    const XLS_TYPE_RK = 0x027e;
134
    const XLS_TYPE_STYLE = 0x0293;
135
    const XLS_TYPE_FORMAT = 0x041e;
136
    const XLS_TYPE_SHAREDFMLA = 0x04bc;
137
    const XLS_TYPE_BOF = 0x0809;
138
    const XLS_TYPE_SHEETPROTECTION = 0x0867;
139
    const XLS_TYPE_RANGEPROTECTION = 0x0868;
140
    const XLS_TYPE_SHEETLAYOUT = 0x0862;
141
    const XLS_TYPE_XFEXT = 0x087d;
142
    const XLS_TYPE_PAGELAYOUTVIEW = 0x088b;
143
    const XLS_TYPE_UNKNOWN = 0xffff;
144
145
    // Encryption type
146
    const MS_BIFF_CRYPTO_NONE = 0;
147
    const MS_BIFF_CRYPTO_XOR = 1;
148
    const MS_BIFF_CRYPTO_RC4 = 2;
149
150
    // Size of stream blocks when using RC4 encryption
151
    const REKEY_BLOCK = 0x400;
152
153
    /**
154
     * Summary Information stream data.
155
     *
156
     * @var string
157
     */
158
    private $summaryInformation;
159
160
    /**
161
     * Extended Summary Information stream data.
162
     *
163
     * @var string
164
     */
165
    private $documentSummaryInformation;
166
167
    /**
168
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
169
     *
170
     * @var string
171
     */
172
    private $data;
173
174
    /**
175
     * Size in bytes of $this->data.
176
     *
177
     * @var int
178
     */
179
    private $dataSize;
180
181
    /**
182
     * Current position in stream.
183
     *
184
     * @var int
185
     */
186
    private $pos;
187
188
    /**
189
     * Workbook to be returned by the reader.
190
     *
191
     * @var Spreadsheet
192
     */
193
    private $spreadsheet;
194
195
    /**
196
     * Worksheet that is currently being built by the reader.
197
     *
198
     * @var Worksheet
199
     */
200
    private $phpSheet;
201
202
    /**
203
     * BIFF version.
204
     *
205
     * @var int
206
     */
207
    private $version;
208
209
    /**
210
     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
211
     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'.
212
     *
213
     * @var string
214
     */
215
    private $codepage;
216
217
    /**
218
     * Shared formats.
219
     *
220
     * @var array
221
     */
222
    private $formats;
223
224
    /**
225
     * Shared fonts.
226
     *
227
     * @var array
228
     */
229
    private $objFonts;
230
231
    /**
232
     * Color palette.
233
     *
234
     * @var array
235
     */
236
    private $palette;
237
238
    /**
239
     * Worksheets.
240
     *
241
     * @var array
242
     */
243
    private $sheets;
244
245
    /**
246
     * External books.
247
     *
248
     * @var array
249
     */
250
    private $externalBooks;
251
252
    /**
253
     * REF structures. Only applies to BIFF8.
254
     *
255
     * @var array
256
     */
257
    private $ref;
258
259
    /**
260
     * External names.
261
     *
262
     * @var array
263
     */
264
    private $externalNames;
265
266
    /**
267
     * Defined names.
268
     *
269
     * @var array
270
     */
271
    private $definedname;
272
273
    /**
274
     * Shared strings. Only applies to BIFF8.
275
     *
276
     * @var array
277
     */
278
    private $sst;
279
280
    /**
281
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
282
     *
283
     * @var bool
284
     */
285
    private $frozen;
286
287
    /**
288
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
289
     *
290
     * @var bool
291
     */
292
    private $isFitToPages;
293
294
    /**
295
     * Objects. One OBJ record contributes with one entry.
296
     *
297
     * @var array
298
     */
299
    private $objs;
300
301
    /**
302
     * Text Objects. One TXO record corresponds with one entry.
303
     *
304
     * @var array
305
     */
306
    private $textObjects;
307
308
    /**
309
     * Cell Annotations (BIFF8).
310
     *
311
     * @var array
312
     */
313
    private $cellNotes;
314
315
    /**
316
     * The combined MSODRAWINGGROUP data.
317
     *
318
     * @var string
319
     */
320
    private $drawingGroupData;
321
322
    /**
323
     * The combined MSODRAWING data (per sheet).
324
     *
325
     * @var string
326
     */
327
    private $drawingData;
328
329
    /**
330
     * Keep track of XF index.
331
     *
332
     * @var int
333
     */
334
    private $xfIndex;
335
336
    /**
337
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
338
     *
339
     * @var array
340
     */
341
    private $mapCellXfIndex;
342
343
    /**
344
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
345
     *
346
     * @var array
347
     */
348
    private $mapCellStyleXfIndex;
349
350
    /**
351
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
352
     *
353
     * @var array
354
     */
355
    private $sharedFormulas;
356
357
    /**
358
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
359
     * refers to a shared formula.
360
     *
361
     * @var array
362
     */
363
    private $sharedFormulaParts;
364
365
    /**
366
     * The type of encryption in use.
367
     *
368
     * @var int
369
     */
370
    private $encryption = 0;
371
372
    /**
373
     * The position in the stream after which contents are encrypted.
374
     *
375
     * @var int
376
     */
377
    private $encryptionStartPos = false;
378
379
    /**
380
     * The current RC4 decryption object.
381
     *
382
     * @var Xls\RC4
383
     */
384
    private $rc4Key;
385
386
    /**
387
     * The position in the stream that the RC4 decryption object was left at.
388
     *
389
     * @var int
390
     */
391
    private $rc4Pos = 0;
392
393
    /**
394
     * The current MD5 context state.
395
     *
396
     * @var string
397
     */
398
    private $md5Ctxt;
399
400
    /**
401
     * @var int
402
     */
403
    private $textObjRef;
404
405
    /**
406
     * @var string
407
     */
408
    private $baseCell;
409
410
    /**
411
     * Create a new Xls Reader instance.
412
     */
413 43
    public function __construct()
414
    {
415 43
        parent::__construct();
416 43
    }
417
418
    /**
419
     * Can the current IReader read the file?
420
     *
421
     * @param string $pFilename
422
     *
423
     * @return bool
424
     */
425 7
    public function canRead($pFilename)
426
    {
427 7
        File::assertFile($pFilename);
428
429
        try {
430
            // Use ParseXL for the hard work.
431 7
            $ole = new OLERead();
432
433
            // get excel data
434 7
            $ole->read($pFilename);
435
436 7
            return true;
437
        } catch (PhpSpreadsheetException $e) {
438
            return false;
439
        }
440
    }
441
442
    public function setCodepage(string $codepage): void
443
    {
444
        if (!CodePage::validate($codepage)) {
445
            throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage);
446
        }
447
448
        $this->codepage = $codepage;
449
    }
450
451
    /**
452
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
453
     *
454
     * @param string $pFilename
455
     *
456
     * @return array
457
     */
458 2
    public function listWorksheetNames($pFilename)
459
    {
460 2
        File::assertFile($pFilename);
461
462 2
        $worksheetNames = [];
463
464
        // Read the OLE file
465 2
        $this->loadOLE($pFilename);
466
467
        // total byte size of Excel data (workbook global substream + sheet substreams)
468 2
        $this->dataSize = strlen($this->data);
469
470 2
        $this->pos = 0;
471 2
        $this->sheets = [];
472
473
        // Parse Workbook Global Substream
474 2
        while ($this->pos < $this->dataSize) {
475 2
            $code = self::getUInt2d($this->data, $this->pos);
476
477
            switch ($code) {
478 2
                case self::XLS_TYPE_BOF:
479 2
                    $this->readBof();
480
481 2
                    break;
482 2
                case self::XLS_TYPE_SHEET:
483 2
                    $this->readSheet();
484
485 2
                    break;
486 2
                case self::XLS_TYPE_EOF:
487 2
                    $this->readDefault();
488
489 2
                    break 2;
490
                default:
491 2
                    $this->readDefault();
492
493 2
                    break;
494
            }
495
        }
496
497 2
        foreach ($this->sheets as $sheet) {
498 2
            if ($sheet['sheetType'] != 0x00) {
499
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
500
                continue;
501
            }
502
503 2
            $worksheetNames[] = $sheet['name'];
504
        }
505
506 2
        return $worksheetNames;
507
    }
508
509
    /**
510
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
511
     *
512
     * @param string $pFilename
513
     *
514
     * @return array
515
     */
516 1
    public function listWorksheetInfo($pFilename)
517
    {
518 1
        File::assertFile($pFilename);
519
520 1
        $worksheetInfo = [];
521
522
        // Read the OLE file
523 1
        $this->loadOLE($pFilename);
524
525
        // total byte size of Excel data (workbook global substream + sheet substreams)
526 1
        $this->dataSize = strlen($this->data);
527
528
        // initialize
529 1
        $this->pos = 0;
530 1
        $this->sheets = [];
531
532
        // Parse Workbook Global Substream
533 1
        while ($this->pos < $this->dataSize) {
534 1
            $code = self::getUInt2d($this->data, $this->pos);
535
536
            switch ($code) {
537 1
                case self::XLS_TYPE_BOF:
538 1
                    $this->readBof();
539
540 1
                    break;
541 1
                case self::XLS_TYPE_SHEET:
542 1
                    $this->readSheet();
543
544 1
                    break;
545 1
                case self::XLS_TYPE_EOF:
546 1
                    $this->readDefault();
547
548 1
                    break 2;
549
                default:
550 1
                    $this->readDefault();
551
552 1
                    break;
553
            }
554
        }
555
556
        // Parse the individual sheets
557 1
        foreach ($this->sheets as $sheet) {
558 1
            if ($sheet['sheetType'] != 0x00) {
559
                // 0x00: Worksheet
560
                // 0x02: Chart
561
                // 0x06: Visual Basic module
562
                continue;
563
            }
564
565 1
            $tmpInfo = [];
566 1
            $tmpInfo['worksheetName'] = $sheet['name'];
567 1
            $tmpInfo['lastColumnLetter'] = 'A';
568 1
            $tmpInfo['lastColumnIndex'] = 0;
569 1
            $tmpInfo['totalRows'] = 0;
570 1
            $tmpInfo['totalColumns'] = 0;
571
572 1
            $this->pos = $sheet['offset'];
573
574 1
            while ($this->pos <= $this->dataSize - 4) {
575 1
                $code = self::getUInt2d($this->data, $this->pos);
576
577
                switch ($code) {
578 1
                    case self::XLS_TYPE_RK:
579 1
                    case self::XLS_TYPE_LABELSST:
580 1
                    case self::XLS_TYPE_NUMBER:
581 1
                    case self::XLS_TYPE_FORMULA:
582 1
                    case self::XLS_TYPE_BOOLERR:
583 1
                    case self::XLS_TYPE_LABEL:
584 1
                        $length = self::getUInt2d($this->data, $this->pos + 2);
585 1
                        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
586
587
                        // move stream pointer to next record
588 1
                        $this->pos += 4 + $length;
589
590 1
                        $rowIndex = self::getUInt2d($recordData, 0) + 1;
591 1
                        $columnIndex = self::getUInt2d($recordData, 2);
592
593 1
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
594 1
                        $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
595
596 1
                        break;
597 1
                    case self::XLS_TYPE_BOF:
598 1
                        $this->readBof();
599
600 1
                        break;
601 1
                    case self::XLS_TYPE_EOF:
602 1
                        $this->readDefault();
603
604 1
                        break 2;
605
                    default:
606 1
                        $this->readDefault();
607
608 1
                        break;
609
                }
610
            }
611
612 1
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
613 1
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
614
615 1
            $worksheetInfo[] = $tmpInfo;
616
        }
617
618 1
        return $worksheetInfo;
619
    }
620
621
    /**
622
     * Loads PhpSpreadsheet from file.
623
     *
624
     * @param string $pFilename
625
     *
626
     * @return Spreadsheet
627
     */
628 36
    public function load($pFilename)
629
    {
630
        // Read the OLE file
631 36
        $this->loadOLE($pFilename);
632
633
        // Initialisations
634 36
        $this->spreadsheet = new Spreadsheet();
635 36
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
636 36
        if (!$this->readDataOnly) {
637 35
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
638 35
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
639
        }
640
641
        // Read the summary information stream (containing meta data)
642 36
        $this->readSummaryInformation();
643
644
        // Read the Additional document summary information stream (containing application-specific meta data)
645 36
        $this->readDocumentSummaryInformation();
646
647
        // total byte size of Excel data (workbook global substream + sheet substreams)
648 36
        $this->dataSize = strlen($this->data);
649
650
        // initialize
651 36
        $this->pos = 0;
652 36
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
653 36
        $this->formats = [];
654 36
        $this->objFonts = [];
655 36
        $this->palette = [];
656 36
        $this->sheets = [];
657 36
        $this->externalBooks = [];
658 36
        $this->ref = [];
659 36
        $this->definedname = [];
660 36
        $this->sst = [];
661 36
        $this->drawingGroupData = '';
662 36
        $this->xfIndex = '';
663 36
        $this->mapCellXfIndex = [];
664 36
        $this->mapCellStyleXfIndex = [];
665
666
        // Parse Workbook Global Substream
667 36
        while ($this->pos < $this->dataSize) {
668 36
            $code = self::getUInt2d($this->data, $this->pos);
669
670
            switch ($code) {
671 36
                case self::XLS_TYPE_BOF:
672 36
                    $this->readBof();
673
674 36
                    break;
675 36
                case self::XLS_TYPE_FILEPASS:
676
                    $this->readFilepass();
677
678
                    break;
679 36
                case self::XLS_TYPE_CODEPAGE:
680 34
                    $this->readCodepage();
681
682 34
                    break;
683 36
                case self::XLS_TYPE_DATEMODE:
684 35
                    $this->readDateMode();
685
686 35
                    break;
687 36
                case self::XLS_TYPE_FONT:
688 36
                    $this->readFont();
689
690 36
                    break;
691 36
                case self::XLS_TYPE_FORMAT:
692 22
                    $this->readFormat();
693
694 22
                    break;
695 36
                case self::XLS_TYPE_XF:
696 36
                    $this->readXf();
697
698 36
                    break;
699 36
                case self::XLS_TYPE_XFEXT:
700 6
                    $this->readXfExt();
701
702 6
                    break;
703 36
                case self::XLS_TYPE_STYLE:
704 36
                    $this->readStyle();
705
706 36
                    break;
707 36
                case self::XLS_TYPE_PALETTE:
708 19
                    $this->readPalette();
709
710 19
                    break;
711 36
                case self::XLS_TYPE_SHEET:
712 36
                    $this->readSheet();
713
714 36
                    break;
715 36
                case self::XLS_TYPE_EXTERNALBOOK:
716 22
                    $this->readExternalBook();
717
718 22
                    break;
719 36
                case self::XLS_TYPE_EXTERNNAME:
720
                    $this->readExternName();
721
722
                    break;
723 36
                case self::XLS_TYPE_EXTERNSHEET:
724 22
                    $this->readExternSheet();
725
726 22
                    break;
727 36
                case self::XLS_TYPE_DEFINEDNAME:
728 6
                    $this->readDefinedName();
729
730 6
                    break;
731 36
                case self::XLS_TYPE_MSODRAWINGGROUP:
732 6
                    $this->readMsoDrawingGroup();
733
734 6
                    break;
735 36
                case self::XLS_TYPE_SST:
736 35
                    $this->readSst();
737
738 35
                    break;
739 36
                case self::XLS_TYPE_EOF:
740 36
                    $this->readDefault();
741
742 36
                    break 2;
743
                default:
744 36
                    $this->readDefault();
745
746 36
                    break;
747
            }
748
        }
749
750
        // Resolve indexed colors for font, fill, and border colors
751
        // Cannot be resolved already in XF record, because PALETTE record comes afterwards
752 36
        if (!$this->readDataOnly) {
753 35
            foreach ($this->objFonts as $objFont) {
754 35
                if (isset($objFont->colorIndex)) {
755 35
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
756 35
                    $objFont->getColor()->setRGB($color['rgb']);
757
                }
758
            }
759
760 35
            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
761
                // fill start and end color
762 35
                $fill = $objStyle->getFill();
763
764 35
                if (isset($fill->startcolorIndex)) {
765 35
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
766 35
                    $fill->getStartColor()->setRGB($startColor['rgb']);
767
                }
768 35
                if (isset($fill->endcolorIndex)) {
769 35
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
770 35
                    $fill->getEndColor()->setRGB($endColor['rgb']);
771
                }
772
773
                // border colors
774 35
                $top = $objStyle->getBorders()->getTop();
775 35
                $right = $objStyle->getBorders()->getRight();
776 35
                $bottom = $objStyle->getBorders()->getBottom();
777 35
                $left = $objStyle->getBorders()->getLeft();
778 35
                $diagonal = $objStyle->getBorders()->getDiagonal();
779
780 35
                if (isset($top->colorIndex)) {
781 35
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
782 35
                    $top->getColor()->setRGB($borderTopColor['rgb']);
783
                }
784 35
                if (isset($right->colorIndex)) {
785 35
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
786 35
                    $right->getColor()->setRGB($borderRightColor['rgb']);
787
                }
788 35
                if (isset($bottom->colorIndex)) {
789 35
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
790 35
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
791
                }
792 35
                if (isset($left->colorIndex)) {
793 35
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
794 35
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
795
                }
796 35
                if (isset($diagonal->colorIndex)) {
797 35
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
798 35
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
799
                }
800
            }
801
        }
802
803
        // treat MSODRAWINGGROUP records, workbook-level Escher
804 36
        if (!$this->readDataOnly && $this->drawingGroupData) {
805 6
            $escherWorkbook = new Escher();
806 6
            $reader = new Xls\Escher($escherWorkbook);
807 6
            $escherWorkbook = $reader->load($this->drawingGroupData);
808
        }
809
810
        // Parse the individual sheets
811 36
        foreach ($this->sheets as $sheet) {
812 36
            if ($sheet['sheetType'] != 0x00) {
813
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
814
                continue;
815
            }
816
817
            // check if sheet should be skipped
818 36
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
819 5
                continue;
820
            }
821
822
            // add sheet to PhpSpreadsheet object
823 36
            $this->phpSheet = $this->spreadsheet->createSheet();
824
            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
825
            //        cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
826
            //        name in line with the formula, not the reverse
827 36
            $this->phpSheet->setTitle($sheet['name'], false, false);
828 36
            $this->phpSheet->setSheetState($sheet['sheetState']);
829
830 36
            $this->pos = $sheet['offset'];
831
832
            // Initialize isFitToPages. May change after reading SHEETPR record.
833 36
            $this->isFitToPages = false;
834
835
            // Initialize drawingData
836 36
            $this->drawingData = '';
837
838
            // Initialize objs
839 36
            $this->objs = [];
840
841
            // Initialize shared formula parts
842 36
            $this->sharedFormulaParts = [];
843
844
            // Initialize shared formulas
845 36
            $this->sharedFormulas = [];
846
847
            // Initialize text objs
848 36
            $this->textObjects = [];
849
850
            // Initialize cell annotations
851 36
            $this->cellNotes = [];
852 36
            $this->textObjRef = -1;
853
854 36
            while ($this->pos <= $this->dataSize - 4) {
855 36
                $code = self::getUInt2d($this->data, $this->pos);
856
857
                switch ($code) {
858 36
                    case self::XLS_TYPE_BOF:
859 36
                        $this->readBof();
860
861 36
                        break;
862 36
                    case self::XLS_TYPE_PRINTGRIDLINES:
863 34
                        $this->readPrintGridlines();
864
865 34
                        break;
866 36
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
867 21
                        $this->readDefaultRowHeight();
868
869 21
                        break;
870 36
                    case self::XLS_TYPE_SHEETPR:
871 35
                        $this->readSheetPr();
872
873 35
                        break;
874 36
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
875 1
                        $this->readHorizontalPageBreaks();
876
877 1
                        break;
878 36
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
879 1
                        $this->readVerticalPageBreaks();
880
881 1
                        break;
882 36
                    case self::XLS_TYPE_HEADER:
883 34
                        $this->readHeader();
884
885 34
                        break;
886 36
                    case self::XLS_TYPE_FOOTER:
887 34
                        $this->readFooter();
888
889 34
                        break;
890 36
                    case self::XLS_TYPE_HCENTER:
891 34
                        $this->readHcenter();
892
893 34
                        break;
894 36
                    case self::XLS_TYPE_VCENTER:
895 34
                        $this->readVcenter();
896
897 34
                        break;
898 36
                    case self::XLS_TYPE_LEFTMARGIN:
899 21
                        $this->readLeftMargin();
900
901 21
                        break;
902 36
                    case self::XLS_TYPE_RIGHTMARGIN:
903 21
                        $this->readRightMargin();
904
905 21
                        break;
906 36
                    case self::XLS_TYPE_TOPMARGIN:
907 21
                        $this->readTopMargin();
908
909 21
                        break;
910 36
                    case self::XLS_TYPE_BOTTOMMARGIN:
911 21
                        $this->readBottomMargin();
912
913 21
                        break;
914 36
                    case self::XLS_TYPE_PAGESETUP:
915 35
                        $this->readPageSetup();
916
917 35
                        break;
918 36
                    case self::XLS_TYPE_PROTECT:
919 2
                        $this->readProtect();
920
921 2
                        break;
922 36
                    case self::XLS_TYPE_SCENPROTECT:
923
                        $this->readScenProtect();
924
925
                        break;
926 36
                    case self::XLS_TYPE_OBJECTPROTECT:
927
                        $this->readObjectProtect();
928
929
                        break;
930 36
                    case self::XLS_TYPE_PASSWORD:
931
                        $this->readPassword();
932
933
                        break;
934 36
                    case self::XLS_TYPE_DEFCOLWIDTH:
935 35
                        $this->readDefColWidth();
936
937 35
                        break;
938 36
                    case self::XLS_TYPE_COLINFO:
939 30
                        $this->readColInfo();
940
941 30
                        break;
942 36
                    case self::XLS_TYPE_DIMENSION:
943 36
                        $this->readDefault();
944
945 36
                        break;
946 36
                    case self::XLS_TYPE_ROW:
947 22
                        $this->readRow();
948
949 22
                        break;
950 36
                    case self::XLS_TYPE_DBCELL:
951 20
                        $this->readDefault();
952
953 20
                        break;
954 36
                    case self::XLS_TYPE_RK:
955 14
                        $this->readRk();
956
957 14
                        break;
958 36
                    case self::XLS_TYPE_LABELSST:
959 24
                        $this->readLabelSst();
960
961 24
                        break;
962 36
                    case self::XLS_TYPE_MULRK:
963 13
                        $this->readMulRk();
964
965 13
                        break;
966 36
                    case self::XLS_TYPE_NUMBER:
967 17
                        $this->readNumber();
968
969 17
                        break;
970 36
                    case self::XLS_TYPE_FORMULA:
971 15
                        $this->readFormula();
972
973 15
                        break;
974 36
                    case self::XLS_TYPE_SHAREDFMLA:
975
                        $this->readSharedFmla();
976
977
                        break;
978 36
                    case self::XLS_TYPE_BOOLERR:
979 9
                        $this->readBoolErr();
980
981 9
                        break;
982 36
                    case self::XLS_TYPE_MULBLANK:
983 12
                        $this->readMulBlank();
984
985 12
                        break;
986 36
                    case self::XLS_TYPE_LABEL:
987 1
                        $this->readLabel();
988
989 1
                        break;
990 36
                    case self::XLS_TYPE_BLANK:
991 7
                        $this->readBlank();
992
993 7
                        break;
994 36
                    case self::XLS_TYPE_MSODRAWING:
995 6
                        $this->readMsoDrawing();
996
997 6
                        break;
998 36
                    case self::XLS_TYPE_OBJ:
999 6
                        $this->readObj();
1000
1001 6
                        break;
1002 36
                    case self::XLS_TYPE_WINDOW2:
1003 36
                        $this->readWindow2();
1004
1005 36
                        break;
1006 36
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
1007 20
                        $this->readPageLayoutView();
1008
1009 20
                        break;
1010 36
                    case self::XLS_TYPE_SCL:
1011 1
                        $this->readScl();
1012
1013 1
                        break;
1014 36
                    case self::XLS_TYPE_PANE:
1015 5
                        $this->readPane();
1016
1017 5
                        break;
1018 36
                    case self::XLS_TYPE_SELECTION:
1019 34
                        $this->readSelection();
1020
1021 34
                        break;
1022 36
                    case self::XLS_TYPE_MERGEDCELLS:
1023 14
                        $this->readMergedCells();
1024
1025 14
                        break;
1026 36
                    case self::XLS_TYPE_HYPERLINK:
1027 3
                        $this->readHyperLink();
1028
1029 3
                        break;
1030 36
                    case self::XLS_TYPE_DATAVALIDATIONS:
1031
                        $this->readDataValidations();
1032
1033
                        break;
1034 36
                    case self::XLS_TYPE_DATAVALIDATION:
1035
                        $this->readDataValidation();
1036
1037
                        break;
1038 36
                    case self::XLS_TYPE_SHEETLAYOUT:
1039 3
                        $this->readSheetLayout();
1040
1041 3
                        break;
1042 36
                    case self::XLS_TYPE_SHEETPROTECTION:
1043 21
                        $this->readSheetProtection();
1044
1045 21
                        break;
1046 36
                    case self::XLS_TYPE_RANGEPROTECTION:
1047 1
                        $this->readRangeProtection();
1048
1049 1
                        break;
1050 36
                    case self::XLS_TYPE_NOTE:
1051 2
                        $this->readNote();
1052
1053 2
                        break;
1054 36
                    case self::XLS_TYPE_TXO:
1055 2
                        $this->readTextObject();
1056
1057 2
                        break;
1058 36
                    case self::XLS_TYPE_CONTINUE:
1059
                        $this->readContinue();
1060
1061
                        break;
1062 36
                    case self::XLS_TYPE_EOF:
1063 36
                        $this->readDefault();
1064
1065 36
                        break 2;
1066
                    default:
1067 35
                        $this->readDefault();
1068
1069 35
                        break;
1070
                }
1071
            }
1072
1073
            // treat MSODRAWING records, sheet-level Escher
1074 36
            if (!$this->readDataOnly && $this->drawingData) {
1075 6
                $escherWorksheet = new Escher();
1076 6
                $reader = new Xls\Escher($escherWorksheet);
1077 6
                $escherWorksheet = $reader->load($this->drawingData);
1078
1079
                // get all spContainers in one long array, so they can be mapped to OBJ records
1080 6
                $allSpContainers = $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers();
1081
            }
1082
1083
            // treat OBJ records
1084 36
            foreach ($this->objs as $n => $obj) {
1085
                // the first shape container never has a corresponding OBJ record, hence $n + 1
1086 6
                if (isset($allSpContainers[$n + 1]) && is_object($allSpContainers[$n + 1])) {
1087 6
                    $spContainer = $allSpContainers[$n + 1];
1088
1089
                    // we skip all spContainers that are a part of a group shape since we cannot yet handle those
1090 6
                    if ($spContainer->getNestingLevel() > 1) {
1091
                        continue;
1092
                    }
1093
1094
                    // calculate the width and height of the shape
1095 6
                    [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
1096 6
                    [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());
1097
1098 6
                    $startOffsetX = $spContainer->getStartOffsetX();
1099 6
                    $startOffsetY = $spContainer->getStartOffsetY();
1100 6
                    $endOffsetX = $spContainer->getEndOffsetX();
1101 6
                    $endOffsetY = $spContainer->getEndOffsetY();
1102
1103 6
                    $width = \PhpOffice\PhpSpreadsheet\Shared\Xls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX);
1104 6
                    $height = \PhpOffice\PhpSpreadsheet\Shared\Xls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY);
1105
1106
                    // calculate offsetX and offsetY of the shape
1107 6
                    $offsetX = $startOffsetX * \PhpOffice\PhpSpreadsheet\Shared\Xls::sizeCol($this->phpSheet, $startColumn) / 1024;
1108 6
                    $offsetY = $startOffsetY * \PhpOffice\PhpSpreadsheet\Shared\Xls::sizeRow($this->phpSheet, $startRow) / 256;
1109
1110 6
                    switch ($obj['otObjType']) {
1111 6
                        case 0x19:
1112
                            // Note
1113 2
                            if (isset($this->cellNotes[$obj['idObjID']])) {
1114 2
                                $cellNote = $this->cellNotes[$obj['idObjID']];
1115
1116 2
                                if (isset($this->textObjects[$obj['idObjID']])) {
1117 2
                                    $textObject = $this->textObjects[$obj['idObjID']];
1118 2
                                    $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject;
1119
                                }
1120
                            }
1121
1122 2
                            break;
1123 6
                        case 0x08:
1124
                            // picture
1125
                            // get index to BSE entry (1-based)
1126 6
                            $BSEindex = $spContainer->getOPT(0x0104);
1127
1128
                            // If there is no BSE Index, we will fail here and other fields are not read.
1129
                            // Fix by checking here.
1130
                            // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
1131
                            // More likely : a uncompatible picture
1132 6
                            if (!$BSEindex) {
1133
                                continue 2;
1134
                            }
1135
1136 6
                            $BSECollection = $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection();
1137 6
                            $BSE = $BSECollection[$BSEindex - 1];
1138 6
                            $blipType = $BSE->getBlipType();
1139
1140
                            // need check because some blip types are not supported by Escher reader such as EMF
1141 6
                            if ($blip = $BSE->getBlip()) {
1142 6
                                $ih = imagecreatefromstring($blip->getData());
1143 6
                                $drawing = new MemoryDrawing();
1144 6
                                $drawing->setImageResource($ih);
1145
1146
                                // width, height, offsetX, offsetY
1147 6
                                $drawing->setResizeProportional(false);
1148 6
                                $drawing->setWidth($width);
1149 6
                                $drawing->setHeight($height);
1150 6
                                $drawing->setOffsetX($offsetX);
1151 6
                                $drawing->setOffsetY($offsetY);
1152
1153 1
                                switch ($blipType) {
1154 5
                                    case BSE::BLIPTYPE_JPEG:
1155 4
                                        $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG);
1156 4
                                        $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG);
1157
1158 4
                                        break;
1159 5
                                    case BSE::BLIPTYPE_PNG:
1160 6
                                        $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG);
1161 6
                                        $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG);
1162
1163 6
                                        break;
1164
                                }
1165
1166 6
                                $drawing->setWorksheet($this->phpSheet);
1167 6
                                $drawing->setCoordinates($spContainer->getStartCoordinates());
1168
                            }
1169
1170 6
                            break;
1171
                        default:
1172
                            // other object type
1173
                            break;
1174
                    }
1175
                }
1176
            }
1177
1178
            // treat SHAREDFMLA records
1179 36
            if ($this->version == self::XLS_BIFF8) {
1180 36
                foreach ($this->sharedFormulaParts as $cell => $baseCell) {
1181
                    [$column, $row] = Coordinate::coordinateFromString($cell);
1182
                    if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
1183
                        $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell);
1184
                        $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
1185
                    }
1186
                }
1187
            }
1188
1189 36
            if (!empty($this->cellNotes)) {
1190 2
                foreach ($this->cellNotes as $note => $noteDetails) {
1191 2
                    if (!isset($noteDetails['objTextData'])) {
1192
                        if (isset($this->textObjects[$note])) {
1193
                            $textObject = $this->textObjects[$note];
1194
                            $noteDetails['objTextData'] = $textObject;
1195
                        } else {
1196
                            $noteDetails['objTextData']['text'] = '';
1197
                        }
1198
                    }
1199 2
                    $cellAddress = str_replace('$', '', $noteDetails['cellRef']);
1200 2
                    $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text']));
1201
                }
1202
            }
1203
        }
1204
1205
        // add the named ranges (defined names)
1206 36
        foreach ($this->definedname as $definedName) {
1207 6
            if ($definedName['isBuiltInName']) {
1208 3
                switch ($definedName['name']) {
1209 3
                    case pack('C', 0x06):
1210
                        // print area
1211
                        //    in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2
1212 3
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1213
1214 3
                        $extractedRanges = [];
1215 3
                        foreach ($ranges as $range) {
1216
                            // $range should look like one of these
1217
                            //        Foo!$C$7:$J$66
1218
                            //        Bar!$A$1:$IV$2
1219 3
                            $explodes = Worksheet::extractSheetTitle($range, true);
1220 3
                            $sheetName = trim($explodes[0], "'");
1221 3
                            if (count($explodes) == 2) {
1222 3
                                if (strpos($explodes[1], ':') === false) {
1223
                                    $explodes[1] = $explodes[1] . ':' . $explodes[1];
1224
                                }
1225 3
                                $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66
1226
                            }
1227
                        }
1228 3
                        if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) {
1229 3
                            $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2
1230
                        }
1231
1232 3
                        break;
1233
                    case pack('C', 0x07):
1234
                        // print titles (repeating rows)
1235
                        // Assuming BIFF8, there are 3 cases
1236
                        // 1. repeating rows
1237
                        //        formula looks like this: Sheet!$A$1:$IV$2
1238
                        //        rows 1-2 repeat
1239
                        // 2. repeating columns
1240
                        //        formula looks like this: Sheet!$A$1:$B$65536
1241
                        //        columns A-B repeat
1242
                        // 3. both repeating rows and repeating columns
1243
                        //        formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2
1244
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1245
                        foreach ($ranges as $range) {
1246
                            // $range should look like this one of these
1247
                            //        Sheet!$A$1:$B$65536
1248
                            //        Sheet!$A$1:$IV$2
1249
                            if (strpos($range, '!') !== false) {
1250
                                $explodes = Worksheet::extractSheetTitle($range, true);
1251
                                if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) {
1252
                                    $extractedRange = $explodes[1];
1253
                                    $extractedRange = str_replace('$', '', $extractedRange);
1254
1255
                                    $coordinateStrings = explode(':', $extractedRange);
1256
                                    if (count($coordinateStrings) == 2) {
1257
                                        [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]);
1258
                                        [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]);
1259
1260
                                        if ($firstColumn == 'A' && $lastColumn == 'IV') {
1261
                                            // then we have repeating rows
1262
                                            $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
1263
                                        } elseif ($firstRow == 1 && $lastRow == 65536) {
1264
                                            // then we have repeating columns
1265
                                            $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
1266
                                        }
1267
                                    }
1268
                                }
1269
                            }
1270
                        }
1271
1272 3
                        break;
1273
                }
1274
            } else {
1275
                // Extract range
1276 3
                if (strpos($definedName['formula'], '!') !== false) {
1277 3
                    $explodes = Worksheet::extractSheetTitle($definedName['formula'], true);
1278
                    if (
1279 3
                        ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) ||
1280 3
                        ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'")))
1281
                    ) {
1282 3
                        $extractedRange = $explodes[1];
1283 3
                        $extractedRange = str_replace('$', '', $extractedRange);
1284
1285 3
                        $localOnly = ($definedName['scope'] == 0) ? false : true;
1286
1287 3
                        $scope = ($definedName['scope'] == 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']);
1288
1289 3
                        $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope));
1290
                    }
1291
                }
1292
                //    Named Value
1293
                    //    TODO Provide support for named values
1294
            }
1295
        }
1296 36
        $this->data = null;
1297
1298 36
        return $this->spreadsheet;
1299
    }
1300
1301
    /**
1302
     * Read record data from stream, decrypting as required.
1303
     *
1304
     * @param string $data Data stream to read from
1305
     * @param int $pos Position to start reading from
1306
     * @param int $len Record data length
1307
     *
1308
     * @return string Record data
1309
     */
1310 39
    private function readRecordData($data, $pos, $len)
1311
    {
1312 39
        $data = substr($data, $pos, $len);
1313
1314
        // File not encrypted, or record before encryption start point
1315 39
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
1316 39
            return $data;
1317
        }
1318
1319
        $recordData = '';
1320
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
1321
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
1322
            $block = floor($pos / self::REKEY_BLOCK);
1323
            $endBlock = floor(($pos + $len) / self::REKEY_BLOCK);
1324
1325
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
1326
            // at a point earlier in the current block, re-use it as we can save some time.
1327
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
1328
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1329
                $step = $pos % self::REKEY_BLOCK;
1330
            } else {
1331
                $step = $pos - $this->rc4Pos;
1332
            }
1333
            $this->rc4Key->RC4(str_repeat("\0", $step));
1334
1335
            // Decrypt record data (re-keying at the end of every block)
1336
            while ($block != $endBlock) {
1337
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1338
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1339
                $data = substr($data, $step);
1340
                $pos += $step;
1341
                $len -= $step;
1342
                ++$block;
1343
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1344
            }
1345
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
1346
1347
            // Keep track of the position of this decryptor.
1348
            // We'll try and re-use it later if we can to speed things up
1349
            $this->rc4Pos = $pos + $len;
1350
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
1351
            throw new Exception('XOr encryption not supported');
1352
        }
1353
1354
        return $recordData;
1355
    }
1356
1357
    /**
1358
     * Use OLE reader to extract the relevant data streams from the OLE file.
1359
     *
1360
     * @param string $pFilename
1361
     */
1362 39
    private function loadOLE($pFilename): void
1363
    {
1364
        // OLE reader
1365 39
        $ole = new OLERead();
1366
        // get excel data,
1367 39
        $ole->read($pFilename);
1368
        // Get workbook data: workbook stream + sheet streams
1369 39
        $this->data = $ole->getStream($ole->wrkbook);
1370
        // Get summary information data
1371 39
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
1372
        // Get additional document summary information data
1373 39
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
1374 39
    }
1375
1376
    /**
1377
     * Read summary information.
1378
     */
1379 36
    private function readSummaryInformation(): void
1380
    {
1381 36
        if (!isset($this->summaryInformation)) {
1382 2
            return;
1383
        }
1384
1385
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
1386
        // offset: 2; size: 2;
1387
        // offset: 4; size: 2; OS version
1388
        // offset: 6; size: 2; OS indicator
1389
        // offset: 8; size: 16
1390
        // offset: 24; size: 4; section count
1391 34
        $secCount = self::getInt4d($this->summaryInformation, 24);
1392
1393
        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
1394
        // offset: 44; size: 4
1395 34
        $secOffset = self::getInt4d($this->summaryInformation, 44);
1396
1397
        // section header
1398
        // offset: $secOffset; size: 4; section length
1399 34
        $secLength = self::getInt4d($this->summaryInformation, $secOffset);
1400
1401
        // offset: $secOffset+4; size: 4; property count
1402 34
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
1403
1404
        // initialize code page (used to resolve string values)
1405 34
        $codePage = 'CP1252';
1406
1407
        // offset: ($secOffset+8); size: var
1408
        // loop through property decarations and properties
1409 34
        for ($i = 0; $i < $countProperties; ++$i) {
1410
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
1411 34
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
1412
1413
            // Use value of property id as appropriate
1414
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
1415 34
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
1416
1417 34
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
1418
1419
            // initialize property value
1420 34
            $value = null;
1421
1422
            // extract property value based on property type
1423
            switch ($type) {
1424 34
                case 0x02: // 2 byte signed integer
1425 34
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
1426
1427 34
                    break;
1428 34
                case 0x03: // 4 byte signed integer
1429 33
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1430
1431 33
                    break;
1432 34
                case 0x13: // 4 byte unsigned integer
1433
                    // not needed yet, fix later if necessary
1434
                    break;
1435 34
                case 0x1E: // null-terminated string prepended by dword string length
1436 34
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1437 34
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
1438 34
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1439 34
                    $value = rtrim($value);
1440
1441 34
                    break;
1442 34
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1443
                    // PHP-time
1444 34
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
1445
1446 34
                    break;
1447
                case 0x47: // Clipboard format
1448
                    // not needed yet, fix later if necessary
1449
                    break;
1450
            }
1451
1452
            switch ($id) {
1453 34
                case 0x01:    //    Code Page
1454 34
                    $codePage = CodePage::numberToName($value);
1455
1456 34
                    break;
1457 34
                case 0x02:    //    Title
1458 18
                    $this->spreadsheet->getProperties()->setTitle($value);
1459
1460 18
                    break;
1461 34
                case 0x03:    //    Subject
1462 5
                    $this->spreadsheet->getProperties()->setSubject($value);
1463
1464 5
                    break;
1465 34
                case 0x04:    //    Author (Creator)
1466 32
                    $this->spreadsheet->getProperties()->setCreator($value);
1467
1468 32
                    break;
1469 34
                case 0x05:    //    Keywords
1470 5
                    $this->spreadsheet->getProperties()->setKeywords($value);
1471
1472 5
                    break;
1473 34
                case 0x06:    //    Comments (Description)
1474 5
                    $this->spreadsheet->getProperties()->setDescription($value);
1475
1476 5
                    break;
1477 34
                case 0x07:    //    Template
1478
                    //    Not supported by PhpSpreadsheet
1479
                    break;
1480 34
                case 0x08:    //    Last Saved By (LastModifiedBy)
1481 33
                    $this->spreadsheet->getProperties()->setLastModifiedBy($value);
1482
1483 33
                    break;
1484 34
                case 0x09:    //    Revision
1485
                    //    Not supported by PhpSpreadsheet
1486 1
                    break;
1487 34
                case 0x0A:    //    Total Editing Time
1488
                    //    Not supported by PhpSpreadsheet
1489 1
                    break;
1490 34
                case 0x0B:    //    Last Printed
1491
                    //    Not supported by PhpSpreadsheet
1492 3
                    break;
1493 34
                case 0x0C:    //    Created Date/Time
1494 34
                    $this->spreadsheet->getProperties()->setCreated($value);
1495
1496 34
                    break;
1497 34
                case 0x0D:    //    Modified Date/Time
1498 34
                    $this->spreadsheet->getProperties()->setModified($value);
1499
1500 34
                    break;
1501 33
                case 0x0E:    //    Number of Pages
1502
                    //    Not supported by PhpSpreadsheet
1503
                    break;
1504 33
                case 0x0F:    //    Number of Words
1505
                    //    Not supported by PhpSpreadsheet
1506
                    break;
1507 33
                case 0x10:    //    Number of Characters
1508
                    //    Not supported by PhpSpreadsheet
1509
                    break;
1510 33
                case 0x11:    //    Thumbnail
1511
                    //    Not supported by PhpSpreadsheet
1512
                    break;
1513 33
                case 0x12:    //    Name of creating application
1514
                    //    Not supported by PhpSpreadsheet
1515 15
                    break;
1516 33
                case 0x13:    //    Security
1517
                    //    Not supported by PhpSpreadsheet
1518 33
                    break;
1519
            }
1520
        }
1521 34
    }
1522
1523
    /**
1524
     * Read additional document summary information.
1525
     */
1526 36
    private function readDocumentSummaryInformation(): void
1527
    {
1528 36
        if (!isset($this->documentSummaryInformation)) {
1529 2
            return;
1530
        }
1531
1532
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
1533
        //    offset: 2;    size: 2;
1534
        //    offset: 4;    size: 2;    OS version
1535
        //    offset: 6;    size: 2;    OS indicator
1536
        //    offset: 8;    size: 16
1537
        //    offset: 24;    size: 4;    section count
1538 34
        $secCount = self::getInt4d($this->documentSummaryInformation, 24);
1539
1540
        // offset: 28;    size: 16;    first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
1541
        // offset: 44;    size: 4;    first section offset
1542 34
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
1543
1544
        //    section header
1545
        //    offset: $secOffset;    size: 4;    section length
1546 34
        $secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
1547
1548
        //    offset: $secOffset+4;    size: 4;    property count
1549 34
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
1550
1551
        // initialize code page (used to resolve string values)
1552 34
        $codePage = 'CP1252';
1553
1554
        //    offset: ($secOffset+8);    size: var
1555
        //    loop through property decarations and properties
1556 34
        for ($i = 0; $i < $countProperties; ++$i) {
1557
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
1558 34
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
1559
1560
            // Use value of property id as appropriate
1561
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
1562 34
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
1563
1564 34
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
1565
1566
            // initialize property value
1567 34
            $value = null;
1568
1569
            // extract property value based on property type
1570
            switch ($type) {
1571 34
                case 0x02:    //    2 byte signed integer
1572 34
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1573
1574 34
                    break;
1575 33
                case 0x03:    //    4 byte signed integer
1576 33
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1577
1578 33
                    break;
1579 33
                case 0x0B:  // Boolean
1580 33
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1581 33
                    $value = ($value == 0 ? false : true);
1582
1583 33
                    break;
1584 33
                case 0x13:    //    4 byte unsigned integer
1585
                    // not needed yet, fix later if necessary
1586
                    break;
1587 33
                case 0x1E:    //    null-terminated string prepended by dword string length
1588 18
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1589 18
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
1590 18
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1591 18
                    $value = rtrim($value);
1592
1593 18
                    break;
1594 33
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1595
                    // PHP-Time
1596
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
1597
1598
                    break;
1599 33
                case 0x47:    //    Clipboard format
1600
                    // not needed yet, fix later if necessary
1601
                    break;
1602
            }
1603
1604
            switch ($id) {
1605 34
                case 0x01:    //    Code Page
1606 34
                    $codePage = CodePage::numberToName($value);
1607
1608 34
                    break;
1609 33
                case 0x02:    //    Category
1610 5
                    $this->spreadsheet->getProperties()->setCategory($value);
1611
1612 5
                    break;
1613 33
                case 0x03:    //    Presentation Target
1614
                    //    Not supported by PhpSpreadsheet
1615
                    break;
1616 33
                case 0x04:    //    Bytes
1617
                    //    Not supported by PhpSpreadsheet
1618
                    break;
1619 33
                case 0x05:    //    Lines
1620
                    //    Not supported by PhpSpreadsheet
1621
                    break;
1622 33
                case 0x06:    //    Paragraphs
1623
                    //    Not supported by PhpSpreadsheet
1624
                    break;
1625 33
                case 0x07:    //    Slides
1626
                    //    Not supported by PhpSpreadsheet
1627
                    break;
1628 33
                case 0x08:    //    Notes
1629
                    //    Not supported by PhpSpreadsheet
1630
                    break;
1631 33
                case 0x09:    //    Hidden Slides
1632
                    //    Not supported by PhpSpreadsheet
1633
                    break;
1634 33
                case 0x0A:    //    MM Clips
1635
                    //    Not supported by PhpSpreadsheet
1636
                    break;
1637 33
                case 0x0B:    //    Scale Crop
1638
                    //    Not supported by PhpSpreadsheet
1639 33
                    break;
1640 33
                case 0x0C:    //    Heading Pairs
1641
                    //    Not supported by PhpSpreadsheet
1642 33
                    break;
1643 33
                case 0x0D:    //    Titles of Parts
1644
                    //    Not supported by PhpSpreadsheet
1645 33
                    break;
1646 33
                case 0x0E:    //    Manager
1647 2
                    $this->spreadsheet->getProperties()->setManager($value);
1648
1649 2
                    break;
1650 33
                case 0x0F:    //    Company
1651 17
                    $this->spreadsheet->getProperties()->setCompany($value);
1652
1653 17
                    break;
1654 33
                case 0x10:    //    Links up-to-date
1655
                    //    Not supported by PhpSpreadsheet
1656 33
                    break;
1657
            }
1658
        }
1659 34
    }
1660
1661
    /**
1662
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
1663
     */
1664 39
    private function readDefault(): void
1665
    {
1666 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
1667
1668
        // move stream pointer to next record
1669 39
        $this->pos += 4 + $length;
1670 39
    }
1671
1672
    /**
1673
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
1674
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
1675
     */
1676 2
    private function readNote(): void
1677
    {
1678 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
1679 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1680
1681
        // move stream pointer to next record
1682 2
        $this->pos += 4 + $length;
1683
1684 2
        if ($this->readDataOnly) {
1685
            return;
1686
        }
1687
1688 2
        $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4));
1689 2
        if ($this->version == self::XLS_BIFF8) {
1690 2
            $noteObjID = self::getUInt2d($recordData, 6);
1691 2
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
1692 2
            $noteAuthor = $noteAuthor['value'];
1693 2
            $this->cellNotes[$noteObjID] = [
1694 2
                'cellRef' => $cellAddress,
1695 2
                'objectID' => $noteObjID,
1696 2
                'author' => $noteAuthor,
1697
            ];
1698
        } else {
1699
            $extension = false;
1700
            if ($cellAddress == '$B$65536') {
1701
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
1702
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
1703
                //        max 2048 bytes will probably throw a wobbly.
1704
                $row = self::getUInt2d($recordData, 0);
1705
                $extension = true;
1706
                $cellAddress = array_pop(array_keys($this->phpSheet->getComments()));
1707
            }
1708
1709
            $cellAddress = str_replace('$', '', $cellAddress);
1710
            $noteLength = self::getUInt2d($recordData, 4);
1711
            $noteText = trim(substr($recordData, 6));
1712
1713
            if ($extension) {
1714
                //    Concatenate this extension with the currently set comment for the cell
1715
                $comment = $this->phpSheet->getComment($cellAddress);
1716
                $commentText = $comment->getText()->getPlainText();
1717
                $comment->setText($this->parseRichText($commentText . $noteText));
1718
            } else {
1719
                //    Set comment for the cell
1720
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1721
//                                                    ->setAuthor($author)
1722
            }
1723
        }
1724 2
    }
1725
1726
    /**
1727
     * The TEXT Object record contains the text associated with a cell annotation.
1728
     */
1729 2
    private function readTextObject(): void
1730
    {
1731 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
1732 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1733
1734
        // move stream pointer to next record
1735 2
        $this->pos += 4 + $length;
1736
1737 2
        if ($this->readDataOnly) {
1738
            return;
1739
        }
1740
1741
        // recordData consists of an array of subrecords looking like this:
1742
        //    grbit: 2 bytes; Option Flags
1743
        //    rot: 2 bytes; rotation
1744
        //    cchText: 2 bytes; length of the text (in the first continue record)
1745
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
1746
        // followed by the continuation records containing the actual text and formatting
1747 2
        $grbitOpts = self::getUInt2d($recordData, 0);
1748 2
        $rot = self::getUInt2d($recordData, 2);
1749 2
        $cchText = self::getUInt2d($recordData, 10);
1750 2
        $cbRuns = self::getUInt2d($recordData, 12);
1751 2
        $text = $this->getSplicedRecordData();
1752
1753 2
        $textByte = $text['spliceOffsets'][1] - $text['spliceOffsets'][0] - 1;
1754 2
        $textStr = substr($text['recordData'], $text['spliceOffsets'][0] + 1, $textByte);
1755
        // get 1 byte
1756 2
        $is16Bit = ord($text['recordData'][0]);
1757
        // it is possible to use a compressed format,
1758
        // which omits the high bytes of all characters, if they are all zero
1759 2
        if (($is16Bit & 0x01) === 0) {
1760 2
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
1761
        } else {
1762
            $textStr = $this->decodeCodepage($textStr);
1763
        }
1764
1765 2
        $this->textObjects[$this->textObjRef] = [
1766 2
            'text' => $textStr,
1767 2
            'format' => substr($text['recordData'], $text['spliceOffsets'][1], $cbRuns),
1768 2
            'alignment' => $grbitOpts,
1769 2
            'rotation' => $rot,
1770
        ];
1771 2
    }
1772
1773
    /**
1774
     * Read BOF.
1775
     */
1776 39
    private function readBof(): void
1777
    {
1778 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
1779 39
        $recordData = substr($this->data, $this->pos + 4, $length);
1780
1781
        // move stream pointer to next record
1782 39
        $this->pos += 4 + $length;
1783
1784
        // offset: 2; size: 2; type of the following data
1785 39
        $substreamType = self::getUInt2d($recordData, 2);
1786
1787
        switch ($substreamType) {
1788 39
            case self::XLS_WORKBOOKGLOBALS:
1789 39
                $version = self::getUInt2d($recordData, 0);
1790 39
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
1791
                    throw new Exception('Cannot read this Excel file. Version is too old.');
1792
                }
1793 39
                $this->version = $version;
1794
1795 39
                break;
1796 37
            case self::XLS_WORKSHEET:
1797
                // do not use this version information for anything
1798
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
1799 37
                break;
1800
            default:
1801
                // substream, e.g. chart
1802
                // just skip the entire substream
1803
                do {
1804
                    $code = self::getUInt2d($this->data, $this->pos);
1805
                    $this->readDefault();
1806
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
1807
1808
                break;
1809
        }
1810 39
    }
1811
1812
    /**
1813
     * FILEPASS.
1814
     *
1815
     * This record is part of the File Protection Block. It
1816
     * contains information about the read/write password of the
1817
     * file. All record contents following this record will be
1818
     * encrypted.
1819
     *
1820
     * --    "OpenOffice.org's Documentation of the Microsoft
1821
     *         Excel File Format"
1822
     *
1823
     * The decryption functions and objects used from here on in
1824
     * are based on the source of Spreadsheet-ParseExcel:
1825
     * https://metacpan.org/release/Spreadsheet-ParseExcel
1826
     */
1827
    private function readFilepass(): void
1828
    {
1829
        $length = self::getUInt2d($this->data, $this->pos + 2);
1830
1831
        if ($length != 54) {
1832
            throw new Exception('Unexpected file pass record length');
1833
        }
1834
1835
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1836
1837
        // move stream pointer to next record
1838
        $this->pos += 4 + $length;
1839
1840
        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
1841
            throw new Exception('Decryption password incorrect');
1842
        }
1843
1844
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
1845
1846
        // Decryption required from the record after next onwards
1847
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
1848
    }
1849
1850
    /**
1851
     * Make an RC4 decryptor for the given block.
1852
     *
1853
     * @param int $block Block for which to create decrypto
1854
     * @param string $valContext MD5 context state
1855
     *
1856
     * @return Xls\RC4
1857
     */
1858
    private function makeKey($block, $valContext)
1859
    {
1860
        $pwarray = str_repeat("\0", 64);
1861
1862
        for ($i = 0; $i < 5; ++$i) {
1863
            $pwarray[$i] = $valContext[$i];
1864
        }
1865
1866
        $pwarray[5] = chr($block & 0xff);
1867
        $pwarray[6] = chr(($block >> 8) & 0xff);
1868
        $pwarray[7] = chr(($block >> 16) & 0xff);
1869
        $pwarray[8] = chr(($block >> 24) & 0xff);
1870
1871
        $pwarray[9] = "\x80";
1872
        $pwarray[56] = "\x48";
1873
1874
        $md5 = new Xls\MD5();
1875
        $md5->add($pwarray);
1876
1877
        $s = $md5->getContext();
1878
1879
        return new Xls\RC4($s);
1880
    }
1881
1882
    /**
1883
     * Verify RC4 file password.
1884
     *
1885
     * @param string $password Password to check
1886
     * @param string $docid Document id
1887
     * @param string $salt_data Salt data
1888
     * @param string $hashedsalt_data Hashed salt data
1889
     * @param string $valContext Set to the MD5 context of the value
1890
     *
1891
     * @return bool Success
1892
     */
1893
    private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext)
1894
    {
1895
        $pwarray = str_repeat("\0", 64);
1896
1897
        $iMax = strlen($password);
1898
        for ($i = 0; $i < $iMax; ++$i) {
1899
            $o = ord(substr($password, $i, 1));
1900
            $pwarray[2 * $i] = chr($o & 0xff);
1901
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xff);
1902
        }
1903
        $pwarray[2 * $i] = chr(0x80);
1904
        $pwarray[56] = chr(($i << 4) & 0xff);
1905
1906
        $md5 = new Xls\MD5();
1907
        $md5->add($pwarray);
1908
1909
        $mdContext1 = $md5->getContext();
1910
1911
        $offset = 0;
1912
        $keyoffset = 0;
1913
        $tocopy = 5;
1914
1915
        $md5->reset();
1916
1917
        while ($offset != 16) {
1918
            if ((64 - $offset) < 5) {
1919
                $tocopy = 64 - $offset;
1920
            }
1921
            for ($i = 0; $i <= $tocopy; ++$i) {
1922
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
1923
            }
1924
            $offset += $tocopy;
1925
1926
            if ($offset == 64) {
1927
                $md5->add($pwarray);
1928
                $keyoffset = $tocopy;
1929
                $tocopy = 5 - $tocopy;
1930
                $offset = 0;
1931
1932
                continue;
1933
            }
1934
1935
            $keyoffset = 0;
1936
            $tocopy = 5;
1937
            for ($i = 0; $i < 16; ++$i) {
1938
                $pwarray[$offset + $i] = $docid[$i];
1939
            }
1940
            $offset += 16;
1941
        }
1942
1943
        $pwarray[16] = "\x80";
1944
        for ($i = 0; $i < 47; ++$i) {
1945
            $pwarray[17 + $i] = "\0";
1946
        }
1947
        $pwarray[56] = "\x80";
1948
        $pwarray[57] = "\x0a";
1949
1950
        $md5->add($pwarray);
1951
        $valContext = $md5->getContext();
1952
1953
        $key = $this->makeKey(0, $valContext);
1954
1955
        $salt = $key->RC4($salt_data);
1956
        $hashedsalt = $key->RC4($hashedsalt_data);
1957
1958
        $salt .= "\x80" . str_repeat("\0", 47);
1959
        $salt[56] = "\x80";
1960
1961
        $md5->reset();
1962
        $md5->add($salt);
1963
        $mdContext2 = $md5->getContext();
1964
1965
        return $mdContext2 == $hashedsalt;
1966
    }
1967
1968
    /**
1969
     * CODEPAGE.
1970
     *
1971
     * This record stores the text encoding used to write byte
1972
     * strings, stored as MS Windows code page identifier.
1973
     *
1974
     * --    "OpenOffice.org's Documentation of the Microsoft
1975
     *         Excel File Format"
1976
     */
1977 34
    private function readCodepage(): void
1978
    {
1979 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
1980 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1981
1982
        // move stream pointer to next record
1983 34
        $this->pos += 4 + $length;
1984
1985
        // offset: 0; size: 2; code page identifier
1986 34
        $codepage = self::getUInt2d($recordData, 0);
1987
1988 34
        $this->codepage = CodePage::numberToName($codepage);
1989 34
    }
1990
1991
    /**
1992
     * DATEMODE.
1993
     *
1994
     * This record specifies the base date for displaying date
1995
     * values. All dates are stored as count of days past this
1996
     * base date. In BIFF2-BIFF4 this record is part of the
1997
     * Calculation Settings Block. In BIFF5-BIFF8 it is
1998
     * stored in the Workbook Globals Substream.
1999
     *
2000
     * --    "OpenOffice.org's Documentation of the Microsoft
2001
     *         Excel File Format"
2002
     */
2003 35
    private function readDateMode(): void
2004
    {
2005 35
        $length = self::getUInt2d($this->data, $this->pos + 2);
2006 35
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2007
2008
        // move stream pointer to next record
2009 35
        $this->pos += 4 + $length;
2010
2011
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
2012 35
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
2013 35
        if (ord($recordData[0]) == 1) {
2014
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
2015
        }
2016 35
    }
2017
2018
    /**
2019
     * Read a FONT record.
2020
     */
2021 36
    private function readFont(): void
2022
    {
2023 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
2024 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2025
2026
        // move stream pointer to next record
2027 36
        $this->pos += 4 + $length;
2028
2029 36
        if (!$this->readDataOnly) {
2030 35
            $objFont = new Font();
2031
2032
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
2033 35
            $size = self::getUInt2d($recordData, 0);
2034 35
            $objFont->setSize($size / 20);
2035
2036
            // offset: 2; size: 2; option flags
2037
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
2038
            // bit: 1; mask 0x0002; italic
2039 35
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
2040 35
            if ($isItalic) {
2041 8
                $objFont->setItalic(true);
2042
            }
2043
2044
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
2045
            // bit: 3; mask 0x0008; strikethrough
2046 35
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
2047 35
            if ($isStrike) {
2048
                $objFont->setStrikethrough(true);
2049
            }
2050
2051
            // offset: 4; size: 2; colour index
2052 35
            $colorIndex = self::getUInt2d($recordData, 4);
2053 35
            $objFont->colorIndex = $colorIndex;
2054
2055
            // offset: 6; size: 2; font weight
2056 35
            $weight = self::getUInt2d($recordData, 6);
2057
            switch ($weight) {
2058 35
                case 0x02BC:
2059 20
                    $objFont->setBold(true);
2060
2061 20
                    break;
2062
            }
2063
2064
            // offset: 8; size: 2; escapement type
2065 35
            $escapement = self::getUInt2d($recordData, 8);
2066
            switch ($escapement) {
2067 35
                case 0x0001:
2068
                    $objFont->setSuperscript(true);
2069
2070
                    break;
2071 35
                case 0x0002:
2072
                    $objFont->setSubscript(true);
2073
2074
                    break;
2075
            }
2076
2077
            // offset: 10; size: 1; underline type
2078 35
            $underlineType = ord($recordData[10]);
2079
            switch ($underlineType) {
2080 35
                case 0x00:
2081 35
                    break; // no underline
2082 3
                case 0x01:
2083 3
                    $objFont->setUnderline(Font::UNDERLINE_SINGLE);
2084
2085 3
                    break;
2086
                case 0x02:
2087
                    $objFont->setUnderline(Font::UNDERLINE_DOUBLE);
2088
2089
                    break;
2090
                case 0x21:
2091
                    $objFont->setUnderline(Font::UNDERLINE_SINGLEACCOUNTING);
2092
2093
                    break;
2094
                case 0x22:
2095
                    $objFont->setUnderline(Font::UNDERLINE_DOUBLEACCOUNTING);
2096
2097
                    break;
2098
            }
2099
2100
            // offset: 11; size: 1; font family
2101
            // offset: 12; size: 1; character set
2102
            // offset: 13; size: 1; not used
2103
            // offset: 14; size: var; font name
2104 35
            if ($this->version == self::XLS_BIFF8) {
2105 35
                $string = self::readUnicodeStringShort(substr($recordData, 14));
2106
            } else {
2107
                $string = $this->readByteStringShort(substr($recordData, 14));
2108
            }
2109 35
            $objFont->setName($string['value']);
2110
2111 35
            $this->objFonts[] = $objFont;
2112
        }
2113 36
    }
2114
2115
    /**
2116
     * FORMAT.
2117
     *
2118
     * This record contains information about a number format.
2119
     * All FORMAT records occur together in a sequential list.
2120
     *
2121
     * In BIFF2-BIFF4 other records referencing a FORMAT record
2122
     * contain a zero-based index into this list. From BIFF5 on
2123
     * the FORMAT record contains the index itself that will be
2124
     * used by other records.
2125
     *
2126
     * --    "OpenOffice.org's Documentation of the Microsoft
2127
     *         Excel File Format"
2128
     */
2129 22
    private function readFormat(): void
2130
    {
2131 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
2132 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2133
2134
        // move stream pointer to next record
2135 22
        $this->pos += 4 + $length;
2136
2137 22
        if (!$this->readDataOnly) {
2138 21
            $indexCode = self::getUInt2d($recordData, 0);
2139
2140 21
            if ($this->version == self::XLS_BIFF8) {
2141 21
                $string = self::readUnicodeStringLong(substr($recordData, 2));
2142
            } else {
2143
                // BIFF7
2144
                $string = $this->readByteStringShort(substr($recordData, 2));
2145
            }
2146
2147 21
            $formatString = $string['value'];
2148 21
            $this->formats[$indexCode] = $formatString;
2149
        }
2150 22
    }
2151
2152
    /**
2153
     * XF - Extended Format.
2154
     *
2155
     * This record contains formatting information for cells, rows, columns or styles.
2156
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
2157
     * and 1 cell XF.
2158
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
2159
     * and XF record 15 is a cell XF
2160
     * We only read the first cell style XF and skip the remaining cell style XF records
2161
     * We read all cell XF records.
2162
     *
2163
     * --    "OpenOffice.org's Documentation of the Microsoft
2164
     *         Excel File Format"
2165
     */
2166 36
    private function readXf(): void
2167
    {
2168 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
2169 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2170
2171
        // move stream pointer to next record
2172 36
        $this->pos += 4 + $length;
2173
2174 36
        $objStyle = new Style();
2175
2176 36
        if (!$this->readDataOnly) {
2177
            // offset:  0; size: 2; Index to FONT record
2178 35
            if (self::getUInt2d($recordData, 0) < 4) {
2179 35
                $fontIndex = self::getUInt2d($recordData, 0);
2180
            } else {
2181
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
2182
                // check the OpenOffice documentation of the FONT record
2183 22
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
2184
            }
2185 35
            $objStyle->setFont($this->objFonts[$fontIndex]);
2186
2187
            // offset:  2; size: 2; Index to FORMAT record
2188 35
            $numberFormatIndex = self::getUInt2d($recordData, 2);
2189 35
            if (isset($this->formats[$numberFormatIndex])) {
2190
                // then we have user-defined format code
2191 19
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
2192 35
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
2193
                // then we have built-in format code
2194 35
                $numberFormat = ['formatCode' => $code];
2195
            } else {
2196
                // we set the general format code
2197 2
                $numberFormat = ['formatCode' => 'General'];
2198
            }
2199 35
            $objStyle->getNumberFormat()->setFormatCode($numberFormat['formatCode']);
2200
2201
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
2202
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
2203 35
            $xfTypeProt = self::getUInt2d($recordData, 4);
2204
            // bit 0; mask 0x01; 1 = cell is locked
2205 35
            $isLocked = (0x01 & $xfTypeProt) >> 0;
2206 35
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
2207
2208
            // bit 1; mask 0x02; 1 = Formula is hidden
2209 35
            $isHidden = (0x02 & $xfTypeProt) >> 1;
2210 35
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
2211
2212
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
2213 35
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
2214
2215
            // offset:  6; size: 1; Alignment and text break
2216
            // bit 2-0, mask 0x07; horizontal alignment
2217 35
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
2218
            switch ($horAlign) {
2219 35
                case 0:
2220 35
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_GENERAL);
2221
2222 35
                    break;
2223 14
                case 1:
2224 4
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT);
2225
2226 4
                    break;
2227 14
                case 2:
2228 11
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
2229
2230 11
                    break;
2231 4
                case 3:
2232 4
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
2233
2234 4
                    break;
2235 3
                case 4:
2236
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_FILL);
2237
2238
                    break;
2239 3
                case 5:
2240 3
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_JUSTIFY);
2241
2242 3
                    break;
2243
                case 6:
2244
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER_CONTINUOUS);
2245
2246
                    break;
2247
            }
2248
            // bit 3, mask 0x08; wrap text
2249 35
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
2250
            switch ($wrapText) {
2251 35
                case 0:
2252 35
                    $objStyle->getAlignment()->setWrapText(false);
2253
2254 35
                    break;
2255 3
                case 1:
2256 3
                    $objStyle->getAlignment()->setWrapText(true);
2257
2258 3
                    break;
2259
            }
2260
            // bit 6-4, mask 0x70; vertical alignment
2261 35
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
2262
            switch ($vertAlign) {
2263 35
                case 0:
2264
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_TOP);
2265
2266
                    break;
2267 35
                case 1:
2268 4
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
2269
2270 4
                    break;
2271 35
                case 2:
2272 35
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_BOTTOM);
2273
2274 35
                    break;
2275
                case 3:
2276
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_JUSTIFY);
2277
2278
                    break;
2279
            }
2280
2281 35
            if ($this->version == self::XLS_BIFF8) {
2282
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
2283 35
                $angle = ord($recordData[7]);
2284 35
                $rotation = 0;
2285 35
                if ($angle <= 90) {
2286 35
                    $rotation = $angle;
2287
                } elseif ($angle <= 180) {
2288
                    $rotation = 90 - $angle;
2289
                } elseif ($angle == 255) {
2290
                    $rotation = -165;
2291
                }
2292 35
                $objStyle->getAlignment()->setTextRotation($rotation);
2293
2294
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
2295
                // bit: 3-0; mask: 0x0F; indent level
2296 35
                $indent = (0x0F & ord($recordData[8])) >> 0;
2297 35
                $objStyle->getAlignment()->setIndent($indent);
2298
2299
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
2300 35
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
2301
                switch ($shrinkToFit) {
2302 35
                    case 0:
2303 35
                        $objStyle->getAlignment()->setShrinkToFit(false);
2304
2305 35
                        break;
2306 1
                    case 1:
2307 1
                        $objStyle->getAlignment()->setShrinkToFit(true);
2308
2309 1
                        break;
2310
                }
2311
2312
                // offset:  9; size: 1; Flags used for attribute groups
2313
2314
                // offset: 10; size: 4; Cell border lines and background area
2315
                // bit: 3-0; mask: 0x0000000F; left style
2316 35
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
2317 35
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
2318
                }
2319
                // bit: 7-4; mask: 0x000000F0; right style
2320 35
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
2321 35
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
2322
                }
2323
                // bit: 11-8; mask: 0x00000F00; top style
2324 35
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
2325 35
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
2326
                }
2327
                // bit: 15-12; mask: 0x0000F000; bottom style
2328 35
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
2329 35
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
2330
                }
2331
                // bit: 22-16; mask: 0x007F0000; left color
2332 35
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
2333
2334
                // bit: 29-23; mask: 0x3F800000; right color
2335 35
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
2336
2337
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
2338 35
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
2339
2340
                // bit: 31; mask: 0x80000000; 1 = diagonal line from bottom left to top right
2341 35
                $diagonalUp = (0x80000000 & self::getInt4d($recordData, 10)) >> 31 ? true : false;
2342
2343 35
                if ($diagonalUp == false && $diagonalDown == false) {
2344 35
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
2345
                } elseif ($diagonalUp == true && $diagonalDown == false) {
2346
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
2347
                } elseif ($diagonalUp == false && $diagonalDown == true) {
2348
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
2349
                } elseif ($diagonalUp == true && $diagonalDown == true) {
2350
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
2351
                }
2352
2353
                // offset: 14; size: 4;
2354
                // bit: 6-0; mask: 0x0000007F; top color
2355 35
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
2356
2357
                // bit: 13-7; mask: 0x00003F80; bottom color
2358 35
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
2359
2360
                // bit: 20-14; mask: 0x001FC000; diagonal color
2361 35
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
2362
2363
                // bit: 24-21; mask: 0x01E00000; diagonal style
2364 35
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
2365 35
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
2366
                }
2367
2368
                // bit: 31-26; mask: 0xFC000000 fill pattern
2369 35
                if ($fillType = Xls\Style\FillPattern::lookup((0xFC000000 & self::getInt4d($recordData, 14)) >> 26)) {
2370 35
                    $objStyle->getFill()->setFillType($fillType);
2371
                }
2372
                // offset: 18; size: 2; pattern and background colour
2373
                // bit: 6-0; mask: 0x007F; color index for pattern color
2374 35
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
2375
2376
                // bit: 13-7; mask: 0x3F80; color index for pattern background
2377 35
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
2378
            } else {
2379
                // BIFF5
2380
2381
                // offset: 7; size: 1; Text orientation and flags
2382
                $orientationAndFlags = ord($recordData[7]);
2383
2384
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
2385
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2386
                switch ($xfOrientation) {
2387
                    case 0:
2388
                        $objStyle->getAlignment()->setTextRotation(0);
2389
2390
                        break;
2391
                    case 1:
2392
                        $objStyle->getAlignment()->setTextRotation(-165);
2393
2394
                        break;
2395
                    case 2:
2396
                        $objStyle->getAlignment()->setTextRotation(90);
2397
2398
                        break;
2399
                    case 3:
2400
                        $objStyle->getAlignment()->setTextRotation(-90);
2401
2402
                        break;
2403
                }
2404
2405
                // offset: 8; size: 4; cell border lines and background area
2406
                $borderAndBackground = self::getInt4d($recordData, 8);
2407
2408
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
2409
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2410
2411
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
2412
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2413
2414
                // bit: 21-16; mask: 0x003F0000; fill pattern
2415
                $objStyle->getFill()->setFillType(Xls\Style\FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2416
2417
                // bit: 24-22; mask: 0x01C00000; bottom line style
2418
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2419
2420
                // bit: 31-25; mask: 0xFE000000; bottom line color
2421
                $objStyle->getBorders()->getBottom()->colorIndex = (0xFE000000 & $borderAndBackground) >> 25;
2422
2423
                // offset: 12; size: 4; cell border lines
2424
                $borderLines = self::getInt4d($recordData, 12);
2425
2426
                // bit: 2-0; mask: 0x00000007; top line style
2427
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2428
2429
                // bit: 5-3; mask: 0x00000038; left line style
2430
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2431
2432
                // bit: 8-6; mask: 0x000001C0; right line style
2433
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2434
2435
                // bit: 15-9; mask: 0x0000FE00; top line color index
2436
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2437
2438
                // bit: 22-16; mask: 0x007F0000; left line color index
2439
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2440
2441
                // bit: 29-23; mask: 0x3F800000; right line color index
2442
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2443
            }
2444
2445
            // add cellStyleXf or cellXf and update mapping
2446 35
            if ($isCellStyleXf) {
2447
                // we only read one style XF record which is always the first
2448 35
                if ($this->xfIndex == 0) {
2449 35
                    $this->spreadsheet->addCellStyleXf($objStyle);
2450 35
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
2451
                }
2452
            } else {
2453
                // we read all cell XF records
2454 35
                $this->spreadsheet->addCellXf($objStyle);
2455 35
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
2456
            }
2457
2458
            // update XF index for when we read next record
2459 35
            ++$this->xfIndex;
2460
        }
2461 36
    }
2462
2463 6
    private function readXfExt(): void
2464
    {
2465 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
2466 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2467
2468
        // move stream pointer to next record
2469 6
        $this->pos += 4 + $length;
2470
2471 6
        if (!$this->readDataOnly) {
2472
            // offset: 0; size: 2; 0x087D = repeated header
2473
2474
            // offset: 2; size: 2
2475
2476
            // offset: 4; size: 8; not used
2477
2478
            // offset: 12; size: 2; record version
2479
2480
            // offset: 14; size: 2; index to XF record which this record modifies
2481 6
            $ixfe = self::getUInt2d($recordData, 14);
2482
2483
            // offset: 16; size: 2; not used
2484
2485
            // offset: 18; size: 2; number of extension properties that follow
2486 6
            $cexts = self::getUInt2d($recordData, 18);
2487
2488
            // start reading the actual extension data
2489 6
            $offset = 20;
2490 6
            while ($offset < $length) {
2491
                // extension type
2492 6
                $extType = self::getUInt2d($recordData, $offset);
2493
2494
                // extension length
2495 6
                $cb = self::getUInt2d($recordData, $offset + 2);
2496
2497
                // extension data
2498 6
                $extData = substr($recordData, $offset + 4, $cb);
2499
2500
                switch ($extType) {
2501 6
                    case 4:        // fill start color
2502 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2503 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2504
2505 6
                        if ($xclfType == 2) {
2506 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2507
2508
                            // modify the relevant style property
2509 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2510 2
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2511 2
                                $fill->getStartColor()->setRGB($rgb);
2512 2
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
2513
                            }
2514
                        }
2515
2516 6
                        break;
2517 6
                    case 5:        // fill end color
2518 2
                        $xclfType = self::getUInt2d($extData, 0); // color type
2519 2
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2520
2521 2
                        if ($xclfType == 2) {
2522 2
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2523
2524
                            // modify the relevant style property
2525 2
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2526 2
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2527 2
                                $fill->getEndColor()->setRGB($rgb);
2528 2
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
2529
                            }
2530
                        }
2531
2532 2
                        break;
2533 6
                    case 7:        // border color top
2534 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2535 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2536
2537 6
                        if ($xclfType == 2) {
2538 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2539
2540
                            // modify the relevant style property
2541 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2542 2
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
2543 2
                                $top->getColor()->setRGB($rgb);
2544 2
                                $top->colorIndex = null; // normal color index does not apply, discard
2545
                            }
2546
                        }
2547
2548 6
                        break;
2549 6
                    case 8:        // border color bottom
2550 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2551 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2552
2553 6
                        if ($xclfType == 2) {
2554 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2555
2556
                            // modify the relevant style property
2557 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2558 2
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
2559 2
                                $bottom->getColor()->setRGB($rgb);
2560 2
                                $bottom->colorIndex = null; // normal color index does not apply, discard
2561
                            }
2562
                        }
2563
2564 6
                        break;
2565 6
                    case 9:        // border color left
2566 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2567 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2568
2569 6
                        if ($xclfType == 2) {
2570 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2571
2572
                            // modify the relevant style property
2573 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2574 2
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
2575 2
                                $left->getColor()->setRGB($rgb);
2576 2
                                $left->colorIndex = null; // normal color index does not apply, discard
2577
                            }
2578
                        }
2579
2580 6
                        break;
2581 6
                    case 10:        // border color right
2582 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2583 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2584
2585 6
                        if ($xclfType == 2) {
2586 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2587
2588
                            // modify the relevant style property
2589 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2590 2
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
2591 2
                                $right->getColor()->setRGB($rgb);
2592 2
                                $right->colorIndex = null; // normal color index does not apply, discard
2593
                            }
2594
                        }
2595
2596 6
                        break;
2597 6
                    case 11:        // border color diagonal
2598
                        $xclfType = self::getUInt2d($extData, 0); // color type
2599
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2600
2601
                        if ($xclfType == 2) {
2602
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2603
2604
                            // modify the relevant style property
2605
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2606
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
2607
                                $diagonal->getColor()->setRGB($rgb);
2608
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
2609
                            }
2610
                        }
2611
2612
                        break;
2613 6
                    case 13:    // font color
2614 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2615 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2616
2617 6
                        if ($xclfType == 2) {
2618 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2619
2620
                            // modify the relevant style property
2621 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2622 2
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
2623 2
                                $font->getColor()->setRGB($rgb);
2624 2
                                $font->colorIndex = null; // normal color index does not apply, discard
2625
                            }
2626
                        }
2627
2628 6
                        break;
2629
                }
2630
2631 6
                $offset += $cb;
2632
            }
2633
        }
2634 6
    }
2635
2636
    /**
2637
     * Read STYLE record.
2638
     */
2639 36
    private function readStyle(): void
2640
    {
2641 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
2642 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2643
2644
        // move stream pointer to next record
2645 36
        $this->pos += 4 + $length;
2646
2647 36
        if (!$this->readDataOnly) {
2648
            // offset: 0; size: 2; index to XF record and flag for built-in style
2649 35
            $ixfe = self::getUInt2d($recordData, 0);
2650
2651
            // bit: 11-0; mask 0x0FFF; index to XF record
2652 35
            $xfIndex = (0x0FFF & $ixfe) >> 0;
2653
2654
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
2655 35
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
2656
2657 35
            if ($isBuiltIn) {
2658
                // offset: 2; size: 1; identifier for built-in style
2659 35
                $builtInId = ord($recordData[2]);
2660
2661
                switch ($builtInId) {
2662 35
                    case 0x00:
2663
                        // currently, we are not using this for anything
2664 35
                        break;
2665
                    default:
2666 19
                        break;
2667
                }
2668
            }
2669
            // user-defined; not supported by PhpSpreadsheet
2670
        }
2671 36
    }
2672
2673
    /**
2674
     * Read PALETTE record.
2675
     */
2676 19
    private function readPalette(): void
2677
    {
2678 19
        $length = self::getUInt2d($this->data, $this->pos + 2);
2679 19
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2680
2681
        // move stream pointer to next record
2682 19
        $this->pos += 4 + $length;
2683
2684 19
        if (!$this->readDataOnly) {
2685
            // offset: 0; size: 2; number of following colors
2686 19
            $nm = self::getUInt2d($recordData, 0);
2687
2688
            // list of RGB colors
2689 19
            for ($i = 0; $i < $nm; ++$i) {
2690 19
                $rgb = substr($recordData, 2 + 4 * $i, 4);
2691 19
                $this->palette[] = self::readRGB($rgb);
2692
            }
2693
        }
2694 19
    }
2695
2696
    /**
2697
     * SHEET.
2698
     *
2699
     * This record is  located in the  Workbook Globals
2700
     * Substream  and represents a sheet inside the workbook.
2701
     * One SHEET record is written for each sheet. It stores the
2702
     * sheet name and a stream offset to the BOF record of the
2703
     * respective Sheet Substream within the Workbook Stream.
2704
     *
2705
     * --    "OpenOffice.org's Documentation of the Microsoft
2706
     *         Excel File Format"
2707
     */
2708 39
    private function readSheet(): void
2709
    {
2710 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
2711 39
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2712
2713
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
2714
        // NOTE: not encrypted
2715 39
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
2716
2717
        // move stream pointer to next record
2718 39
        $this->pos += 4 + $length;
2719
2720
        // offset: 4; size: 1; sheet state
2721 39
        switch (ord($recordData[4])) {
2722 39
            case 0x00:
2723 39
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2724
2725 39
                break;
2726
            case 0x01:
2727
                $sheetState = Worksheet::SHEETSTATE_HIDDEN;
2728
2729
                break;
2730
            case 0x02:
2731
                $sheetState = Worksheet::SHEETSTATE_VERYHIDDEN;
2732
2733
                break;
2734
            default:
2735
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2736
2737
                break;
2738
        }
2739
2740
        // offset: 5; size: 1; sheet type
2741 39
        $sheetType = ord($recordData[5]);
2742
2743
        // offset: 6; size: var; sheet name
2744 39
        if ($this->version == self::XLS_BIFF8) {
2745 39
            $string = self::readUnicodeStringShort(substr($recordData, 6));
2746 39
            $rec_name = $string['value'];
2747
        } elseif ($this->version == self::XLS_BIFF7) {
2748
            $string = $this->readByteStringShort(substr($recordData, 6));
2749
            $rec_name = $string['value'];
2750
        }
2751
2752 39
        $this->sheets[] = [
2753 39
            'name' => $rec_name,
2754 39
            'offset' => $rec_offset,
2755 39
            'sheetState' => $sheetState,
2756 39
            'sheetType' => $sheetType,
2757
        ];
2758 39
    }
2759
2760
    /**
2761
     * Read EXTERNALBOOK record.
2762
     */
2763 22
    private function readExternalBook(): void
2764
    {
2765 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
2766 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2767
2768
        // move stream pointer to next record
2769 22
        $this->pos += 4 + $length;
2770
2771
        // offset within record data
2772 22
        $offset = 0;
2773
2774
        // there are 4 types of records
2775 22
        if (strlen($recordData) > 4) {
2776
            // external reference
2777
            // offset: 0; size: 2; number of sheet names ($nm)
2778
            $nm = self::getUInt2d($recordData, 0);
2779
            $offset += 2;
2780
2781
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
2782
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
2783
            $offset += $encodedUrlString['size'];
2784
2785
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
2786
            $externalSheetNames = [];
2787
            for ($i = 0; $i < $nm; ++$i) {
2788
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
2789
                $externalSheetNames[] = $externalSheetNameString['value'];
2790
                $offset += $externalSheetNameString['size'];
2791
            }
2792
2793
            // store the record data
2794
            $this->externalBooks[] = [
2795
                'type' => 'external',
2796
                'encodedUrl' => $encodedUrlString['value'],
2797
                'externalSheetNames' => $externalSheetNames,
2798
            ];
2799 22
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
2800
            // internal reference
2801
            // offset: 0; size: 2; number of sheet in this document
2802
            // offset: 2; size: 2; 0x01 0x04
2803 22
            $this->externalBooks[] = [
2804 22
                'type' => 'internal',
2805
            ];
2806
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
2807
            // add-in function
2808
            // offset: 0; size: 2; 0x0001
2809
            $this->externalBooks[] = [
2810
                'type' => 'addInFunction',
2811
            ];
2812
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
2813
            // DDE links, OLE links
2814
            // offset: 0; size: 2; 0x0000
2815
            // offset: 2; size: var; encoded source document name
2816
            $this->externalBooks[] = [
2817
                'type' => 'DDEorOLE',
2818
            ];
2819
        }
2820 22
    }
2821
2822
    /**
2823
     * Read EXTERNNAME record.
2824
     */
2825
    private function readExternName(): void
2826
    {
2827
        $length = self::getUInt2d($this->data, $this->pos + 2);
2828
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2829
2830
        // move stream pointer to next record
2831
        $this->pos += 4 + $length;
2832
2833
        // external sheet references provided for named cells
2834
        if ($this->version == self::XLS_BIFF8) {
2835
            // offset: 0; size: 2; options
2836
            $options = self::getUInt2d($recordData, 0);
2837
2838
            // offset: 2; size: 2;
2839
2840
            // offset: 4; size: 2; not used
2841
2842
            // offset: 6; size: var
2843
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
2844
2845
            // offset: var; size: var; formula data
2846
            $offset = 6 + $nameString['size'];
2847
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
2848
2849
            $this->externalNames[] = [
2850
                'name' => $nameString['value'],
2851
                'formula' => $formula,
2852
            ];
2853
        }
2854
    }
2855
2856
    /**
2857
     * Read EXTERNSHEET record.
2858
     */
2859 22
    private function readExternSheet(): void
2860
    {
2861 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
2862 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2863
2864
        // move stream pointer to next record
2865 22
        $this->pos += 4 + $length;
2866
2867
        // external sheet references provided for named cells
2868 22
        if ($this->version == self::XLS_BIFF8) {
2869
            // offset: 0; size: 2; number of following ref structures
2870 22
            $nm = self::getUInt2d($recordData, 0);
2871 22
            for ($i = 0; $i < $nm; ++$i) {
2872 21
                $this->ref[] = [
2873
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
2874 21
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
2875
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
2876 21
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
2877
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
2878 21
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
2879
                ];
2880
            }
2881
        }
2882 22
    }
2883
2884
    /**
2885
     * DEFINEDNAME.
2886
     *
2887
     * This record is part of a Link Table. It contains the name
2888
     * and the token array of an internal defined name. Token
2889
     * arrays of defined names contain tokens with aberrant
2890
     * token classes.
2891
     *
2892
     * --    "OpenOffice.org's Documentation of the Microsoft
2893
     *         Excel File Format"
2894
     */
2895 6
    private function readDefinedName(): void
2896
    {
2897 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
2898 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2899
2900
        // move stream pointer to next record
2901 6
        $this->pos += 4 + $length;
2902
2903 6
        if ($this->version == self::XLS_BIFF8) {
2904
            // retrieves named cells
2905
2906
            // offset: 0; size: 2; option flags
2907 6
            $opts = self::getUInt2d($recordData, 0);
2908
2909
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
2910 6
            $isBuiltInName = (0x0020 & $opts) >> 5;
2911
2912
            // offset: 2; size: 1; keyboard shortcut
2913
2914
            // offset: 3; size: 1; length of the name (character count)
2915 6
            $nlen = ord($recordData[3]);
2916
2917
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
2918
            // note: there can also be additional data, this is not included in $flen
2919 6
            $flen = self::getUInt2d($recordData, 4);
2920
2921
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
2922 6
            $scope = self::getUInt2d($recordData, 8);
2923
2924
            // offset: 14; size: var; Name (Unicode string without length field)
2925 6
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
2926
2927
            // offset: var; size: $flen; formula data
2928 6
            $offset = 14 + $string['size'];
2929 6
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
2930
2931
            try {
2932 6
                $formula = $this->getFormulaFromStructure($formulaStructure);
2933
            } catch (PhpSpreadsheetException $e) {
2934
                $formula = '';
2935
            }
2936
2937 6
            $this->definedname[] = [
2938 6
                'isBuiltInName' => $isBuiltInName,
2939 6
                'name' => $string['value'],
2940 6
                'formula' => $formula,
2941 6
                'scope' => $scope,
2942
            ];
2943
        }
2944 6
    }
2945
2946
    /**
2947
     * Read MSODRAWINGGROUP record.
2948
     */
2949 6
    private function readMsoDrawingGroup(): void
2950
    {
2951 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
2952
2953
        // get spliced record data
2954 6
        $splicedRecordData = $this->getSplicedRecordData();
2955 6
        $recordData = $splicedRecordData['recordData'];
2956
2957 6
        $this->drawingGroupData .= $recordData;
2958 6
    }
2959
2960
    /**
2961
     * SST - Shared String Table.
2962
     *
2963
     * This record contains a list of all strings used anywhere
2964
     * in the workbook. Each string occurs only once. The
2965
     * workbook uses indexes into the list to reference the
2966
     * strings.
2967
     *
2968
     * --    "OpenOffice.org's Documentation of the Microsoft
2969
     *         Excel File Format"
2970
     */
2971 35
    private function readSst(): void
2972
    {
2973
        // offset within (spliced) record data
2974 35
        $pos = 0;
2975
2976
        // get spliced record data
2977 35
        $splicedRecordData = $this->getSplicedRecordData();
2978
2979 35
        $recordData = $splicedRecordData['recordData'];
2980 35
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
2981
2982
        // offset: 0; size: 4; total number of strings in the workbook
2983 35
        $pos += 4;
2984
2985
        // offset: 4; size: 4; number of following strings ($nm)
2986 35
        $nm = self::getInt4d($recordData, 4);
2987 35
        $pos += 4;
2988
2989
        // loop through the Unicode strings (16-bit length)
2990 35
        for ($i = 0; $i < $nm; ++$i) {
2991
            // number of characters in the Unicode string
2992 25
            $numChars = self::getUInt2d($recordData, $pos);
2993 25
            $pos += 2;
2994
2995
            // option flags
2996 25
            $optionFlags = ord($recordData[$pos]);
2997 25
            ++$pos;
2998
2999
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
3000 25
            $isCompressed = (($optionFlags & 0x01) == 0);
3001
3002
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
3003 25
            $hasAsian = (($optionFlags & 0x04) != 0);
3004
3005
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
3006 25
            $hasRichText = (($optionFlags & 0x08) != 0);
3007
3008 25
            if ($hasRichText) {
3009
                // number of Rich-Text formatting runs
3010 3
                $formattingRuns = self::getUInt2d($recordData, $pos);
3011 3
                $pos += 2;
3012
            }
3013
3014 25
            if ($hasAsian) {
3015
                // size of Asian phonetic setting
3016
                $extendedRunLength = self::getInt4d($recordData, $pos);
3017
                $pos += 4;
3018
            }
3019
3020
            // expected byte length of character array if not split
3021 25
            $len = ($isCompressed) ? $numChars : $numChars * 2;
3022
3023
            // look up limit position
3024 25
            foreach ($spliceOffsets as $spliceOffset) {
3025
                // it can happen that the string is empty, therefore we need
3026
                // <= and not just <
3027 25
                if ($pos <= $spliceOffset) {
3028 25
                    $limitpos = $spliceOffset;
3029
3030 25
                    break;
3031
                }
3032
            }
3033
3034 25
            if ($pos + $len <= $limitpos) {
3035
                // character array is not split between records
3036
3037 25
                $retstr = substr($recordData, $pos, $len);
3038 25
                $pos += $len;
3039
            } else {
3040
                // character array is split between records
3041
3042
                // first part of character array
3043
                $retstr = substr($recordData, $pos, $limitpos - $pos);
3044
3045
                $bytesRead = $limitpos - $pos;
3046
3047
                // remaining characters in Unicode string
3048
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
3049
3050
                $pos = $limitpos;
3051
3052
                // keep reading the characters
3053
                while ($charsLeft > 0) {
3054
                    // look up next limit position, in case the string span more than one continue record
3055
                    foreach ($spliceOffsets as $spliceOffset) {
3056
                        if ($pos < $spliceOffset) {
3057
                            $limitpos = $spliceOffset;
3058
3059
                            break;
3060
                        }
3061
                    }
3062
3063
                    // repeated option flags
3064
                    // OpenOffice.org documentation 5.21
3065
                    $option = ord($recordData[$pos]);
3066
                    ++$pos;
3067
3068
                    if ($isCompressed && ($option == 0)) {
3069
                        // 1st fragment compressed
3070
                        // this fragment compressed
3071
                        $len = min($charsLeft, $limitpos - $pos);
3072
                        $retstr .= substr($recordData, $pos, $len);
3073
                        $charsLeft -= $len;
3074
                        $isCompressed = true;
3075
                    } elseif (!$isCompressed && ($option != 0)) {
3076
                        // 1st fragment uncompressed
3077
                        // this fragment uncompressed
3078
                        $len = min($charsLeft * 2, $limitpos - $pos);
3079
                        $retstr .= substr($recordData, $pos, $len);
3080
                        $charsLeft -= $len / 2;
3081
                        $isCompressed = false;
3082
                    } elseif (!$isCompressed && ($option == 0)) {
3083
                        // 1st fragment uncompressed
3084
                        // this fragment compressed
3085
                        $len = min($charsLeft, $limitpos - $pos);
3086
                        for ($j = 0; $j < $len; ++$j) {
3087
                            $retstr .= $recordData[$pos + $j]
3088
                            . chr(0);
3089
                        }
3090
                        $charsLeft -= $len;
3091
                        $isCompressed = false;
3092
                    } else {
3093
                        // 1st fragment compressed
3094
                        // this fragment uncompressed
3095
                        $newstr = '';
3096
                        $jMax = strlen($retstr);
3097
                        for ($j = 0; $j < $jMax; ++$j) {
3098
                            $newstr .= $retstr[$j] . chr(0);
3099
                        }
3100
                        $retstr = $newstr;
3101
                        $len = min($charsLeft * 2, $limitpos - $pos);
3102
                        $retstr .= substr($recordData, $pos, $len);
3103
                        $charsLeft -= $len / 2;
3104
                        $isCompressed = false;
3105
                    }
3106
3107
                    $pos += $len;
3108
                }
3109
            }
3110
3111
            // convert to UTF-8
3112 25
            $retstr = self::encodeUTF16($retstr, $isCompressed);
3113
3114
            // read additional Rich-Text information, if any
3115 25
            $fmtRuns = [];
3116 25
            if ($hasRichText) {
3117
                // list of formatting runs
3118 3
                for ($j = 0; $j < $formattingRuns; ++$j) {
3119
                    // first formatted character; zero-based
3120 3
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
3121
3122
                    // index to font record
3123 3
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
3124
3125 3
                    $fmtRuns[] = [
3126 3
                        'charPos' => $charPos,
3127 3
                        'fontIndex' => $fontIndex,
3128
                    ];
3129
                }
3130 3
                $pos += 4 * $formattingRuns;
3131
            }
3132
3133
            // read additional Asian phonetics information, if any
3134 25
            if ($hasAsian) {
3135
                // For Asian phonetic settings, we skip the extended string data
3136
                $pos += $extendedRunLength;
3137
            }
3138
3139
            // store the shared sting
3140 25
            $this->sst[] = [
3141 25
                'value' => $retstr,
3142 25
                'fmtRuns' => $fmtRuns,
3143
            ];
3144
        }
3145
3146
        // getSplicedRecordData() takes care of moving current position in data stream
3147 35
    }
3148
3149
    /**
3150
     * Read PRINTGRIDLINES record.
3151
     */
3152 34
    private function readPrintGridlines(): void
3153
    {
3154 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
3155 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3156
3157
        // move stream pointer to next record
3158 34
        $this->pos += 4 + $length;
3159
3160 34
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3161
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
3162 33
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
3163 33
            $this->phpSheet->setPrintGridlines($printGridlines);
3164
        }
3165 34
    }
3166
3167
    /**
3168
     * Read DEFAULTROWHEIGHT record.
3169
     */
3170 21
    private function readDefaultRowHeight(): void
3171
    {
3172 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3173 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3174
3175
        // move stream pointer to next record
3176 21
        $this->pos += 4 + $length;
3177
3178
        // offset: 0; size: 2; option flags
3179
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
3180 21
        $height = self::getUInt2d($recordData, 2);
3181 21
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
3182 21
    }
3183
3184
    /**
3185
     * Read SHEETPR record.
3186
     */
3187 35
    private function readSheetPr(): void
3188
    {
3189 35
        $length = self::getUInt2d($this->data, $this->pos + 2);
3190 35
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3191
3192
        // move stream pointer to next record
3193 35
        $this->pos += 4 + $length;
3194
3195
        // offset: 0; size: 2
3196
3197
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
3198 35
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
3199 35
        $this->phpSheet->setShowSummaryBelow($isSummaryBelow);
3200
3201
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
3202 35
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
3203 35
        $this->phpSheet->setShowSummaryRight($isSummaryRight);
3204
3205
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
3206
        // this corresponds to radio button setting in page setup dialog in Excel
3207 35
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
3208 35
    }
3209
3210
    /**
3211
     * Read HORIZONTALPAGEBREAKS record.
3212
     */
3213 1
    private function readHorizontalPageBreaks(): void
3214
    {
3215 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
3216 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3217
3218
        // move stream pointer to next record
3219 1
        $this->pos += 4 + $length;
3220
3221 1
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3222
            // offset: 0; size: 2; number of the following row index structures
3223 1
            $nm = self::getUInt2d($recordData, 0);
3224
3225
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3226 1
            for ($i = 0; $i < $nm; ++$i) {
3227
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3228
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3229
                $cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3230
3231
                // not sure why two column indexes are necessary?
3232
                $this->phpSheet->setBreakByColumnAndRow($cf + 1, $r, Worksheet::BREAK_ROW);
3233
            }
3234
        }
3235 1
    }
3236
3237
    /**
3238
     * Read VERTICALPAGEBREAKS record.
3239
     */
3240 1
    private function readVerticalPageBreaks(): void
3241
    {
3242 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
3243 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3244
3245
        // move stream pointer to next record
3246 1
        $this->pos += 4 + $length;
3247
3248 1
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3249
            // offset: 0; size: 2; number of the following column index structures
3250 1
            $nm = self::getUInt2d($recordData, 0);
3251
3252
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3253 1
            for ($i = 0; $i < $nm; ++$i) {
3254
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3255
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3256
                $rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3257
3258
                // not sure why two row indexes are necessary?
3259
                $this->phpSheet->setBreakByColumnAndRow($c + 1, $rf, Worksheet::BREAK_COLUMN);
3260
            }
3261
        }
3262 1
    }
3263
3264
    /**
3265
     * Read HEADER record.
3266
     */
3267 34
    private function readHeader(): void
3268
    {
3269 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
3270 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3271
3272
        // move stream pointer to next record
3273 34
        $this->pos += 4 + $length;
3274
3275 34
        if (!$this->readDataOnly) {
3276
            // offset: 0; size: var
3277
            // realized that $recordData can be empty even when record exists
3278 33
            if ($recordData) {
3279 17
                if ($this->version == self::XLS_BIFF8) {
3280 17
                    $string = self::readUnicodeStringLong($recordData);
3281
                } else {
3282
                    $string = $this->readByteStringShort($recordData);
3283
                }
3284
3285 17
                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
3286 17
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
3287
            }
3288
        }
3289 34
    }
3290
3291
    /**
3292
     * Read FOOTER record.
3293
     */
3294 34
    private function readFooter(): void
3295
    {
3296 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
3297 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3298
3299
        // move stream pointer to next record
3300 34
        $this->pos += 4 + $length;
3301
3302 34
        if (!$this->readDataOnly) {
3303
            // offset: 0; size: var
3304
            // realized that $recordData can be empty even when record exists
3305 33
            if ($recordData) {
3306 17
                if ($this->version == self::XLS_BIFF8) {
3307 17
                    $string = self::readUnicodeStringLong($recordData);
3308
                } else {
3309
                    $string = $this->readByteStringShort($recordData);
3310
                }
3311 17
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
3312 17
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
3313
            }
3314
        }
3315 34
    }
3316
3317
    /**
3318
     * Read HCENTER record.
3319
     */
3320 34
    private function readHcenter(): void
3321
    {
3322 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
3323 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3324
3325
        // move stream pointer to next record
3326 34
        $this->pos += 4 + $length;
3327
3328 34
        if (!$this->readDataOnly) {
3329
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
3330 33
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
3331
3332 33
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
3333
        }
3334 34
    }
3335
3336
    /**
3337
     * Read VCENTER record.
3338
     */
3339 34
    private function readVcenter(): void
3340
    {
3341 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
3342 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3343
3344
        // move stream pointer to next record
3345 34
        $this->pos += 4 + $length;
3346
3347 34
        if (!$this->readDataOnly) {
3348
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
3349 33
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
3350
3351 33
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
3352
        }
3353 34
    }
3354
3355
    /**
3356
     * Read LEFTMARGIN record.
3357
     */
3358 21
    private function readLeftMargin(): void
3359
    {
3360 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3361 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3362
3363
        // move stream pointer to next record
3364 21
        $this->pos += 4 + $length;
3365
3366 21
        if (!$this->readDataOnly) {
3367
            // offset: 0; size: 8
3368 21
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
3369
        }
3370 21
    }
3371
3372
    /**
3373
     * Read RIGHTMARGIN record.
3374
     */
3375 21
    private function readRightMargin(): void
3376
    {
3377 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3378 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3379
3380
        // move stream pointer to next record
3381 21
        $this->pos += 4 + $length;
3382
3383 21
        if (!$this->readDataOnly) {
3384
            // offset: 0; size: 8
3385 21
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
3386
        }
3387 21
    }
3388
3389
    /**
3390
     * Read TOPMARGIN record.
3391
     */
3392 21
    private function readTopMargin(): void
3393
    {
3394 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3395 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3396
3397
        // move stream pointer to next record
3398 21
        $this->pos += 4 + $length;
3399
3400 21
        if (!$this->readDataOnly) {
3401
            // offset: 0; size: 8
3402 21
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
3403
        }
3404 21
    }
3405
3406
    /**
3407
     * Read BOTTOMMARGIN record.
3408
     */
3409 21
    private function readBottomMargin(): void
3410
    {
3411 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3412 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3413
3414
        // move stream pointer to next record
3415 21
        $this->pos += 4 + $length;
3416
3417 21
        if (!$this->readDataOnly) {
3418
            // offset: 0; size: 8
3419 21
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
3420
        }
3421 21
    }
3422
3423
    /**
3424
     * Read PAGESETUP record.
3425
     */
3426 35
    private function readPageSetup(): void
3427
    {
3428 35
        $length = self::getUInt2d($this->data, $this->pos + 2);
3429 35
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3430
3431
        // move stream pointer to next record
3432 35
        $this->pos += 4 + $length;
3433
3434 35
        if (!$this->readDataOnly) {
3435
            // offset: 0; size: 2; paper size
3436 34
            $paperSize = self::getUInt2d($recordData, 0);
3437
3438
            // offset: 2; size: 2; scaling factor
3439 34
            $scale = self::getUInt2d($recordData, 2);
3440
3441
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
3442 34
            $fitToWidth = self::getUInt2d($recordData, 6);
3443
3444
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
3445 34
            $fitToHeight = self::getUInt2d($recordData, 8);
3446
3447
            // offset: 10; size: 2; option flags
3448
3449
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
3450 34
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
3451
3452
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
3453 34
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
3454
3455
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
3456
            // when this bit is set, do not use flags for those properties
3457 34
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
3458
3459 34
            if (!$isNotInit) {
3460 33
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
3461 33
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
3462 33
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
3463
3464 33
                $this->phpSheet->getPageSetup()->setScale($scale, false);
3465 33
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
3466 33
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
3467 33
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
3468
            }
3469
3470
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
3471 34
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
3472 34
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
3473
3474
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
3475 34
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
3476 34
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
3477
        }
3478 35
    }
3479
3480
    /**
3481
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
3482
     *   if this record is omitted, then it also means no sheet protection.
3483
     */
3484 2
    private function readProtect(): void
3485
    {
3486 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
3487 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3488
3489
        // move stream pointer to next record
3490 2
        $this->pos += 4 + $length;
3491
3492 2
        if ($this->readDataOnly) {
3493
            return;
3494
        }
3495
3496
        // offset: 0; size: 2;
3497
3498
        // bit 0, mask 0x01; 1 = sheet is protected
3499 2
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3500 2
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
3501 2
    }
3502
3503
    /**
3504
     * SCENPROTECT.
3505
     */
3506
    private function readScenProtect(): void
3507
    {
3508
        $length = self::getUInt2d($this->data, $this->pos + 2);
3509
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3510
3511
        // move stream pointer to next record
3512
        $this->pos += 4 + $length;
3513
3514
        if ($this->readDataOnly) {
3515
            return;
3516
        }
3517
3518
        // offset: 0; size: 2;
3519
3520
        // bit: 0, mask 0x01; 1 = scenarios are protected
3521
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3522
3523
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
3524
    }
3525
3526
    /**
3527
     * OBJECTPROTECT.
3528
     */
3529
    private function readObjectProtect(): void
3530
    {
3531
        $length = self::getUInt2d($this->data, $this->pos + 2);
3532
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3533
3534
        // move stream pointer to next record
3535
        $this->pos += 4 + $length;
3536
3537
        if ($this->readDataOnly) {
3538
            return;
3539
        }
3540
3541
        // offset: 0; size: 2;
3542
3543
        // bit: 0, mask 0x01; 1 = objects are protected
3544
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3545
3546
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
3547
    }
3548
3549
    /**
3550
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
3551
     */
3552
    private function readPassword(): void
3553
    {
3554
        $length = self::getUInt2d($this->data, $this->pos + 2);
3555
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3556
3557
        // move stream pointer to next record
3558
        $this->pos += 4 + $length;
3559
3560
        if (!$this->readDataOnly) {
3561
            // offset: 0; size: 2; 16-bit hash value of password
3562
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3563
            $this->phpSheet->getProtection()->setPassword($password, true);
3564
        }
3565
    }
3566
3567
    /**
3568
     * Read DEFCOLWIDTH record.
3569
     */
3570 35
    private function readDefColWidth(): void
3571
    {
3572 35
        $length = self::getUInt2d($this->data, $this->pos + 2);
3573 35
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3574
3575
        // move stream pointer to next record
3576 35
        $this->pos += 4 + $length;
3577
3578
        // offset: 0; size: 2; default column width
3579 35
        $width = self::getUInt2d($recordData, 0);
3580 35
        if ($width != 8) {
3581 2
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
3582
        }
3583 35
    }
3584
3585
    /**
3586
     * Read COLINFO record.
3587
     */
3588 30
    private function readColInfo(): void
3589
    {
3590 30
        $length = self::getUInt2d($this->data, $this->pos + 2);
3591 30
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3592
3593
        // move stream pointer to next record
3594 30
        $this->pos += 4 + $length;
3595
3596 30
        if (!$this->readDataOnly) {
3597
            // offset: 0; size: 2; index to first column in range
3598 29
            $firstColumnIndex = self::getUInt2d($recordData, 0);
3599
3600
            // offset: 2; size: 2; index to last column in range
3601 29
            $lastColumnIndex = self::getUInt2d($recordData, 2);
3602
3603
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
3604 29
            $width = self::getUInt2d($recordData, 4);
3605
3606
            // offset: 6; size: 2; index to XF record for default column formatting
3607 29
            $xfIndex = self::getUInt2d($recordData, 6);
3608
3609
            // offset: 8; size: 2; option flags
3610
            // bit: 0; mask: 0x0001; 1= columns are hidden
3611 29
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
3612
3613
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
3614 29
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
3615
3616
            // bit: 12; mask: 0x1000; 1 = collapsed
3617 29
            $isCollapsed = (0x1000 & self::getUInt2d($recordData, 8)) >> 12;
3618
3619
            // offset: 10; size: 2; not used
3620
3621 29
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
3622 29
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
3623 1
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
3624
3625 1
                    break;
3626
                }
3627 28
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
3628 28
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
3629 28
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
3630 28
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
3631 28
                if (isset($this->mapCellXfIndex[$xfIndex])) {
3632 27
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3633
                }
3634
            }
3635
        }
3636 30
    }
3637
3638
    /**
3639
     * ROW.
3640
     *
3641
     * This record contains the properties of a single row in a
3642
     * sheet. Rows and cells in a sheet are divided into blocks
3643
     * of 32 rows.
3644
     *
3645
     * --    "OpenOffice.org's Documentation of the Microsoft
3646
     *         Excel File Format"
3647
     */
3648 22
    private function readRow(): void
3649
    {
3650 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
3651 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3652
3653
        // move stream pointer to next record
3654 22
        $this->pos += 4 + $length;
3655
3656 22
        if (!$this->readDataOnly) {
3657
            // offset: 0; size: 2; index of this row
3658 21
            $r = self::getUInt2d($recordData, 0);
3659
3660
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
3661
3662
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
3663
3664
            // offset: 6; size: 2;
3665
3666
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
3667 21
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
3668
3669
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
3670 21
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
3671
3672 21
            if (!$useDefaultHeight) {
3673 20
                $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
3674
            }
3675
3676
            // offset: 8; size: 2; not used
3677
3678
            // offset: 10; size: 2; not used in BIFF5-BIFF8
3679
3680
            // offset: 12; size: 4; option flags and default row formatting
3681
3682
            // bit: 2-0: mask: 0x00000007; outline level of the row
3683 21
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
3684 21
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
3685
3686
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
3687 21
            $isCollapsed = (0x00000010 & self::getInt4d($recordData, 12)) >> 4;
3688 21
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
3689
3690
            // bit: 5; mask: 0x00000020; 1 = row is hidden
3691 21
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
3692 21
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
3693
3694
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
3695 21
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
3696
3697
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
3698 21
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
3699
3700 21
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
3701 3
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3702
            }
3703
        }
3704 22
    }
3705
3706
    /**
3707
     * Read RK record
3708
     * This record represents a cell that contains an RK value
3709
     * (encoded integer or floating-point value). If a
3710
     * floating-point value cannot be encoded to an RK value,
3711
     * a NUMBER record will be written. This record replaces the
3712
     * record INTEGER written in BIFF2.
3713
     *
3714
     * --    "OpenOffice.org's Documentation of the Microsoft
3715
     *         Excel File Format"
3716
     */
3717 14
    private function readRk(): void
3718
    {
3719 14
        $length = self::getUInt2d($this->data, $this->pos + 2);
3720 14
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3721
3722
        // move stream pointer to next record
3723 14
        $this->pos += 4 + $length;
3724
3725
        // offset: 0; size: 2; index to row
3726 14
        $row = self::getUInt2d($recordData, 0);
3727
3728
        // offset: 2; size: 2; index to column
3729 14
        $column = self::getUInt2d($recordData, 2);
3730 14
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3731
3732
        // Read cell?
3733 14
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3734
            // offset: 4; size: 2; index to XF record
3735 14
            $xfIndex = self::getUInt2d($recordData, 4);
3736
3737
            // offset: 6; size: 4; RK value
3738 14
            $rknum = self::getInt4d($recordData, 6);
3739 14
            $numValue = self::getIEEE754($rknum);
3740
3741 14
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3742 14
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3743
                // add style information
3744 12
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3745
            }
3746
3747
            // add cell
3748 14
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3749
        }
3750 14
    }
3751
3752
    /**
3753
     * Read LABELSST record
3754
     * This record represents a cell that contains a string. It
3755
     * replaces the LABEL record and RSTRING record used in
3756
     * BIFF2-BIFF5.
3757
     *
3758
     * --    "OpenOffice.org's Documentation of the Microsoft
3759
     *         Excel File Format"
3760
     */
3761 24
    private function readLabelSst(): void
3762
    {
3763 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
3764 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3765
3766
        // move stream pointer to next record
3767 24
        $this->pos += 4 + $length;
3768
3769
        // offset: 0; size: 2; index to row
3770 24
        $row = self::getUInt2d($recordData, 0);
3771
3772
        // offset: 2; size: 2; index to column
3773 24
        $column = self::getUInt2d($recordData, 2);
3774 24
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3775
3776 24
        $emptyCell = true;
3777
        // Read cell?
3778 24
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3779
            // offset: 4; size: 2; index to XF record
3780 24
            $xfIndex = self::getUInt2d($recordData, 4);
3781
3782
            // offset: 6; size: 4; index to SST record
3783 24
            $index = self::getInt4d($recordData, 6);
3784
3785
            // add cell
3786 24
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
3787
                // then we should treat as rich text
3788 3
                $richText = new RichText();
3789 3
                $charPos = 0;
3790 3
                $sstCount = count($this->sst[$index]['fmtRuns']);
3791 3
                for ($i = 0; $i <= $sstCount; ++$i) {
3792 3
                    if (isset($fmtRuns[$i])) {
3793 3
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $fmtRuns[$i]['charPos'] - $charPos);
3794 3
                        $charPos = $fmtRuns[$i]['charPos'];
3795
                    } else {
3796 3
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
3797
                    }
3798
3799 3
                    if (StringHelper::countCharacters($text) > 0) {
3800 3
                        if ($i == 0) { // first text run, no style
3801 2
                            $richText->createText($text);
3802
                        } else {
3803 3
                            $textRun = $richText->createTextRun($text);
3804 3
                            if (isset($fmtRuns[$i - 1])) {
3805 3
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
3806 3
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
3807
                                } else {
3808
                                    // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
3809
                                    // check the OpenOffice documentation of the FONT record
3810 3
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1;
3811
                                }
3812 3
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
3813
                            }
3814
                        }
3815
                    }
3816
                }
3817 3
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
3818 3
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3819 3
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
3820 3
                    $emptyCell = false;
3821
                }
3822
            } else {
3823 24
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
3824 24
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3825 24
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
3826 24
                    $emptyCell = false;
3827
                }
3828
            }
3829
3830 24
            if (!$this->readDataOnly && !$emptyCell && isset($this->mapCellXfIndex[$xfIndex])) {
3831
                // add style information
3832 23
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3833
            }
3834
        }
3835 24
    }
3836
3837
    /**
3838
     * Read MULRK record
3839
     * This record represents a cell range containing RK value
3840
     * cells. All cells are located in the same row.
3841
     *
3842
     * --    "OpenOffice.org's Documentation of the Microsoft
3843
     *         Excel File Format"
3844
     */
3845 13
    private function readMulRk(): void
3846
    {
3847 13
        $length = self::getUInt2d($this->data, $this->pos + 2);
3848 13
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3849
3850
        // move stream pointer to next record
3851 13
        $this->pos += 4 + $length;
3852
3853
        // offset: 0; size: 2; index to row
3854 13
        $row = self::getUInt2d($recordData, 0);
3855
3856
        // offset: 2; size: 2; index to first column
3857 13
        $colFirst = self::getUInt2d($recordData, 2);
3858
3859
        // offset: var; size: 2; index to last column
3860 13
        $colLast = self::getUInt2d($recordData, $length - 2);
3861 13
        $columns = $colLast - $colFirst + 1;
3862
3863
        // offset within record data
3864 13
        $offset = 4;
3865
3866 13
        for ($i = 1; $i <= $columns; ++$i) {
3867 13
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
3868
3869
            // Read cell?
3870 13
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3871
                // offset: var; size: 2; index to XF record
3872 13
                $xfIndex = self::getUInt2d($recordData, $offset);
3873
3874
                // offset: var; size: 4; RK value
3875 13
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
3876 13
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3877 13
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3878
                    // add style
3879 12
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3880
                }
3881
3882
                // add cell value
3883 13
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3884
            }
3885
3886 13
            $offset += 6;
3887
        }
3888 13
    }
3889
3890
    /**
3891
     * Read NUMBER record
3892
     * This record represents a cell that contains a
3893
     * floating-point value.
3894
     *
3895
     * --    "OpenOffice.org's Documentation of the Microsoft
3896
     *         Excel File Format"
3897
     */
3898 17
    private function readNumber(): void
3899
    {
3900 17
        $length = self::getUInt2d($this->data, $this->pos + 2);
3901 17
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3902
3903
        // move stream pointer to next record
3904 17
        $this->pos += 4 + $length;
3905
3906
        // offset: 0; size: 2; index to row
3907 17
        $row = self::getUInt2d($recordData, 0);
3908
3909
        // offset: 2; size 2; index to column
3910 17
        $column = self::getUInt2d($recordData, 2);
3911 17
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3912
3913
        // Read cell?
3914 17
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3915
            // offset 4; size: 2; index to XF record
3916 17
            $xfIndex = self::getUInt2d($recordData, 4);
3917
3918 17
            $numValue = self::extractNumber(substr($recordData, 6, 8));
3919
3920 17
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3921 17
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3922
                // add cell style
3923 16
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3924
            }
3925
3926
            // add cell value
3927 17
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3928
        }
3929 17
    }
3930
3931
    /**
3932
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
3933
     * This record contains the token array and the result of a
3934
     * formula cell.
3935
     *
3936
     * --    "OpenOffice.org's Documentation of the Microsoft
3937
     *         Excel File Format"
3938
     */
3939 15
    private function readFormula(): void
3940
    {
3941 15
        $length = self::getUInt2d($this->data, $this->pos + 2);
3942 15
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3943
3944
        // move stream pointer to next record
3945 15
        $this->pos += 4 + $length;
3946
3947
        // offset: 0; size: 2; row index
3948 15
        $row = self::getUInt2d($recordData, 0);
3949
3950
        // offset: 2; size: 2; col index
3951 15
        $column = self::getUInt2d($recordData, 2);
3952 15
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3953
3954
        // offset: 20: size: variable; formula structure
3955 15
        $formulaStructure = substr($recordData, 20);
3956
3957
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
3958 15
        $options = self::getUInt2d($recordData, 14);
3959
3960
        // bit: 0; mask: 0x0001; 1 = recalculate always
3961
        // bit: 1; mask: 0x0002; 1 = calculate on open
3962
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
3963 15
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
3964
3965
        // WARNING:
3966
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
3967
        // the formula data may be ordinary formula data, therefore we need to check
3968
        // explicitly for the tExp token (0x01)
3969 15
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
3970
3971 15
        if ($isPartOfSharedFormula) {
3972
            // part of shared formula which means there will be a formula with a tExp token and nothing else
3973
            // get the base cell, grab tExp token
3974
            $baseRow = self::getUInt2d($formulaStructure, 3);
3975
            $baseCol = self::getUInt2d($formulaStructure, 5);
3976
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
3977
        }
3978
3979
        // Read cell?
3980 15
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3981 15
            if ($isPartOfSharedFormula) {
3982
                // formula is added to this cell after the sheet has been read
3983
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
3984
            }
3985
3986
            // offset: 16: size: 4; not used
3987
3988
            // offset: 4; size: 2; XF index
3989 15
            $xfIndex = self::getUInt2d($recordData, 4);
3990
3991
            // offset: 6; size: 8; result of the formula
3992 15
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
3993
                // String formula. Result follows in appended STRING record
3994
                $dataType = DataType::TYPE_STRING;
3995
3996
                // read possible SHAREDFMLA record
3997
                $code = self::getUInt2d($this->data, $this->pos);
3998
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
3999
                    $this->readSharedFmla();
4000
                }
4001
4002
                // read STRING record
4003
                $value = $this->readString();
4004
            } elseif (
4005 15
                (ord($recordData[6]) == 1)
4006 15
                && (ord($recordData[12]) == 255)
4007 15
                && (ord($recordData[13]) == 255)
4008
            ) {
4009
                // Boolean formula. Result is in +2; 0=false, 1=true
4010
                $dataType = DataType::TYPE_BOOL;
4011
                $value = (bool) ord($recordData[8]);
4012
            } elseif (
4013 15
                (ord($recordData[6]) == 2)
4014 15
                && (ord($recordData[12]) == 255)
4015 15
                && (ord($recordData[13]) == 255)
4016
            ) {
4017
                // Error formula. Error code is in +2
4018 8
                $dataType = DataType::TYPE_ERROR;
4019 8
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
4020
            } elseif (
4021 15
                (ord($recordData[6]) == 3)
4022 15
                && (ord($recordData[12]) == 255)
4023 15
                && (ord($recordData[13]) == 255)
4024
            ) {
4025
                // Formula result is a null string
4026 1
                $dataType = DataType::TYPE_NULL;
4027 1
                $value = '';
4028
            } else {
4029
                // forumla result is a number, first 14 bytes like _NUMBER record
4030 15
                $dataType = DataType::TYPE_NUMERIC;
4031 15
                $value = self::extractNumber(substr($recordData, 6, 8));
4032
            }
4033
4034 15
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4035 15
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4036
                // add cell style
4037 14
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4038
            }
4039
4040
            // store the formula
4041 15
            if (!$isPartOfSharedFormula) {
4042
                // not part of shared formula
4043
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
4044
                try {
4045 15
                    if ($this->version != self::XLS_BIFF8) {
4046
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
4047
                    }
4048 15
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
4049 15
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
4050
                } catch (PhpSpreadsheetException $e) {
4051 15
                    $cell->setValueExplicit($value, $dataType);
4052
                }
4053
            } else {
4054
                if ($this->version == self::XLS_BIFF8) {
4055
                    // do nothing at this point, formula id added later in the code
4056
                } else {
4057
                    $cell->setValueExplicit($value, $dataType);
4058
                }
4059
            }
4060
4061
            // store the cached calculated value
4062 15
            $cell->setCalculatedValue($value);
4063
        }
4064 15
    }
4065
4066
    /**
4067
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
4068
     * which usually contains relative references.
4069
     * These will be used to construct the formula in each shared formula part after the sheet is read.
4070
     */
4071
    private function readSharedFmla(): void
4072
    {
4073
        $length = self::getUInt2d($this->data, $this->pos + 2);
4074
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4075
4076
        // move stream pointer to next record
4077
        $this->pos += 4 + $length;
4078
4079
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
4080
        $cellRange = substr($recordData, 0, 6);
4081
        $cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
4082
4083
        // offset: 6, size: 1; not used
4084
4085
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
4086
        $no = ord($recordData[7]);
4087
4088
        // offset: 8, size: var; Binary token array of the shared formula
4089
        $formula = substr($recordData, 8);
4090
4091
        // at this point we only store the shared formula for later use
4092
        $this->sharedFormulas[$this->baseCell] = $formula;
4093
    }
4094
4095
    /**
4096
     * Read a STRING record from current stream position and advance the stream pointer to next record
4097
     * This record is used for storing result from FORMULA record when it is a string, and
4098
     * it occurs directly after the FORMULA record.
4099
     *
4100
     * @return string The string contents as UTF-8
4101
     */
4102
    private function readString()
4103
    {
4104
        $length = self::getUInt2d($this->data, $this->pos + 2);
4105
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4106
4107
        // move stream pointer to next record
4108
        $this->pos += 4 + $length;
4109
4110
        if ($this->version == self::XLS_BIFF8) {
4111
            $string = self::readUnicodeStringLong($recordData);
4112
            $value = $string['value'];
4113
        } else {
4114
            $string = $this->readByteStringLong($recordData);
4115
            $value = $string['value'];
4116
        }
4117
4118
        return $value;
4119
    }
4120
4121
    /**
4122
     * Read BOOLERR record
4123
     * This record represents a Boolean value or error value
4124
     * cell.
4125
     *
4126
     * --    "OpenOffice.org's Documentation of the Microsoft
4127
     *         Excel File Format"
4128
     */
4129 9
    private function readBoolErr(): void
4130
    {
4131 9
        $length = self::getUInt2d($this->data, $this->pos + 2);
4132 9
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4133
4134
        // move stream pointer to next record
4135 9
        $this->pos += 4 + $length;
4136
4137
        // offset: 0; size: 2; row index
4138 9
        $row = self::getUInt2d($recordData, 0);
4139
4140
        // offset: 2; size: 2; column index
4141 9
        $column = self::getUInt2d($recordData, 2);
4142 9
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4143
4144
        // Read cell?
4145 9
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4146
            // offset: 4; size: 2; index to XF record
4147 9
            $xfIndex = self::getUInt2d($recordData, 4);
4148
4149
            // offset: 6; size: 1; the boolean value or error value
4150 9
            $boolErr = ord($recordData[6]);
4151
4152
            // offset: 7; size: 1; 0=boolean; 1=error
4153 9
            $isError = ord($recordData[7]);
4154
4155 9
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4156
            switch ($isError) {
4157 9
                case 0: // boolean
4158 9
                    $value = (bool) $boolErr;
4159
4160
                    // add cell value
4161 9
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
4162
4163 9
                    break;
4164
                case 1: // error type
4165
                    $value = Xls\ErrorCode::lookup($boolErr);
4166
4167
                    // add cell value
4168
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
4169
4170
                    break;
4171
            }
4172
4173 9
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4174
                // add cell style
4175 8
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4176
            }
4177
        }
4178 9
    }
4179
4180
    /**
4181
     * Read MULBLANK record
4182
     * This record represents a cell range of empty cells. All
4183
     * cells are located in the same row.
4184
     *
4185
     * --    "OpenOffice.org's Documentation of the Microsoft
4186
     *         Excel File Format"
4187
     */
4188 12
    private function readMulBlank(): void
4189
    {
4190 12
        $length = self::getUInt2d($this->data, $this->pos + 2);
4191 12
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4192
4193
        // move stream pointer to next record
4194 12
        $this->pos += 4 + $length;
4195
4196
        // offset: 0; size: 2; index to row
4197 12
        $row = self::getUInt2d($recordData, 0);
4198
4199
        // offset: 2; size: 2; index to first column
4200 12
        $fc = self::getUInt2d($recordData, 2);
4201
4202
        // offset: 4; size: 2 x nc; list of indexes to XF records
4203
        // add style information
4204 12
        if (!$this->readDataOnly && $this->readEmptyCells) {
4205 11
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
4206 11
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
4207
4208
                // Read cell?
4209 11
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4210 11
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
4211 11
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
4212 11
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4213
                    }
4214
                }
4215
            }
4216
        }
4217
4218
        // offset: 6; size 2; index to last column (not needed)
4219 12
    }
4220
4221
    /**
4222
     * Read LABEL record
4223
     * This record represents a cell that contains a string. In
4224
     * BIFF8 it is usually replaced by the LABELSST record.
4225
     * Excel still uses this record, if it copies unformatted
4226
     * text cells to the clipboard.
4227
     *
4228
     * --    "OpenOffice.org's Documentation of the Microsoft
4229
     *         Excel File Format"
4230
     */
4231 1
    private function readLabel(): void
4232
    {
4233 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
4234 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4235
4236
        // move stream pointer to next record
4237 1
        $this->pos += 4 + $length;
4238
4239
        // offset: 0; size: 2; index to row
4240 1
        $row = self::getUInt2d($recordData, 0);
4241
4242
        // offset: 2; size: 2; index to column
4243 1
        $column = self::getUInt2d($recordData, 2);
4244 1
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4245
4246
        // Read cell?
4247 1
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4248
            // offset: 4; size: 2; XF index
4249 1
            $xfIndex = self::getUInt2d($recordData, 4);
4250
4251
            // add cell value
4252
            // todo: what if string is very long? continue record
4253 1
            if ($this->version == self::XLS_BIFF8) {
4254 1
                $string = self::readUnicodeStringLong(substr($recordData, 6));
4255 1
                $value = $string['value'];
4256
            } else {
4257
                $string = $this->readByteStringLong(substr($recordData, 6));
4258
                $value = $string['value'];
4259
            }
4260 1
            if ($this->readEmptyCells || trim($value) !== '') {
4261 1
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4262 1
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4263
4264 1
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4265
                    // add cell style
4266 1
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4267
                }
4268
            }
4269
        }
4270 1
    }
4271
4272
    /**
4273
     * Read BLANK record.
4274
     */
4275 7
    private function readBlank(): void
4276
    {
4277 7
        $length = self::getUInt2d($this->data, $this->pos + 2);
4278 7
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4279
4280
        // move stream pointer to next record
4281 7
        $this->pos += 4 + $length;
4282
4283
        // offset: 0; size: 2; row index
4284 7
        $row = self::getUInt2d($recordData, 0);
4285
4286
        // offset: 2; size: 2; col index
4287 7
        $col = self::getUInt2d($recordData, 2);
4288 7
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
4289
4290
        // Read cell?
4291 7
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4292
            // offset: 4; size: 2; XF index
4293 7
            $xfIndex = self::getUInt2d($recordData, 4);
4294
4295
            // add style information
4296 7
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
4297 7
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4298
            }
4299
        }
4300 7
    }
4301
4302
    /**
4303
     * Read MSODRAWING record.
4304
     */
4305 6
    private function readMsoDrawing(): void
4306
    {
4307 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
4308
4309
        // get spliced record data
4310 6
        $splicedRecordData = $this->getSplicedRecordData();
4311 6
        $recordData = $splicedRecordData['recordData'];
4312
4313 6
        $this->drawingData .= $recordData;
4314 6
    }
4315
4316
    /**
4317
     * Read OBJ record.
4318
     */
4319 6
    private function readObj(): void
4320
    {
4321 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
4322 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4323
4324
        // move stream pointer to next record
4325 6
        $this->pos += 4 + $length;
4326
4327 6
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
4328
            return;
4329
        }
4330
4331
        // recordData consists of an array of subrecords looking like this:
4332
        //    ft: 2 bytes; ftCmo type (0x15)
4333
        //    cb: 2 bytes; size in bytes of ftCmo data
4334
        //    ot: 2 bytes; Object Type
4335
        //    id: 2 bytes; Object id number
4336
        //    grbit: 2 bytes; Option Flags
4337
        //    data: var; subrecord data
4338
4339
        // for now, we are just interested in the second subrecord containing the object type
4340 6
        $ftCmoType = self::getUInt2d($recordData, 0);
4341 6
        $cbCmoSize = self::getUInt2d($recordData, 2);
4342 6
        $otObjType = self::getUInt2d($recordData, 4);
4343 6
        $idObjID = self::getUInt2d($recordData, 6);
4344 6
        $grbitOpts = self::getUInt2d($recordData, 6);
4345
4346 6
        $this->objs[] = [
4347 6
            'ftCmoType' => $ftCmoType,
4348 6
            'cbCmoSize' => $cbCmoSize,
4349 6
            'otObjType' => $otObjType,
4350 6
            'idObjID' => $idObjID,
4351 6
            'grbitOpts' => $grbitOpts,
4352
        ];
4353 6
        $this->textObjRef = $idObjID;
4354 6
    }
4355
4356
    /**
4357
     * Read WINDOW2 record.
4358
     */
4359 36
    private function readWindow2(): void
4360
    {
4361 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
4362 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4363
4364
        // move stream pointer to next record
4365 36
        $this->pos += 4 + $length;
4366
4367
        // offset: 0; size: 2; option flags
4368 36
        $options = self::getUInt2d($recordData, 0);
4369
4370
        // offset: 2; size: 2; index to first visible row
4371 36
        $firstVisibleRow = self::getUInt2d($recordData, 2);
4372
4373
        // offset: 4; size: 2; index to first visible colum
4374 36
        $firstVisibleColumn = self::getUInt2d($recordData, 4);
4375 36
        if ($this->version === self::XLS_BIFF8) {
4376
            // offset:  8; size: 2; not used
4377
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
4378
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
4379
            // offset: 14; size: 4; not used
4380 36
            if (!isset($recordData[10])) {
4381
                $zoomscaleInPageBreakPreview = 0;
4382
            } else {
4383 36
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
4384
            }
4385
4386 36
            if ($zoomscaleInPageBreakPreview === 0) {
4387 36
                $zoomscaleInPageBreakPreview = 60;
4388
            }
4389
4390 36
            if (!isset($recordData[12])) {
4391
                $zoomscaleInNormalView = 0;
4392
            } else {
4393 36
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
4394
            }
4395
4396 36
            if ($zoomscaleInNormalView === 0) {
4397 21
                $zoomscaleInNormalView = 100;
4398
            }
4399
        }
4400
4401
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
4402 36
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
4403 36
        $this->phpSheet->setShowGridlines($showGridlines);
4404
4405
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
4406 36
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
4407 36
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
4408
4409
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
4410 36
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
4411
4412
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
4413 36
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
4414
4415
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
4416 36
        $isActive = (bool) ((0x0400 & $options) >> 10);
4417 36
        if ($isActive) {
4418 33
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
4419
        }
4420
4421
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
4422 36
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
4423
4424
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
4425
4426 36
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
4427
            //NOTE: this setting is inferior to page layout view(Excel2007-)
4428 36
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
4429 36
            $this->phpSheet->getSheetView()->setView($view);
4430 36
            if ($this->version === self::XLS_BIFF8) {
4431 36
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
4432 36
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
4433 36
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
4434
            }
4435
        }
4436 36
    }
4437
4438
    /**
4439
     * Read PLV Record(Created by Excel2007 or upper).
4440
     */
4441 20
    private function readPageLayoutView(): void
4442
    {
4443 20
        $length = self::getUInt2d($this->data, $this->pos + 2);
4444 20
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4445
4446
        // move stream pointer to next record
4447 20
        $this->pos += 4 + $length;
4448
4449
        // offset: 0; size: 2; rt
4450
        //->ignore
4451 20
        $rt = self::getUInt2d($recordData, 0);
4452
        // offset: 2; size: 2; grbitfr
4453
        //->ignore
4454 20
        $grbitFrt = self::getUInt2d($recordData, 2);
4455
        // offset: 4; size: 8; reserved
4456
        //->ignore
4457
4458
        // offset: 12; size 2; zoom scale
4459 20
        $wScalePLV = self::getUInt2d($recordData, 12);
4460
        // offset: 14; size 2; grbit
4461 20
        $grbit = self::getUInt2d($recordData, 14);
4462
4463
        // decomprise grbit
4464 20
        $fPageLayoutView = $grbit & 0x01;
4465 20
        $fRulerVisible = ($grbit >> 1) & 0x01; //no support
4466 20
        $fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
4467
4468 20
        if ($fPageLayoutView === 1) {
4469
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
4470
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
4471
        }
4472
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
4473 20
    }
4474
4475
    /**
4476
     * Read SCL record.
4477
     */
4478 1
    private function readScl(): void
4479
    {
4480 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
4481 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4482
4483
        // move stream pointer to next record
4484 1
        $this->pos += 4 + $length;
4485
4486
        // offset: 0; size: 2; numerator of the view magnification
4487 1
        $numerator = self::getUInt2d($recordData, 0);
4488
4489
        // offset: 2; size: 2; numerator of the view magnification
4490 1
        $denumerator = self::getUInt2d($recordData, 2);
4491
4492
        // set the zoom scale (in percent)
4493 1
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
4494 1
    }
4495
4496
    /**
4497
     * Read PANE record.
4498
     */
4499 5
    private function readPane(): void
4500
    {
4501 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4502 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4503
4504
        // move stream pointer to next record
4505 5
        $this->pos += 4 + $length;
4506
4507 5
        if (!$this->readDataOnly) {
4508
            // offset: 0; size: 2; position of vertical split
4509 5
            $px = self::getUInt2d($recordData, 0);
4510
4511
            // offset: 2; size: 2; position of horizontal split
4512 5
            $py = self::getUInt2d($recordData, 2);
4513
4514
            // offset: 4; size: 2; top most visible row in the bottom pane
4515 5
            $rwTop = self::getUInt2d($recordData, 4);
4516
4517
            // offset: 6; size: 2; first visible left column in the right pane
4518 5
            $colLeft = self::getUInt2d($recordData, 6);
4519
4520 5
            if ($this->frozen) {
4521
                // frozen panes
4522 5
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
4523 5
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
4524 5
                $this->phpSheet->freezePane($cell, $topLeftCell);
4525
            }
4526
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
4527
        }
4528 5
    }
4529
4530
    /**
4531
     * Read SELECTION record. There is one such record for each pane in the sheet.
4532
     */
4533 34
    private function readSelection(): void
4534
    {
4535 34
        $length = self::getUInt2d($this->data, $this->pos + 2);
4536 34
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4537
4538
        // move stream pointer to next record
4539 34
        $this->pos += 4 + $length;
4540
4541 34
        if (!$this->readDataOnly) {
4542
            // offset: 0; size: 1; pane identifier
4543 33
            $paneId = ord($recordData[0]);
4544
4545
            // offset: 1; size: 2; index to row of the active cell
4546 33
            $r = self::getUInt2d($recordData, 1);
4547
4548
            // offset: 3; size: 2; index to column of the active cell
4549 33
            $c = self::getUInt2d($recordData, 3);
4550
4551
            // offset: 5; size: 2; index into the following cell range list to the
4552
            //  entry that contains the active cell
4553 33
            $index = self::getUInt2d($recordData, 5);
4554
4555
            // offset: 7; size: var; cell range address list containing all selected cell ranges
4556 33
            $data = substr($recordData, 7);
4557 33
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
4558
4559 33
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
4560
4561
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
4562 33
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
4563
                $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
4564
            }
4565
4566
            // first row '1' + last row '65536' indicates that full column is selected
4567 33
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
4568
                $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
4569
            }
4570
4571
            // first column 'A' + last column 'IV' indicates that full row is selected
4572 33
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
4573 2
                $selectedCells = preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
4574
            }
4575
4576 33
            $this->phpSheet->setSelectedCells($selectedCells);
4577
        }
4578 34
    }
4579
4580 13
    private function includeCellRangeFiltered($cellRangeAddress)
4581
    {
4582 13
        $includeCellRange = true;
4583 13
        if ($this->getReadFilter() !== null) {
4584 13
            $includeCellRange = false;
4585 13
            $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
4586 13
            ++$rangeBoundaries[1][0];
4587 13
            for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
4588 13
                for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; ++$column) {
4589 13
                    if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
4590 13
                        $includeCellRange = true;
4591
4592 13
                        break 2;
4593
                    }
4594
                }
4595
            }
4596
        }
4597
4598 13
        return $includeCellRange;
4599
    }
4600
4601
    /**
4602
     * MERGEDCELLS.
4603
     *
4604
     * This record contains the addresses of merged cell ranges
4605
     * in the current sheet.
4606
     *
4607
     * --    "OpenOffice.org's Documentation of the Microsoft
4608
     *         Excel File Format"
4609
     */
4610 14
    private function readMergedCells(): void
4611
    {
4612 14
        $length = self::getUInt2d($this->data, $this->pos + 2);
4613 14
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4614
4615
        // move stream pointer to next record
4616 14
        $this->pos += 4 + $length;
4617
4618 14
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
4619 13
            $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData);
4620 13
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
4621
                if (
4622 13
                    (strpos($cellRangeAddress, ':') !== false) &&
4623 13
                    ($this->includeCellRangeFiltered($cellRangeAddress))
4624
                ) {
4625 13
                    $this->phpSheet->mergeCells($cellRangeAddress);
4626
                }
4627
            }
4628
        }
4629 14
    }
4630
4631
    /**
4632
     * Read HYPERLINK record.
4633
     */
4634 3
    private function readHyperLink(): void
4635
    {
4636 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4637 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4638
4639
        // move stream pointer forward to next record
4640 3
        $this->pos += 4 + $length;
4641
4642 3
        if (!$this->readDataOnly) {
4643
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
4644
            try {
4645 3
                $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData);
4646
            } catch (PhpSpreadsheetException $e) {
4647
                return;
4648
            }
4649
4650
            // offset: 8, size: 16; GUID of StdLink
4651
4652
            // offset: 24, size: 4; unknown value
4653
4654
            // offset: 28, size: 4; option flags
4655
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
4656 3
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
4657
4658
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
4659 3
            $isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
4660
4661
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
4662 3
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
4663
4664
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
4665 3
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
4666
4667
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
4668 3
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
4669
4670
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
4671 3
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
4672
4673
            // offset within record data
4674 3
            $offset = 32;
4675
4676 3
            if ($hasDesc) {
4677
                // offset: 32; size: var; character count of description text
4678 2
                $dl = self::getInt4d($recordData, 32);
4679
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
4680 2
                $desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
4681 2
                $offset += 4 + 2 * $dl;
4682
            }
4683 3
            if ($hasFrame) {
4684
                $fl = self::getInt4d($recordData, $offset);
4685
                $offset += 4 + 2 * $fl;
4686
            }
4687
4688
            // detect type of hyperlink (there are 4 types)
4689 3
            $hyperlinkType = null;
4690
4691 3
            if ($isUNC) {
4692
                $hyperlinkType = 'UNC';
4693 3
            } elseif (!$isFileLinkOrUrl) {
4694 3
                $hyperlinkType = 'workbook';
4695 3
            } elseif (ord($recordData[$offset]) == 0x03) {
4696
                $hyperlinkType = 'local';
4697 3
            } elseif (ord($recordData[$offset]) == 0xE0) {
4698 3
                $hyperlinkType = 'URL';
4699
            }
4700
4701
            switch ($hyperlinkType) {
4702 3
                case 'URL':
4703
                    // section 5.58.2: Hyperlink containing a URL
4704
                    // e.g. http://example.org/index.php
4705
4706
                    // offset: var; size: 16; GUID of URL Moniker
4707 3
                    $offset += 16;
4708
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
4709 3
                    $us = self::getInt4d($recordData, $offset);
4710 3
                    $offset += 4;
4711
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
4712 3
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
4713 3
                    $nullOffset = strpos($url, chr(0x00));
4714 3
                    if ($nullOffset) {
4715 2
                        $url = substr($url, 0, $nullOffset);
4716
                    }
4717 3
                    $url .= $hasText ? '#' : '';
4718 3
                    $offset += $us;
4719
4720 3
                    break;
4721 3
                case 'local':
4722
                    // section 5.58.3: Hyperlink to local file
4723
                    // examples:
4724
                    //   mydoc.txt
4725
                    //   ../../somedoc.xls#Sheet!A1
4726
4727
                    // offset: var; size: 16; GUI of File Moniker
4728
                    $offset += 16;
4729
4730
                    // offset: var; size: 2; directory up-level count.
4731
                    $upLevelCount = self::getUInt2d($recordData, $offset);
4732
                    $offset += 2;
4733
4734
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
4735
                    $sl = self::getInt4d($recordData, $offset);
4736
                    $offset += 4;
4737
4738
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
4739
                    $shortenedFilePath = substr($recordData, $offset, $sl);
4740
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
4741
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
4742
4743
                    $offset += $sl;
4744
4745
                    // offset: var; size: 24; unknown sequence
4746
                    $offset += 24;
4747
4748
                    // extended file path
4749
                    // offset: var; size: 4; size of the following file link field including string lenth mark
4750
                    $sz = self::getInt4d($recordData, $offset);
4751
                    $offset += 4;
4752
4753
                    // only present if $sz > 0
4754
                    if ($sz > 0) {
4755
                        // offset: var; size: 4; size of the character array of the extended file path and name
4756
                        $xl = self::getInt4d($recordData, $offset);
4757
                        $offset += 4;
4758
4759
                        // offset: var; size 2; unknown
4760
                        $offset += 2;
4761
4762
                        // offset: var; size $xl; character array of the extended file path and name.
4763
                        $extendedFilePath = substr($recordData, $offset, $xl);
4764
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
4765
                        $offset += $xl;
4766
                    }
4767
4768
                    // construct the path
4769
                    $url = str_repeat('..\\', $upLevelCount);
4770
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
4771
                    $url .= $hasText ? '#' : '';
4772
4773
                    break;
4774 3
                case 'UNC':
4775
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
4776
                    // todo: implement
4777
                    return;
4778 3
                case 'workbook':
4779
                    // section 5.58.5: Hyperlink to the Current Workbook
4780
                    // e.g. Sheet2!B1:C2, stored in text mark field
4781 3
                    $url = 'sheet://';
4782
4783 3
                    break;
4784
                default:
4785
                    return;
4786
            }
4787
4788 3
            if ($hasText) {
4789
                // offset: var; size: 4; character count of text mark including trailing zero word
4790 3
                $tl = self::getInt4d($recordData, $offset);
4791 3
                $offset += 4;
4792
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
4793 3
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4794 3
                $url .= $text;
4795
            }
4796
4797
            // apply the hyperlink to all the relevant cells
4798 3
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
4799 3
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
4800
            }
4801
        }
4802 3
    }
4803
4804
    /**
4805
     * Read DATAVALIDATIONS record.
4806
     */
4807
    private function readDataValidations(): void
4808
    {
4809
        $length = self::getUInt2d($this->data, $this->pos + 2);
4810
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4811
4812
        // move stream pointer forward to next record
4813
        $this->pos += 4 + $length;
4814
    }
4815
4816
    /**
4817
     * Read DATAVALIDATION record.
4818
     */
4819
    private function readDataValidation(): void
4820
    {
4821
        $length = self::getUInt2d($this->data, $this->pos + 2);
4822
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4823
4824
        // move stream pointer forward to next record
4825
        $this->pos += 4 + $length;
4826
4827
        if ($this->readDataOnly) {
4828
            return;
4829
        }
4830
4831
        // offset: 0; size: 4; Options
4832
        $options = self::getInt4d($recordData, 0);
4833
4834
        // bit: 0-3; mask: 0x0000000F; type
4835
        $type = (0x0000000F & $options) >> 0;
4836
        switch ($type) {
4837
            case 0x00:
4838
                $type = DataValidation::TYPE_NONE;
4839
4840
                break;
4841
            case 0x01:
4842
                $type = DataValidation::TYPE_WHOLE;
4843
4844
                break;
4845
            case 0x02:
4846
                $type = DataValidation::TYPE_DECIMAL;
4847
4848
                break;
4849
            case 0x03:
4850
                $type = DataValidation::TYPE_LIST;
4851
4852
                break;
4853
            case 0x04:
4854
                $type = DataValidation::TYPE_DATE;
4855
4856
                break;
4857
            case 0x05:
4858
                $type = DataValidation::TYPE_TIME;
4859
4860
                break;
4861
            case 0x06:
4862
                $type = DataValidation::TYPE_TEXTLENGTH;
4863
4864
                break;
4865
            case 0x07:
4866
                $type = DataValidation::TYPE_CUSTOM;
4867
4868
                break;
4869
        }
4870
4871
        // bit: 4-6; mask: 0x00000070; error type
4872
        $errorStyle = (0x00000070 & $options) >> 4;
4873
        switch ($errorStyle) {
4874
            case 0x00:
4875
                $errorStyle = DataValidation::STYLE_STOP;
4876
4877
                break;
4878
            case 0x01:
4879
                $errorStyle = DataValidation::STYLE_WARNING;
4880
4881
                break;
4882
            case 0x02:
4883
                $errorStyle = DataValidation::STYLE_INFORMATION;
4884
4885
                break;
4886
        }
4887
4888
        // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
4889
        // I have only seen cases where this is 1
4890
        $explicitFormula = (0x00000080 & $options) >> 7;
4891
4892
        // bit: 8; mask: 0x00000100; 1= empty cells allowed
4893
        $allowBlank = (0x00000100 & $options) >> 8;
4894
4895
        // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
4896
        $suppressDropDown = (0x00000200 & $options) >> 9;
4897
4898
        // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
4899
        $showInputMessage = (0x00040000 & $options) >> 18;
4900
4901
        // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
4902
        $showErrorMessage = (0x00080000 & $options) >> 19;
4903
4904
        // bit: 20-23; mask: 0x00F00000; condition operator
4905
        $operator = (0x00F00000 & $options) >> 20;
4906
        switch ($operator) {
4907
            case 0x00:
4908
                $operator = DataValidation::OPERATOR_BETWEEN;
4909
4910
                break;
4911
            case 0x01:
4912
                $operator = DataValidation::OPERATOR_NOTBETWEEN;
4913
4914
                break;
4915
            case 0x02:
4916
                $operator = DataValidation::OPERATOR_EQUAL;
4917
4918
                break;
4919
            case 0x03:
4920
                $operator = DataValidation::OPERATOR_NOTEQUAL;
4921
4922
                break;
4923
            case 0x04:
4924
                $operator = DataValidation::OPERATOR_GREATERTHAN;
4925
4926
                break;
4927
            case 0x05:
4928
                $operator = DataValidation::OPERATOR_LESSTHAN;
4929
4930
                break;
4931
            case 0x06:
4932
                $operator = DataValidation::OPERATOR_GREATERTHANOREQUAL;
4933
4934
                break;
4935
            case 0x07:
4936
                $operator = DataValidation::OPERATOR_LESSTHANOREQUAL;
4937
4938
                break;
4939
        }
4940
4941
        // offset: 4; size: var; title of the prompt box
4942
        $offset = 4;
4943
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4944
        $promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4945
        $offset += $string['size'];
4946
4947
        // offset: var; size: var; title of the error box
4948
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4949
        $errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4950
        $offset += $string['size'];
4951
4952
        // offset: var; size: var; text of the prompt box
4953
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4954
        $prompt = $string['value'] !== chr(0) ? $string['value'] : '';
4955
        $offset += $string['size'];
4956
4957
        // offset: var; size: var; text of the error box
4958
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4959
        $error = $string['value'] !== chr(0) ? $string['value'] : '';
4960
        $offset += $string['size'];
4961
4962
        // offset: var; size: 2; size of the formula data for the first condition
4963
        $sz1 = self::getUInt2d($recordData, $offset);
4964
        $offset += 2;
4965
4966
        // offset: var; size: 2; not used
4967
        $offset += 2;
4968
4969
        // offset: var; size: $sz1; formula data for first condition (without size field)
4970
        $formula1 = substr($recordData, $offset, $sz1);
4971
        $formula1 = pack('v', $sz1) . $formula1; // prepend the length
4972
4973
        try {
4974
            $formula1 = $this->getFormulaFromStructure($formula1);
4975
4976
            // in list type validity, null characters are used as item separators
4977
            if ($type == DataValidation::TYPE_LIST) {
4978
                $formula1 = str_replace(chr(0), ',', $formula1);
4979
            }
4980
        } catch (PhpSpreadsheetException $e) {
4981
            return;
4982
        }
4983
        $offset += $sz1;
4984
4985
        // offset: var; size: 2; size of the formula data for the first condition
4986
        $sz2 = self::getUInt2d($recordData, $offset);
4987
        $offset += 2;
4988
4989
        // offset: var; size: 2; not used
4990
        $offset += 2;
4991
4992
        // offset: var; size: $sz2; formula data for second condition (without size field)
4993
        $formula2 = substr($recordData, $offset, $sz2);
4994
        $formula2 = pack('v', $sz2) . $formula2; // prepend the length
4995
4996
        try {
4997
            $formula2 = $this->getFormulaFromStructure($formula2);
4998
        } catch (PhpSpreadsheetException $e) {
4999
            return;
5000
        }
5001
        $offset += $sz2;
5002
5003
        // offset: var; size: var; cell range address list with
5004
        $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset));
5005
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
5006
5007
        foreach ($cellRangeAddresses as $cellRange) {
5008
            $stRange = $this->phpSheet->shrinkRangeToFit($cellRange);
5009
            foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
5010
                $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation();
5011
                $objValidation->setType($type);
5012
                $objValidation->setErrorStyle($errorStyle);
5013
                $objValidation->setAllowBlank((bool) $allowBlank);
5014
                $objValidation->setShowInputMessage((bool) $showInputMessage);
5015
                $objValidation->setShowErrorMessage((bool) $showErrorMessage);
5016
                $objValidation->setShowDropDown(!$suppressDropDown);
5017
                $objValidation->setOperator($operator);
5018
                $objValidation->setErrorTitle($errorTitle);
5019
                $objValidation->setError($error);
5020
                $objValidation->setPromptTitle($promptTitle);
5021
                $objValidation->setPrompt($prompt);
5022
                $objValidation->setFormula1($formula1);
5023
                $objValidation->setFormula2($formula2);
5024
            }
5025
        }
5026
    }
5027
5028
    /**
5029
     * Read SHEETLAYOUT record. Stores sheet tab color information.
5030
     */
5031 3
    private function readSheetLayout(): void
5032
    {
5033 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
5034 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5035
5036
        // move stream pointer to next record
5037 3
        $this->pos += 4 + $length;
5038
5039
        // local pointer in record data
5040 3
        $offset = 0;
5041
5042 3
        if (!$this->readDataOnly) {
5043
            // offset: 0; size: 2; repeated record identifier 0x0862
5044
5045
            // offset: 2; size: 10; not used
5046
5047
            // offset: 12; size: 4; size of record data
5048
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
5049 3
            $sz = self::getInt4d($recordData, 12);
5050
5051
            switch ($sz) {
5052 3
                case 0x14:
5053
                    // offset: 16; size: 2; color index for sheet tab
5054 1
                    $colorIndex = self::getUInt2d($recordData, 16);
5055 1
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
5056 1
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
5057
5058 1
                    break;
5059 2
                case 0x28:
5060
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
5061 2
                    return;
5062
5063
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
5064
            }
5065
        }
5066 1
    }
5067
5068
    /**
5069
     * Read SHEETPROTECTION record (FEATHEADR).
5070
     */
5071 21
    private function readSheetProtection(): void
5072
    {
5073 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
5074 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5075
5076
        // move stream pointer to next record
5077 21
        $this->pos += 4 + $length;
5078
5079 21
        if ($this->readDataOnly) {
5080
            return;
5081
        }
5082
5083
        // offset: 0; size: 2; repeated record header
5084
5085
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
5086
5087
        // offset: 4; size: 8; Currently not used and set to 0
5088
5089
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
5090 21
        $isf = self::getUInt2d($recordData, 12);
5091 21
        if ($isf != 2) {
5092
            return;
5093
        }
5094
5095
        // offset: 14; size: 1; =1 since this is a feat header
5096
5097
        // offset: 15; size: 4; size of rgbHdrSData
5098
5099
        // rgbHdrSData, assume "Enhanced Protection"
5100
        // offset: 19; size: 2; option flags
5101 21
        $options = self::getUInt2d($recordData, 19);
5102
5103
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
5104 21
        $bool = (0x0001 & $options) >> 0;
5105 21
        $this->phpSheet->getProtection()->setObjects(!$bool);
5106
5107
        // bit: 1; mask 0x0002; edit scenarios
5108 21
        $bool = (0x0002 & $options) >> 1;
5109 21
        $this->phpSheet->getProtection()->setScenarios(!$bool);
5110
5111
        // bit: 2; mask 0x0004; format cells
5112 21
        $bool = (0x0004 & $options) >> 2;
5113 21
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
5114
5115
        // bit: 3; mask 0x0008; format columns
5116 21
        $bool = (0x0008 & $options) >> 3;
5117 21
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
5118
5119
        // bit: 4; mask 0x0010; format rows
5120 21
        $bool = (0x0010 & $options) >> 4;
5121 21
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
5122
5123
        // bit: 5; mask 0x0020; insert columns
5124 21
        $bool = (0x0020 & $options) >> 5;
5125 21
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
5126
5127
        // bit: 6; mask 0x0040; insert rows
5128 21
        $bool = (0x0040 & $options) >> 6;
5129 21
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
5130
5131
        // bit: 7; mask 0x0080; insert hyperlinks
5132 21
        $bool = (0x0080 & $options) >> 7;
5133 21
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
5134
5135
        // bit: 8; mask 0x0100; delete columns
5136 21
        $bool = (0x0100 & $options) >> 8;
5137 21
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
5138
5139
        // bit: 9; mask 0x0200; delete rows
5140 21
        $bool = (0x0200 & $options) >> 9;
5141 21
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
5142
5143
        // bit: 10; mask 0x0400; select locked cells
5144 21
        $bool = (0x0400 & $options) >> 10;
5145 21
        $this->phpSheet->getProtection()->setSelectLockedCells(!$bool);
5146
5147
        // bit: 11; mask 0x0800; sort cell range
5148 21
        $bool = (0x0800 & $options) >> 11;
5149 21
        $this->phpSheet->getProtection()->setSort(!$bool);
5150
5151
        // bit: 12; mask 0x1000; auto filter
5152 21
        $bool = (0x1000 & $options) >> 12;
5153 21
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
5154
5155
        // bit: 13; mask 0x2000; pivot tables
5156 21
        $bool = (0x2000 & $options) >> 13;
5157 21
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
5158
5159
        // bit: 14; mask 0x4000; select unlocked cells
5160 21
        $bool = (0x4000 & $options) >> 14;
5161 21
        $this->phpSheet->getProtection()->setSelectUnlockedCells(!$bool);
5162
5163
        // offset: 21; size: 2; not used
5164 21
    }
5165
5166
    /**
5167
     * Read RANGEPROTECTION record
5168
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
5169
     * where it is referred to as FEAT record.
5170
     */
5171 1
    private function readRangeProtection(): void
5172
    {
5173 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
5174 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5175
5176
        // move stream pointer to next record
5177 1
        $this->pos += 4 + $length;
5178
5179
        // local pointer in record data
5180 1
        $offset = 0;
5181
5182 1
        if (!$this->readDataOnly) {
5183 1
            $offset += 12;
5184
5185
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
5186 1
            $isf = self::getUInt2d($recordData, 12);
5187 1
            if ($isf != 2) {
5188
                // we only read FEAT records of type 2
5189
                return;
5190
            }
5191 1
            $offset += 2;
5192
5193 1
            $offset += 5;
5194
5195
            // offset: 19; size: 2; count of ref ranges this feature is on
5196 1
            $cref = self::getUInt2d($recordData, 19);
5197 1
            $offset += 2;
5198
5199 1
            $offset += 6;
5200
5201
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
5202 1
            $cellRanges = [];
5203 1
            for ($i = 0; $i < $cref; ++$i) {
5204
                try {
5205 1
                    $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
5206
                } catch (PhpSpreadsheetException $e) {
5207
                    return;
5208
                }
5209 1
                $cellRanges[] = $cellRange;
5210 1
                $offset += 8;
5211
            }
5212
5213
            // offset: var; size: var; variable length of feature specific data
5214 1
            $rgbFeat = substr($recordData, $offset);
5215 1
            $offset += 4;
5216
5217
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
5218 1
            $wPassword = self::getInt4d($recordData, $offset);
5219 1
            $offset += 4;
5220
5221
            // Apply range protection to sheet
5222 1
            if ($cellRanges) {
5223 1
                $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
5224
            }
5225
        }
5226 1
    }
5227
5228
    /**
5229
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
5230
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
5231
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
5232
     */
5233
    private function readContinue(): void
5234
    {
5235
        $length = self::getUInt2d($this->data, $this->pos + 2);
5236
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5237
5238
        // check if we are reading drawing data
5239
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
5240
        if ($this->drawingData == '') {
5241
            // move stream pointer to next record
5242
            $this->pos += 4 + $length;
5243
5244
            return;
5245
        }
5246
5247
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
5248
        if ($length < 4) {
5249
            // move stream pointer to next record
5250
            $this->pos += 4 + $length;
5251
5252
            return;
5253
        }
5254
5255
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
5256
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
5257
        // we know that Escher stream may be split at least at
5258
        //        0xF003 MsofbtSpgrContainer
5259
        //        0xF004 MsofbtSpContainer
5260
        //        0xF00D MsofbtClientTextbox
5261
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
5262
5263
        $splitPoint = self::getUInt2d($recordData, 2);
5264
        if (in_array($splitPoint, $validSplitPoints)) {
5265
            // get spliced record data (and move pointer to next record)
5266
            $splicedRecordData = $this->getSplicedRecordData();
5267
            $this->drawingData .= $splicedRecordData['recordData'];
5268
5269
            return;
5270
        }
5271
5272
        // move stream pointer to next record
5273
        $this->pos += 4 + $length;
5274
    }
5275
5276
    /**
5277
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
5278
     * records are found. Splices the record data pieces and returns the combined string as if record data
5279
     * is in one piece.
5280
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
5281
     *
5282
     * @return array
5283
     */
5284 35
    private function getSplicedRecordData()
5285
    {
5286 35
        $data = '';
5287 35
        $spliceOffsets = [];
5288
5289 35
        $i = 0;
5290 35
        $spliceOffsets[0] = 0;
5291
5292
        do {
5293 35
            ++$i;
5294
5295
            // offset: 0; size: 2; identifier
5296 35
            $identifier = self::getUInt2d($this->data, $this->pos);
5297
            // offset: 2; size: 2; length
5298 35
            $length = self::getUInt2d($this->data, $this->pos + 2);
5299 35
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
5300
5301 35
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
5302
5303 35
            $this->pos += 4 + $length;
5304 35
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
5305 35
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
5306
5307
        return [
5308 35
            'recordData' => $data,
5309 35
            'spliceOffsets' => $spliceOffsets,
5310
        ];
5311
    }
5312
5313
    /**
5314
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
5315
     *
5316
     * @param string $formulaStructure The complete binary data for the formula
5317
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5318
     *
5319
     * @return string Human readable formula
5320
     */
5321 17
    private function getFormulaFromStructure($formulaStructure, $baseCell = 'A1')
5322
    {
5323
        // offset: 0; size: 2; size of the following formula data
5324 17
        $sz = self::getUInt2d($formulaStructure, 0);
5325
5326
        // offset: 2; size: sz
5327 17
        $formulaData = substr($formulaStructure, 2, $sz);
5328
5329
        // offset: 2 + sz; size: variable (optional)
5330 17
        if (strlen($formulaStructure) > 2 + $sz) {
5331
            $additionalData = substr($formulaStructure, 2 + $sz);
5332
        } else {
5333 17
            $additionalData = '';
5334
        }
5335
5336 17
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
5337
    }
5338
5339
    /**
5340
     * Take formula data and additional data for formula and return human readable formula.
5341
     *
5342
     * @param string $formulaData The binary data for the formula itself
5343
     * @param string $additionalData Additional binary data going with the formula
5344
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5345
     *
5346
     * @return string Human readable formula
5347
     */
5348 17
    private function getFormulaFromData($formulaData, $additionalData = '', $baseCell = 'A1')
5349
    {
5350
        // start parsing the formula data
5351 17
        $tokens = [];
5352
5353 17
        while (strlen($formulaData) > 0 && $token = $this->getNextToken($formulaData, $baseCell)) {
5354 17
            $tokens[] = $token;
5355 17
            $formulaData = substr($formulaData, $token['size']);
5356
        }
5357
5358 17
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
5359
5360 17
        return $formulaString;
5361
    }
5362
5363
    /**
5364
     * Take array of tokens together with additional data for formula and return human readable formula.
5365
     *
5366
     * @param array $tokens
5367
     * @param string $additionalData Additional binary data going with the formula
5368
     *
5369
     * @return string Human readable formula
5370
     */
5371 17
    private function createFormulaFromTokens($tokens, $additionalData)
5372
    {
5373
        // empty formula?
5374 17
        if (empty($tokens)) {
5375
            return '';
5376
        }
5377
5378 17
        $formulaStrings = [];
5379 17
        foreach ($tokens as $token) {
5380
            // initialize spaces
5381 17
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
5382 17
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
5383 17
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
5384 17
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
5385 17
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
5386 17
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
5387
5388 17
            switch ($token['name']) {
5389 17
                case 'tAdd': // addition
5390 17
                case 'tConcat': // addition
5391 17
                case 'tDiv': // division
5392 17
                case 'tEQ': // equality
5393 17
                case 'tGE': // greater than or equal
5394 17
                case 'tGT': // greater than
5395 17
                case 'tIsect': // intersection
5396 17
                case 'tLE': // less than or equal
5397 17
                case 'tList': // less than or equal
5398 17
                case 'tLT': // less than
5399 17
                case 'tMul': // multiplication
5400 17
                case 'tNE': // multiplication
5401 17
                case 'tPower': // power
5402 17
                case 'tRange': // range
5403 17
                case 'tSub': // subtraction
5404 12
                    $op2 = array_pop($formulaStrings);
5405 12
                    $op1 = array_pop($formulaStrings);
5406 12
                    $formulaStrings[] = "$op1$space1$space0{$token['data']}$op2";
5407 12
                    unset($space0, $space1);
5408
5409 12
                    break;
5410 17
                case 'tUplus': // unary plus
5411 17
                case 'tUminus': // unary minus
5412
                    $op = array_pop($formulaStrings);
5413
                    $formulaStrings[] = "$space1$space0{$token['data']}$op";
5414
                    unset($space0, $space1);
5415
5416
                    break;
5417 17
                case 'tPercent': // percent sign
5418
                    $op = array_pop($formulaStrings);
5419
                    $formulaStrings[] = "$op$space1$space0{$token['data']}";
5420
                    unset($space0, $space1);
5421
5422
                    break;
5423 17
                case 'tAttrVolatile': // indicates volatile function
5424 17
                case 'tAttrIf':
5425 17
                case 'tAttrSkip':
5426 17
                case 'tAttrChoose':
5427
                    // token is only important for Excel formula evaluator
5428
                    // do nothing
5429
                    break;
5430 17
                case 'tAttrSpace': // space / carriage return
5431
                    // space will be used when next token arrives, do not alter formulaString stack
5432
                    switch ($token['data']['spacetype']) {
5433
                        case 'type0':
5434
                            $space0 = str_repeat(' ', $token['data']['spacecount']);
5435
5436
                            break;
5437
                        case 'type1':
5438
                            $space1 = str_repeat("\n", $token['data']['spacecount']);
5439
5440
                            break;
5441
                        case 'type2':
5442
                            $space2 = str_repeat(' ', $token['data']['spacecount']);
5443
5444
                            break;
5445
                        case 'type3':
5446
                            $space3 = str_repeat("\n", $token['data']['spacecount']);
5447
5448
                            break;
5449
                        case 'type4':
5450
                            $space4 = str_repeat(' ', $token['data']['spacecount']);
5451
5452
                            break;
5453
                        case 'type5':
5454
                            $space5 = str_repeat("\n", $token['data']['spacecount']);
5455
5456
                            break;
5457
                    }
5458
5459
                    break;
5460 17
                case 'tAttrSum': // SUM function with one parameter
5461 10
                    $op = array_pop($formulaStrings);
5462 10
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
5463 10
                    unset($space0, $space1);
5464
5465 10
                    break;
5466 17
                case 'tFunc': // function with fixed number of arguments
5467 17
                case 'tFuncV': // function with variable number of arguments
5468 13
                    if ($token['data']['function'] != '') {
5469
                        // normal function
5470 13
                        $ops = []; // array of operators
5471 13
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
5472 5
                            $ops[] = array_pop($formulaStrings);
5473
                        }
5474 13
                        $ops = array_reverse($ops);
5475 13
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
5476 13
                        unset($space0, $space1);
5477
                    } else {
5478
                        // add-in function
5479
                        $ops = []; // array of operators
5480
                        for ($i = 0; $i < $token['data']['args'] - 1; ++$i) {
5481
                            $ops[] = array_pop($formulaStrings);
5482
                        }
5483
                        $ops = array_reverse($ops);
5484
                        $function = array_pop($formulaStrings);
5485
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
5486
                        unset($space0, $space1);
5487
                    }
5488
5489 13
                    break;
5490 17
                case 'tParen': // parenthesis
5491
                    $expression = array_pop($formulaStrings);
5492
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
5493
                    unset($space2, $space3, $space4, $space5);
5494
5495
                    break;
5496 17
                case 'tArray': // array constant
5497
                    $constantArray = self::readBIFF8ConstantArray($additionalData);
5498
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
5499
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
5500
                    unset($space0, $space1);
5501
5502
                    break;
5503 17
                case 'tMemArea':
5504
                    // bite off chunk of additional data
5505
                    $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData);
5506
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
5507
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5508
                    unset($space0, $space1);
5509
5510
                    break;
5511 17
                case 'tArea': // cell range address
5512 15
                case 'tBool': // boolean
5513 15
                case 'tErr': // error code
5514 15
                case 'tInt': // integer
5515 7
                case 'tMemErr':
5516 7
                case 'tMemFunc':
5517 7
                case 'tMissArg':
5518 7
                case 'tName':
5519 7
                case 'tNameX':
5520 7
                case 'tNum': // number
5521 7
                case 'tRef': // single cell reference
5522 7
                case 'tRef3d': // 3d cell reference
5523 6
                case 'tArea3d': // 3d cell range reference
5524 1
                case 'tRefN':
5525 1
                case 'tAreaN':
5526 1
                case 'tStr': // string
5527 17
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5528 17
                    unset($space0, $space1);
5529
5530 17
                    break;
5531
            }
5532
        }
5533 17
        $formulaString = $formulaStrings[0];
5534
5535 17
        return $formulaString;
5536
    }
5537
5538
    /**
5539
     * Fetch next token from binary formula data.
5540
     *
5541
     * @param string $formulaData Formula data
5542
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5543
     *
5544
     * @return array
5545
     */
5546 17
    private function getNextToken($formulaData, $baseCell = 'A1')
5547
    {
5548
        // offset: 0; size: 1; token id
5549 17
        $id = ord($formulaData[0]); // token id
5550 17
        $name = false; // initialize token name
5551
5552
        switch ($id) {
5553 17
            case 0x03:
5554 1
                $name = 'tAdd';
5555 1
                $size = 1;
5556 1
                $data = '+';
5557
5558 1
                break;
5559 17
            case 0x04:
5560
                $name = 'tSub';
5561
                $size = 1;
5562
                $data = '-';
5563
5564
                break;
5565 17
            case 0x05:
5566 3
                $name = 'tMul';
5567 3
                $size = 1;
5568 3
                $data = '*';
5569
5570 3
                break;
5571 17
            case 0x06:
5572 8
                $name = 'tDiv';
5573 8
                $size = 1;
5574 8
                $data = '/';
5575
5576 8
                break;
5577 17
            case 0x07:
5578
                $name = 'tPower';
5579
                $size = 1;
5580
                $data = '^';
5581
5582
                break;
5583 17
            case 0x08:
5584
                $name = 'tConcat';
5585
                $size = 1;
5586
                $data = '&';
5587
5588
                break;
5589 17
            case 0x09:
5590
                $name = 'tLT';
5591
                $size = 1;
5592
                $data = '<';
5593
5594
                break;
5595 17
            case 0x0A:
5596
                $name = 'tLE';
5597
                $size = 1;
5598
                $data = '<=';
5599
5600
                break;
5601 17
            case 0x0B:
5602
                $name = 'tEQ';
5603
                $size = 1;
5604
                $data = '=';
5605
5606
                break;
5607 17
            case 0x0C:
5608
                $name = 'tGE';
5609
                $size = 1;
5610
                $data = '>=';
5611
5612
                break;
5613 17
            case 0x0D:
5614
                $name = 'tGT';
5615
                $size = 1;
5616
                $data = '>';
5617
5618
                break;
5619 17
            case 0x0E:
5620 1
                $name = 'tNE';
5621 1
                $size = 1;
5622 1
                $data = '<>';
5623
5624 1
                break;
5625 17
            case 0x0F:
5626
                $name = 'tIsect';
5627
                $size = 1;
5628
                $data = ' ';
5629
5630
                break;
5631 17
            case 0x10:
5632 1
                $name = 'tList';
5633 1
                $size = 1;
5634 1
                $data = ',';
5635
5636 1
                break;
5637 17
            case 0x11:
5638
                $name = 'tRange';
5639
                $size = 1;
5640
                $data = ':';
5641
5642
                break;
5643 17
            case 0x12:
5644
                $name = 'tUplus';
5645
                $size = 1;
5646
                $data = '+';
5647
5648
                break;
5649 17
            case 0x13:
5650
                $name = 'tUminus';
5651
                $size = 1;
5652
                $data = '-';
5653
5654
                break;
5655 17
            case 0x14:
5656
                $name = 'tPercent';
5657
                $size = 1;
5658
                $data = '%';
5659
5660
                break;
5661 17
            case 0x15:    //    parenthesis
5662
                $name = 'tParen';
5663
                $size = 1;
5664
                $data = null;
5665
5666
                break;
5667 17
            case 0x16:    //    missing argument
5668
                $name = 'tMissArg';
5669
                $size = 1;
5670
                $data = '';
5671
5672
                break;
5673 17
            case 0x17:    //    string
5674 1
                $name = 'tStr';
5675
                // offset: 1; size: var; Unicode string, 8-bit string length
5676 1
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
5677 1
                $size = 1 + $string['size'];
5678 1
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
5679
5680 1
                break;
5681 17
            case 0x19:    //    Special attribute
5682
                // offset: 1; size: 1; attribute type flags:
5683 10
                switch (ord($formulaData[1])) {
5684 10
                    case 0x01:
5685
                        $name = 'tAttrVolatile';
5686
                        $size = 4;
5687
                        $data = null;
5688
5689
                        break;
5690 10
                    case 0x02:
5691
                        $name = 'tAttrIf';
5692
                        $size = 4;
5693
                        $data = null;
5694
5695
                        break;
5696 10
                    case 0x04:
5697
                        $name = 'tAttrChoose';
5698
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
5699
                        $nc = self::getUInt2d($formulaData, 2);
5700
                        // offset: 4; size: 2 * $nc
5701
                        // offset: 4 + 2 * $nc; size: 2
5702
                        $size = 2 * $nc + 6;
5703
                        $data = null;
5704
5705
                        break;
5706 10
                    case 0x08:
5707
                        $name = 'tAttrSkip';
5708
                        $size = 4;
5709
                        $data = null;
5710
5711
                        break;
5712 10
                    case 0x10:
5713 10
                        $name = 'tAttrSum';
5714 10
                        $size = 4;
5715 10
                        $data = null;
5716
5717 10
                        break;
5718
                    case 0x40:
5719
                    case 0x41:
5720
                        $name = 'tAttrSpace';
5721
                        $size = 4;
5722
                        // offset: 2; size: 2; space type and position
5723
                        switch (ord($formulaData[2])) {
5724
                            case 0x00:
5725
                                $spacetype = 'type0';
5726
5727
                                break;
5728
                            case 0x01:
5729
                                $spacetype = 'type1';
5730
5731
                                break;
5732
                            case 0x02:
5733
                                $spacetype = 'type2';
5734
5735
                                break;
5736
                            case 0x03:
5737
                                $spacetype = 'type3';
5738
5739
                                break;
5740
                            case 0x04:
5741
                                $spacetype = 'type4';
5742
5743
                                break;
5744
                            case 0x05:
5745
                                $spacetype = 'type5';
5746
5747
                                break;
5748
                            default:
5749
                                throw new Exception('Unrecognized space type in tAttrSpace token');
5750
5751
                                break;
5752
                        }
5753
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
5754
                        $spacecount = ord($formulaData[3]);
5755
5756
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
5757
5758
                        break;
5759
                    default:
5760
                        throw new Exception('Unrecognized attribute flag in tAttr token');
5761
5762
                        break;
5763
                }
5764
5765 10
                break;
5766 17
            case 0x1C:    //    error code
5767
                // offset: 1; size: 1; error code
5768
                $name = 'tErr';
5769
                $size = 2;
5770
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5771
5772
                break;
5773 17
            case 0x1D:    //    boolean
5774
                // offset: 1; size: 1; 0 = false, 1 = true;
5775
                $name = 'tBool';
5776
                $size = 2;
5777
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
5778
5779
                break;
5780 17
            case 0x1E:    //    integer
5781
                // offset: 1; size: 2; unsigned 16-bit integer
5782 8
                $name = 'tInt';
5783 8
                $size = 3;
5784 8
                $data = self::getUInt2d($formulaData, 1);
5785
5786 8
                break;
5787 17
            case 0x1F:    //    number
5788
                // offset: 1; size: 8;
5789 3
                $name = 'tNum';
5790 3
                $size = 9;
5791 3
                $data = self::extractNumber(substr($formulaData, 1));
5792 3
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
5793
5794 3
                break;
5795 17
            case 0x20:    //    array constant
5796 17
            case 0x40:
5797 17
            case 0x60:
5798
                // offset: 1; size: 7; not used
5799
                $name = 'tArray';
5800
                $size = 8;
5801
                $data = null;
5802
5803
                break;
5804 17
            case 0x21:    //    function with fixed number of arguments
5805 17
            case 0x41:
5806 17
            case 0x61:
5807 8
                $name = 'tFunc';
5808 8
                $size = 3;
5809
                // offset: 1; size: 2; index to built-in sheet function
5810 8
                switch (self::getUInt2d($formulaData, 1)) {
5811 8
                    case 2:
5812
                        $function = 'ISNA';
5813
                        $args = 1;
5814
5815
                        break;
5816 8
                    case 3:
5817
                        $function = 'ISERROR';
5818
                        $args = 1;
5819
5820
                        break;
5821 8
                    case 10:
5822 8
                        $function = 'NA';
5823 8
                        $args = 0;
5824
5825 8
                        break;
5826
                    case 15:
5827
                        $function = 'SIN';
5828
                        $args = 1;
5829
5830
                        break;
5831
                    case 16:
5832
                        $function = 'COS';
5833
                        $args = 1;
5834
5835
                        break;
5836
                    case 17:
5837
                        $function = 'TAN';
5838
                        $args = 1;
5839
5840
                        break;
5841
                    case 18:
5842
                        $function = 'ATAN';
5843
                        $args = 1;
5844
5845
                        break;
5846
                    case 19:
5847
                        $function = 'PI';
5848
                        $args = 0;
5849
5850
                        break;
5851
                    case 20:
5852
                        $function = 'SQRT';
5853
                        $args = 1;
5854
5855
                        break;
5856
                    case 21:
5857
                        $function = 'EXP';
5858
                        $args = 1;
5859
5860
                        break;
5861
                    case 22:
5862
                        $function = 'LN';
5863
                        $args = 1;
5864
5865
                        break;
5866
                    case 23:
5867
                        $function = 'LOG10';
5868
                        $args = 1;
5869
5870
                        break;
5871
                    case 24:
5872
                        $function = 'ABS';
5873
                        $args = 1;
5874
5875
                        break;
5876
                    case 25:
5877
                        $function = 'INT';
5878
                        $args = 1;
5879
5880
                        break;
5881
                    case 26:
5882
                        $function = 'SIGN';
5883
                        $args = 1;
5884
5885
                        break;
5886
                    case 27:
5887
                        $function = 'ROUND';
5888
                        $args = 2;
5889
5890
                        break;
5891
                    case 30:
5892
                        $function = 'REPT';
5893
                        $args = 2;
5894
5895
                        break;
5896
                    case 31:
5897
                        $function = 'MID';
5898
                        $args = 3;
5899
5900
                        break;
5901
                    case 32:
5902
                        $function = 'LEN';
5903
                        $args = 1;
5904
5905
                        break;
5906
                    case 33:
5907
                        $function = 'VALUE';
5908
                        $args = 1;
5909
5910
                        break;
5911
                    case 34:
5912
                        $function = 'TRUE';
5913
                        $args = 0;
5914
5915
                        break;
5916
                    case 35:
5917
                        $function = 'FALSE';
5918
                        $args = 0;
5919
5920
                        break;
5921
                    case 38:
5922
                        $function = 'NOT';
5923
                        $args = 1;
5924
5925
                        break;
5926
                    case 39:
5927
                        $function = 'MOD';
5928
                        $args = 2;
5929
5930
                        break;
5931
                    case 40:
5932
                        $function = 'DCOUNT';
5933
                        $args = 3;
5934
5935
                        break;
5936
                    case 41:
5937
                        $function = 'DSUM';
5938
                        $args = 3;
5939
5940
                        break;
5941
                    case 42:
5942
                        $function = 'DAVERAGE';
5943
                        $args = 3;
5944
5945
                        break;
5946
                    case 43:
5947
                        $function = 'DMIN';
5948
                        $args = 3;
5949
5950
                        break;
5951
                    case 44:
5952
                        $function = 'DMAX';
5953
                        $args = 3;
5954
5955
                        break;
5956
                    case 45:
5957
                        $function = 'DSTDEV';
5958
                        $args = 3;
5959
5960
                        break;
5961
                    case 48:
5962
                        $function = 'TEXT';
5963
                        $args = 2;
5964
5965
                        break;
5966
                    case 61:
5967
                        $function = 'MIRR';
5968
                        $args = 3;
5969
5970
                        break;
5971
                    case 63:
5972
                        $function = 'RAND';
5973
                        $args = 0;
5974
5975
                        break;
5976
                    case 65:
5977
                        $function = 'DATE';
5978
                        $args = 3;
5979
5980
                        break;
5981
                    case 66:
5982
                        $function = 'TIME';
5983
                        $args = 3;
5984
5985
                        break;
5986
                    case 67:
5987
                        $function = 'DAY';
5988
                        $args = 1;
5989
5990
                        break;
5991
                    case 68:
5992
                        $function = 'MONTH';
5993
                        $args = 1;
5994
5995
                        break;
5996
                    case 69:
5997
                        $function = 'YEAR';
5998
                        $args = 1;
5999
6000
                        break;
6001
                    case 71:
6002
                        $function = 'HOUR';
6003
                        $args = 1;
6004
6005
                        break;
6006
                    case 72:
6007
                        $function = 'MINUTE';
6008
                        $args = 1;
6009
6010
                        break;
6011
                    case 73:
6012
                        $function = 'SECOND';
6013
                        $args = 1;
6014
6015
                        break;
6016
                    case 74:
6017
                        $function = 'NOW';
6018
                        $args = 0;
6019
6020
                        break;
6021
                    case 75:
6022
                        $function = 'AREAS';
6023
                        $args = 1;
6024
6025
                        break;
6026
                    case 76:
6027
                        $function = 'ROWS';
6028
                        $args = 1;
6029
6030
                        break;
6031
                    case 77:
6032
                        $function = 'COLUMNS';
6033
                        $args = 1;
6034
6035
                        break;
6036
                    case 83:
6037
                        $function = 'TRANSPOSE';
6038
                        $args = 1;
6039
6040
                        break;
6041
                    case 86:
6042
                        $function = 'TYPE';
6043
                        $args = 1;
6044
6045
                        break;
6046
                    case 97:
6047
                        $function = 'ATAN2';
6048
                        $args = 2;
6049
6050
                        break;
6051
                    case 98:
6052
                        $function = 'ASIN';
6053
                        $args = 1;
6054
6055
                        break;
6056
                    case 99:
6057
                        $function = 'ACOS';
6058
                        $args = 1;
6059
6060
                        break;
6061
                    case 105:
6062
                        $function = 'ISREF';
6063
                        $args = 1;
6064
6065
                        break;
6066
                    case 111:
6067
                        $function = 'CHAR';
6068
                        $args = 1;
6069
6070
                        break;
6071
                    case 112:
6072
                        $function = 'LOWER';
6073
                        $args = 1;
6074
6075
                        break;
6076
                    case 113:
6077
                        $function = 'UPPER';
6078
                        $args = 1;
6079
6080
                        break;
6081
                    case 114:
6082
                        $function = 'PROPER';
6083
                        $args = 1;
6084
6085
                        break;
6086
                    case 117:
6087
                        $function = 'EXACT';
6088
                        $args = 2;
6089
6090
                        break;
6091
                    case 118:
6092
                        $function = 'TRIM';
6093
                        $args = 1;
6094
6095
                        break;
6096
                    case 119:
6097
                        $function = 'REPLACE';
6098
                        $args = 4;
6099
6100
                        break;
6101
                    case 121:
6102
                        $function = 'CODE';
6103
                        $args = 1;
6104
6105
                        break;
6106
                    case 126:
6107
                        $function = 'ISERR';
6108
                        $args = 1;
6109
6110
                        break;
6111
                    case 127:
6112
                        $function = 'ISTEXT';
6113
                        $args = 1;
6114
6115
                        break;
6116
                    case 128:
6117
                        $function = 'ISNUMBER';
6118
                        $args = 1;
6119
6120
                        break;
6121
                    case 129:
6122
                        $function = 'ISBLANK';
6123
                        $args = 1;
6124
6125
                        break;
6126
                    case 130:
6127
                        $function = 'T';
6128
                        $args = 1;
6129
6130
                        break;
6131
                    case 131:
6132
                        $function = 'N';
6133
                        $args = 1;
6134
6135
                        break;
6136
                    case 140:
6137
                        $function = 'DATEVALUE';
6138
                        $args = 1;
6139
6140
                        break;
6141
                    case 141:
6142
                        $function = 'TIMEVALUE';
6143
                        $args = 1;
6144
6145
                        break;
6146
                    case 142:
6147
                        $function = 'SLN';
6148
                        $args = 3;
6149
6150
                        break;
6151
                    case 143:
6152
                        $function = 'SYD';
6153
                        $args = 4;
6154
6155
                        break;
6156
                    case 162:
6157
                        $function = 'CLEAN';
6158
                        $args = 1;
6159
6160
                        break;
6161
                    case 163:
6162
                        $function = 'MDETERM';
6163
                        $args = 1;
6164
6165
                        break;
6166
                    case 164:
6167
                        $function = 'MINVERSE';
6168
                        $args = 1;
6169
6170
                        break;
6171
                    case 165:
6172
                        $function = 'MMULT';
6173
                        $args = 2;
6174
6175
                        break;
6176
                    case 184:
6177
                        $function = 'FACT';
6178
                        $args = 1;
6179
6180
                        break;
6181
                    case 189:
6182
                        $function = 'DPRODUCT';
6183
                        $args = 3;
6184
6185
                        break;
6186
                    case 190:
6187
                        $function = 'ISNONTEXT';
6188
                        $args = 1;
6189
6190
                        break;
6191
                    case 195:
6192
                        $function = 'DSTDEVP';
6193
                        $args = 3;
6194
6195
                        break;
6196
                    case 196:
6197
                        $function = 'DVARP';
6198
                        $args = 3;
6199
6200
                        break;
6201
                    case 198:
6202
                        $function = 'ISLOGICAL';
6203
                        $args = 1;
6204
6205
                        break;
6206
                    case 199:
6207
                        $function = 'DCOUNTA';
6208
                        $args = 3;
6209
6210
                        break;
6211
                    case 207:
6212
                        $function = 'REPLACEB';
6213
                        $args = 4;
6214
6215
                        break;
6216
                    case 210:
6217
                        $function = 'MIDB';
6218
                        $args = 3;
6219
6220
                        break;
6221
                    case 211:
6222
                        $function = 'LENB';
6223
                        $args = 1;
6224
6225
                        break;
6226
                    case 212:
6227
                        $function = 'ROUNDUP';
6228
                        $args = 2;
6229
6230
                        break;
6231
                    case 213:
6232
                        $function = 'ROUNDDOWN';
6233
                        $args = 2;
6234
6235
                        break;
6236
                    case 214:
6237
                        $function = 'ASC';
6238
                        $args = 1;
6239
6240
                        break;
6241
                    case 215:
6242
                        $function = 'DBCS';
6243
                        $args = 1;
6244
6245
                        break;
6246
                    case 221:
6247
                        $function = 'TODAY';
6248
                        $args = 0;
6249
6250
                        break;
6251
                    case 229:
6252
                        $function = 'SINH';
6253
                        $args = 1;
6254
6255
                        break;
6256
                    case 230:
6257
                        $function = 'COSH';
6258
                        $args = 1;
6259
6260
                        break;
6261
                    case 231:
6262
                        $function = 'TANH';
6263
                        $args = 1;
6264
6265
                        break;
6266
                    case 232:
6267
                        $function = 'ASINH';
6268
                        $args = 1;
6269
6270
                        break;
6271
                    case 233:
6272
                        $function = 'ACOSH';
6273
                        $args = 1;
6274
6275
                        break;
6276
                    case 234:
6277
                        $function = 'ATANH';
6278
                        $args = 1;
6279
6280
                        break;
6281
                    case 235:
6282
                        $function = 'DGET';
6283
                        $args = 3;
6284
6285
                        break;
6286
                    case 244:
6287
                        $function = 'INFO';
6288
                        $args = 1;
6289
6290
                        break;
6291
                    case 252:
6292
                        $function = 'FREQUENCY';
6293
                        $args = 2;
6294
6295
                        break;
6296
                    case 261:
6297
                        $function = 'ERROR.TYPE';
6298
                        $args = 1;
6299
6300
                        break;
6301
                    case 271:
6302
                        $function = 'GAMMALN';
6303
                        $args = 1;
6304
6305
                        break;
6306
                    case 273:
6307
                        $function = 'BINOMDIST';
6308
                        $args = 4;
6309
6310
                        break;
6311
                    case 274:
6312
                        $function = 'CHIDIST';
6313
                        $args = 2;
6314
6315
                        break;
6316
                    case 275:
6317
                        $function = 'CHIINV';
6318
                        $args = 2;
6319
6320
                        break;
6321
                    case 276:
6322
                        $function = 'COMBIN';
6323
                        $args = 2;
6324
6325
                        break;
6326
                    case 277:
6327
                        $function = 'CONFIDENCE';
6328
                        $args = 3;
6329
6330
                        break;
6331
                    case 278:
6332
                        $function = 'CRITBINOM';
6333
                        $args = 3;
6334
6335
                        break;
6336
                    case 279:
6337
                        $function = 'EVEN';
6338
                        $args = 1;
6339
6340
                        break;
6341
                    case 280:
6342
                        $function = 'EXPONDIST';
6343
                        $args = 3;
6344
6345
                        break;
6346
                    case 281:
6347
                        $function = 'FDIST';
6348
                        $args = 3;
6349
6350
                        break;
6351
                    case 282:
6352
                        $function = 'FINV';
6353
                        $args = 3;
6354
6355
                        break;
6356
                    case 283:
6357
                        $function = 'FISHER';
6358
                        $args = 1;
6359
6360
                        break;
6361
                    case 284:
6362
                        $function = 'FISHERINV';
6363
                        $args = 1;
6364
6365
                        break;
6366
                    case 285:
6367
                        $function = 'FLOOR';
6368
                        $args = 2;
6369
6370
                        break;
6371
                    case 286:
6372
                        $function = 'GAMMADIST';
6373
                        $args = 4;
6374
6375
                        break;
6376
                    case 287:
6377
                        $function = 'GAMMAINV';
6378
                        $args = 3;
6379
6380
                        break;
6381
                    case 288:
6382
                        $function = 'CEILING';
6383
                        $args = 2;
6384
6385
                        break;
6386
                    case 289:
6387
                        $function = 'HYPGEOMDIST';
6388
                        $args = 4;
6389
6390
                        break;
6391
                    case 290:
6392
                        $function = 'LOGNORMDIST';
6393
                        $args = 3;
6394
6395
                        break;
6396
                    case 291:
6397
                        $function = 'LOGINV';
6398
                        $args = 3;
6399
6400
                        break;
6401
                    case 292:
6402
                        $function = 'NEGBINOMDIST';
6403
                        $args = 3;
6404
6405
                        break;
6406
                    case 293:
6407
                        $function = 'NORMDIST';
6408
                        $args = 4;
6409
6410
                        break;
6411
                    case 294:
6412
                        $function = 'NORMSDIST';
6413
                        $args = 1;
6414
6415
                        break;
6416
                    case 295:
6417
                        $function = 'NORMINV';
6418
                        $args = 3;
6419
6420
                        break;
6421
                    case 296:
6422
                        $function = 'NORMSINV';
6423
                        $args = 1;
6424
6425
                        break;
6426
                    case 297:
6427
                        $function = 'STANDARDIZE';
6428
                        $args = 3;
6429
6430
                        break;
6431
                    case 298:
6432
                        $function = 'ODD';
6433
                        $args = 1;
6434
6435
                        break;
6436
                    case 299:
6437
                        $function = 'PERMUT';
6438
                        $args = 2;
6439
6440
                        break;
6441
                    case 300:
6442
                        $function = 'POISSON';
6443
                        $args = 3;
6444
6445
                        break;
6446
                    case 301:
6447
                        $function = 'TDIST';
6448
                        $args = 3;
6449
6450
                        break;
6451
                    case 302:
6452
                        $function = 'WEIBULL';
6453
                        $args = 4;
6454
6455
                        break;
6456
                    case 303:
6457
                        $function = 'SUMXMY2';
6458
                        $args = 2;
6459
6460
                        break;
6461
                    case 304:
6462
                        $function = 'SUMX2MY2';
6463
                        $args = 2;
6464
6465
                        break;
6466
                    case 305:
6467
                        $function = 'SUMX2PY2';
6468
                        $args = 2;
6469
6470
                        break;
6471
                    case 306:
6472
                        $function = 'CHITEST';
6473
                        $args = 2;
6474
6475
                        break;
6476
                    case 307:
6477
                        $function = 'CORREL';
6478
                        $args = 2;
6479
6480
                        break;
6481
                    case 308:
6482
                        $function = 'COVAR';
6483
                        $args = 2;
6484
6485
                        break;
6486
                    case 309:
6487
                        $function = 'FORECAST';
6488
                        $args = 3;
6489
6490
                        break;
6491
                    case 310:
6492
                        $function = 'FTEST';
6493
                        $args = 2;
6494
6495
                        break;
6496
                    case 311:
6497
                        $function = 'INTERCEPT';
6498
                        $args = 2;
6499
6500
                        break;
6501
                    case 312:
6502
                        $function = 'PEARSON';
6503
                        $args = 2;
6504
6505
                        break;
6506
                    case 313:
6507
                        $function = 'RSQ';
6508
                        $args = 2;
6509
6510
                        break;
6511
                    case 314:
6512
                        $function = 'STEYX';
6513
                        $args = 2;
6514
6515
                        break;
6516
                    case 315:
6517
                        $function = 'SLOPE';
6518
                        $args = 2;
6519
6520
                        break;
6521
                    case 316:
6522
                        $function = 'TTEST';
6523
                        $args = 4;
6524
6525
                        break;
6526
                    case 325:
6527
                        $function = 'LARGE';
6528
                        $args = 2;
6529
6530
                        break;
6531
                    case 326:
6532
                        $function = 'SMALL';
6533
                        $args = 2;
6534
6535
                        break;
6536
                    case 327:
6537
                        $function = 'QUARTILE';
6538
                        $args = 2;
6539
6540
                        break;
6541
                    case 328:
6542
                        $function = 'PERCENTILE';
6543
                        $args = 2;
6544
6545
                        break;
6546
                    case 331:
6547
                        $function = 'TRIMMEAN';
6548
                        $args = 2;
6549
6550
                        break;
6551
                    case 332:
6552
                        $function = 'TINV';
6553
                        $args = 2;
6554
6555
                        break;
6556
                    case 337:
6557
                        $function = 'POWER';
6558
                        $args = 2;
6559
6560
                        break;
6561
                    case 342:
6562
                        $function = 'RADIANS';
6563
                        $args = 1;
6564
6565
                        break;
6566
                    case 343:
6567
                        $function = 'DEGREES';
6568
                        $args = 1;
6569
6570
                        break;
6571
                    case 346:
6572
                        $function = 'COUNTIF';
6573
                        $args = 2;
6574
6575
                        break;
6576
                    case 347:
6577
                        $function = 'COUNTBLANK';
6578
                        $args = 1;
6579
6580
                        break;
6581
                    case 350:
6582
                        $function = 'ISPMT';
6583
                        $args = 4;
6584
6585
                        break;
6586
                    case 351:
6587
                        $function = 'DATEDIF';
6588
                        $args = 3;
6589
6590
                        break;
6591
                    case 352:
6592
                        $function = 'DATESTRING';
6593
                        $args = 1;
6594
6595
                        break;
6596
                    case 353:
6597
                        $function = 'NUMBERSTRING';
6598
                        $args = 2;
6599
6600
                        break;
6601
                    case 360:
6602
                        $function = 'PHONETIC';
6603
                        $args = 1;
6604
6605
                        break;
6606
                    case 368:
6607
                        $function = 'BAHTTEXT';
6608
                        $args = 1;
6609
6610
                        break;
6611
                    default:
6612
                        throw new Exception('Unrecognized function in formula');
6613
6614
                        break;
6615
                }
6616 8
                $data = ['function' => $function, 'args' => $args];
6617
6618 8
                break;
6619 17
            case 0x22:    //    function with variable number of arguments
6620 17
            case 0x42:
6621 17
            case 0x62:
6622 5
                $name = 'tFuncV';
6623 5
                $size = 4;
6624
                // offset: 1; size: 1; number of arguments
6625 5
                $args = ord($formulaData[1]);
6626
                // offset: 2: size: 2; index to built-in sheet function
6627 5
                $index = self::getUInt2d($formulaData, 2);
6628
                switch ($index) {
6629 5
                    case 0:
6630 2
                        $function = 'COUNT';
6631
6632 2
                        break;
6633 5
                    case 1:
6634 1
                        $function = 'IF';
6635
6636 1
                        break;
6637 5
                    case 4:
6638 5
                        $function = 'SUM';
6639
6640 5
                        break;
6641
                    case 5:
6642
                        $function = 'AVERAGE';
6643
6644
                        break;
6645
                    case 6:
6646
                        $function = 'MIN';
6647
6648
                        break;
6649
                    case 7:
6650
                        $function = 'MAX';
6651
6652
                        break;
6653
                    case 8:
6654
                        $function = 'ROW';
6655
6656
                        break;
6657
                    case 9:
6658
                        $function = 'COLUMN';
6659
6660
                        break;
6661
                    case 11:
6662
                        $function = 'NPV';
6663
6664
                        break;
6665
                    case 12:
6666
                        $function = 'STDEV';
6667
6668
                        break;
6669
                    case 13:
6670
                        $function = 'DOLLAR';
6671
6672
                        break;
6673
                    case 14:
6674
                        $function = 'FIXED';
6675
6676
                        break;
6677
                    case 28:
6678
                        $function = 'LOOKUP';
6679
6680
                        break;
6681
                    case 29:
6682
                        $function = 'INDEX';
6683
6684
                        break;
6685
                    case 36:
6686
                        $function = 'AND';
6687
6688
                        break;
6689
                    case 37:
6690
                        $function = 'OR';
6691
6692
                        break;
6693
                    case 46:
6694
                        $function = 'VAR';
6695
6696
                        break;
6697
                    case 49:
6698
                        $function = 'LINEST';
6699
6700
                        break;
6701
                    case 50:
6702
                        $function = 'TREND';
6703
6704
                        break;
6705
                    case 51:
6706
                        $function = 'LOGEST';
6707
6708
                        break;
6709
                    case 52:
6710
                        $function = 'GROWTH';
6711
6712
                        break;
6713
                    case 56:
6714
                        $function = 'PV';
6715
6716
                        break;
6717
                    case 57:
6718
                        $function = 'FV';
6719
6720
                        break;
6721
                    case 58:
6722
                        $function = 'NPER';
6723
6724
                        break;
6725
                    case 59:
6726
                        $function = 'PMT';
6727
6728
                        break;
6729
                    case 60:
6730
                        $function = 'RATE';
6731
6732
                        break;
6733
                    case 62:
6734
                        $function = 'IRR';
6735
6736
                        break;
6737
                    case 64:
6738
                        $function = 'MATCH';
6739
6740
                        break;
6741
                    case 70:
6742
                        $function = 'WEEKDAY';
6743
6744
                        break;
6745
                    case 78:
6746
                        $function = 'OFFSET';
6747
6748
                        break;
6749
                    case 82:
6750
                        $function = 'SEARCH';
6751
6752
                        break;
6753
                    case 100:
6754
                        $function = 'CHOOSE';
6755
6756
                        break;
6757
                    case 101:
6758
                        $function = 'HLOOKUP';
6759
6760
                        break;
6761
                    case 102:
6762
                        $function = 'VLOOKUP';
6763
6764
                        break;
6765
                    case 109:
6766
                        $function = 'LOG';
6767
6768
                        break;
6769
                    case 115:
6770
                        $function = 'LEFT';
6771
6772
                        break;
6773
                    case 116:
6774
                        $function = 'RIGHT';
6775
6776
                        break;
6777
                    case 120:
6778
                        $function = 'SUBSTITUTE';
6779
6780
                        break;
6781
                    case 124:
6782
                        $function = 'FIND';
6783
6784
                        break;
6785
                    case 125:
6786
                        $function = 'CELL';
6787
6788
                        break;
6789
                    case 144:
6790
                        $function = 'DDB';
6791
6792
                        break;
6793
                    case 148:
6794
                        $function = 'INDIRECT';
6795
6796
                        break;
6797
                    case 167:
6798
                        $function = 'IPMT';
6799
6800
                        break;
6801
                    case 168:
6802
                        $function = 'PPMT';
6803
6804
                        break;
6805
                    case 169:
6806
                        $function = 'COUNTA';
6807
6808
                        break;
6809
                    case 183:
6810
                        $function = 'PRODUCT';
6811
6812
                        break;
6813
                    case 193:
6814
                        $function = 'STDEVP';
6815
6816
                        break;
6817
                    case 194:
6818
                        $function = 'VARP';
6819
6820
                        break;
6821
                    case 197:
6822
                        $function = 'TRUNC';
6823
6824
                        break;
6825
                    case 204:
6826
                        $function = 'USDOLLAR';
6827
6828
                        break;
6829
                    case 205:
6830
                        $function = 'FINDB';
6831
6832
                        break;
6833
                    case 206:
6834
                        $function = 'SEARCHB';
6835
6836
                        break;
6837
                    case 208:
6838
                        $function = 'LEFTB';
6839
6840
                        break;
6841
                    case 209:
6842
                        $function = 'RIGHTB';
6843
6844
                        break;
6845
                    case 216:
6846
                        $function = 'RANK';
6847
6848
                        break;
6849
                    case 219:
6850
                        $function = 'ADDRESS';
6851
6852
                        break;
6853
                    case 220:
6854
                        $function = 'DAYS360';
6855
6856
                        break;
6857
                    case 222:
6858
                        $function = 'VDB';
6859
6860
                        break;
6861
                    case 227:
6862
                        $function = 'MEDIAN';
6863
6864
                        break;
6865
                    case 228:
6866
                        $function = 'SUMPRODUCT';
6867
6868
                        break;
6869
                    case 247:
6870
                        $function = 'DB';
6871
6872
                        break;
6873
                    case 255:
6874
                        $function = '';
6875
6876
                        break;
6877
                    case 269:
6878
                        $function = 'AVEDEV';
6879
6880
                        break;
6881
                    case 270:
6882
                        $function = 'BETADIST';
6883
6884
                        break;
6885
                    case 272:
6886
                        $function = 'BETAINV';
6887
6888
                        break;
6889
                    case 317:
6890
                        $function = 'PROB';
6891
6892
                        break;
6893
                    case 318:
6894
                        $function = 'DEVSQ';
6895
6896
                        break;
6897
                    case 319:
6898
                        $function = 'GEOMEAN';
6899
6900
                        break;
6901
                    case 320:
6902
                        $function = 'HARMEAN';
6903
6904
                        break;
6905
                    case 321:
6906
                        $function = 'SUMSQ';
6907
6908
                        break;
6909
                    case 322:
6910
                        $function = 'KURT';
6911
6912
                        break;
6913
                    case 323:
6914
                        $function = 'SKEW';
6915
6916
                        break;
6917
                    case 324:
6918
                        $function = 'ZTEST';
6919
6920
                        break;
6921
                    case 329:
6922
                        $function = 'PERCENTRANK';
6923
6924
                        break;
6925
                    case 330:
6926
                        $function = 'MODE';
6927
6928
                        break;
6929
                    case 336:
6930
                        $function = 'CONCATENATE';
6931
6932
                        break;
6933
                    case 344:
6934
                        $function = 'SUBTOTAL';
6935
6936
                        break;
6937
                    case 345:
6938
                        $function = 'SUMIF';
6939
6940
                        break;
6941
                    case 354:
6942
                        $function = 'ROMAN';
6943
6944
                        break;
6945
                    case 358:
6946
                        $function = 'GETPIVOTDATA';
6947
6948
                        break;
6949
                    case 359:
6950
                        $function = 'HYPERLINK';
6951
6952
                        break;
6953
                    case 361:
6954
                        $function = 'AVERAGEA';
6955
6956
                        break;
6957
                    case 362:
6958
                        $function = 'MAXA';
6959
6960
                        break;
6961
                    case 363:
6962
                        $function = 'MINA';
6963
6964
                        break;
6965
                    case 364:
6966
                        $function = 'STDEVPA';
6967
6968
                        break;
6969
                    case 365:
6970
                        $function = 'VARPA';
6971
6972
                        break;
6973
                    case 366:
6974
                        $function = 'STDEVA';
6975
6976
                        break;
6977
                    case 367:
6978
                        $function = 'VARA';
6979
6980
                        break;
6981
                    default:
6982
                        throw new Exception('Unrecognized function in formula');
6983
6984
                        break;
6985
                }
6986 5
                $data = ['function' => $function, 'args' => $args];
6987
6988 5
                break;
6989 17
            case 0x23:    //    index to defined name
6990 17
            case 0x43:
6991 17
            case 0x63:
6992
                $name = 'tName';
6993
                $size = 5;
6994
                // offset: 1; size: 2; one-based index to definedname record
6995
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
6996
                // offset: 2; size: 2; not used
6997
                $data = $this->definedname[$definedNameIndex]['name'];
6998
6999
                break;
7000 17
            case 0x24:    //    single cell reference e.g. A5
7001 17
            case 0x44:
7002 17
            case 0x64:
7003 3
                $name = 'tRef';
7004 3
                $size = 5;
7005 3
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));
7006
7007 3
                break;
7008 17
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
7009 6
            case 0x45:
7010 6
            case 0x65:
7011 15
                $name = 'tArea';
7012 15
                $size = 9;
7013 15
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
7014
7015 15
                break;
7016 6
            case 0x26:    //    Constant reference sub-expression
7017 6
            case 0x46:
7018 6
            case 0x66:
7019
                $name = 'tMemArea';
7020
                // offset: 1; size: 4; not used
7021
                // offset: 5; size: 2; size of the following subexpression
7022
                $subSize = self::getUInt2d($formulaData, 5);
7023
                $size = 7 + $subSize;
7024
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
7025
7026
                break;
7027 6
            case 0x27:    //    Deleted constant reference sub-expression
7028 6
            case 0x47:
7029 6
            case 0x67:
7030
                $name = 'tMemErr';
7031
                // offset: 1; size: 4; not used
7032
                // offset: 5; size: 2; size of the following subexpression
7033
                $subSize = self::getUInt2d($formulaData, 5);
7034
                $size = 7 + $subSize;
7035
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
7036
7037
                break;
7038 6
            case 0x29:    //    Variable reference sub-expression
7039 6
            case 0x49:
7040 6
            case 0x69:
7041
                $name = 'tMemFunc';
7042
                // offset: 1; size: 2; size of the following sub-expression
7043
                $subSize = self::getUInt2d($formulaData, 1);
7044
                $size = 3 + $subSize;
7045
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
7046
7047
                break;
7048 6
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
7049 6
            case 0x4C:
7050 6
            case 0x6C:
7051
                $name = 'tRefN';
7052
                $size = 5;
7053
                $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
7054
7055
                break;
7056 6
            case 0x2D:    //    Relative 2d range reference
7057 6
            case 0x4D:
7058 6
            case 0x6D:
7059
                $name = 'tAreaN';
7060
                $size = 9;
7061
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
7062
7063
                break;
7064 6
            case 0x39:    //    External name
7065 6
            case 0x59:
7066 6
            case 0x79:
7067
                $name = 'tNameX';
7068
                $size = 7;
7069
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
7070
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
7071
                $index = self::getUInt2d($formulaData, 3);
7072
                // assume index is to EXTERNNAME record
7073
                $data = $this->externalNames[$index - 1]['name'];
7074
                // offset: 5; size: 2; not used
7075
                break;
7076 6
            case 0x3A:    //    3d reference to cell
7077 5
            case 0x5A:
7078 5
            case 0x7A:
7079 1
                $name = 'tRef3d';
7080 1
                $size = 7;
7081
7082
                try {
7083
                    // offset: 1; size: 2; index to REF entry
7084 1
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
7085
                    // offset: 3; size: 4; cell address
7086 1
                    $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4));
7087
7088 1
                    $data = "$sheetRange!$cellAddress";
7089
                } catch (PhpSpreadsheetException $e) {
7090
                    // deleted sheet reference
7091
                    $data = '#REF!';
7092
                }
7093
7094 1
                break;
7095 5
            case 0x3B:    //    3d reference to cell range
7096
            case 0x5B:
7097
            case 0x7B:
7098 5
                $name = 'tArea3d';
7099 5
                $size = 11;
7100
7101
                try {
7102
                    // offset: 1; size: 2; index to REF entry
7103 5
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
7104
                    // offset: 3; size: 8; cell address
7105 5
                    $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
7106
7107 5
                    $data = "$sheetRange!$cellRangeAddress";
7108
                } catch (PhpSpreadsheetException $e) {
7109
                    // deleted sheet reference
7110
                    $data = '#REF!';
7111
                }
7112
7113 5
                break;
7114
            // Unknown cases    // don't know how to deal with
7115
            default:
7116
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
7117
7118
                break;
7119
        }
7120
7121
        return [
7122 17
            'id' => $id,
7123 17
            'name' => $name,
7124 17
            'size' => $size,
7125 17
            'data' => $data,
7126
        ];
7127
    }
7128
7129
    /**
7130
     * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2'
7131
     * section 3.3.4.
7132
     *
7133
     * @param string $cellAddressStructure
7134
     *
7135
     * @return string
7136
     */
7137 4
    private function readBIFF8CellAddress($cellAddressStructure)
7138
    {
7139
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7140 4
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
7141
7142
        // offset: 2; size: 2; index to column or column offset + relative flags
7143
        // bit: 7-0; mask 0x00FF; column index
7144 4
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);
7145
7146
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7147 4
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
7148 3
            $column = '$' . $column;
7149
        }
7150
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7151 4
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7152 3
            $row = '$' . $row;
7153
        }
7154
7155 4
        return $column . $row;
7156
    }
7157
7158
    /**
7159
     * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column
7160
     * to indicate offsets from a base cell
7161
     * section 3.3.4.
7162
     *
7163
     * @param string $cellAddressStructure
7164
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
7165
     *
7166
     * @return string
7167
     */
7168
    private function readBIFF8CellAddressB($cellAddressStructure, $baseCell = 'A1')
7169
    {
7170
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
7171
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
7172
7173
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7174
        $rowIndex = self::getUInt2d($cellAddressStructure, 0);
7175
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
7176
7177
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7178
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
7179
            // offset: 2; size: 2; index to column or column offset + relative flags
7180
            // bit: 7-0; mask 0x00FF; column index
7181
            $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2);
7182
7183
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
7184
            $column = '$' . $column;
7185
        } else {
7186
            // offset: 2; size: 2; index to column or column offset + relative flags
7187
            // bit: 7-0; mask 0x00FF; column index
7188
            $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2);
7189
            $colIndex = $baseCol + $relativeColIndex;
7190
            $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256;
7191
            $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256;
7192
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
7193
        }
7194
7195
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7196
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7197
            $row = '$' . $row;
7198
        } else {
7199
            $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536;
7200
            $row = $baseRow + $rowIndex;
7201
        }
7202
7203
        return $column . $row;
7204
    }
7205
7206
    /**
7207
     * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1'
7208
     * always fixed range
7209
     * section 2.5.14.
7210
     *
7211
     * @param string $subData
7212
     *
7213
     * @return string
7214
     */
7215 33
    private function readBIFF5CellRangeAddressFixed($subData)
7216
    {
7217
        // offset: 0; size: 2; index to first row
7218 33
        $fr = self::getUInt2d($subData, 0) + 1;
7219
7220
        // offset: 2; size: 2; index to last row
7221 33
        $lr = self::getUInt2d($subData, 2) + 1;
7222
7223
        // offset: 4; size: 1; index to first column
7224 33
        $fc = ord($subData[4]);
7225
7226
        // offset: 5; size: 1; index to last column
7227 33
        $lc = ord($subData[5]);
7228
7229
        // check values
7230 33
        if ($fr > $lr || $fc > $lc) {
7231
            throw new Exception('Not a cell range address');
7232
        }
7233
7234
        // column index to letter
7235 33
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7236 33
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7237
7238 33
        if ($fr == $lr && $fc == $lc) {
7239 29
            return "$fc$fr";
7240
        }
7241
7242 16
        return "$fc$fr:$lc$lr";
7243
    }
7244
7245
    /**
7246
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1'
7247
     * always fixed range
7248
     * section 2.5.14.
7249
     *
7250
     * @param string $subData
7251
     *
7252
     * @return string
7253
     */
7254 13
    private function readBIFF8CellRangeAddressFixed($subData)
7255
    {
7256
        // offset: 0; size: 2; index to first row
7257 13
        $fr = self::getUInt2d($subData, 0) + 1;
7258
7259
        // offset: 2; size: 2; index to last row
7260 13
        $lr = self::getUInt2d($subData, 2) + 1;
7261
7262
        // offset: 4; size: 2; index to first column
7263 13
        $fc = self::getUInt2d($subData, 4);
7264
7265
        // offset: 6; size: 2; index to last column
7266 13
        $lc = self::getUInt2d($subData, 6);
7267
7268
        // check values
7269 13
        if ($fr > $lr || $fc > $lc) {
7270
            throw new Exception('Not a cell range address');
7271
        }
7272
7273
        // column index to letter
7274 13
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7275 13
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7276
7277 13
        if ($fr == $lr && $fc == $lc) {
7278 3
            return "$fc$fr";
7279
        }
7280
7281 13
        return "$fc$fr:$lc$lr";
7282
    }
7283
7284
    /**
7285
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6'
7286
     * there are flags indicating whether column/row index is relative
7287
     * section 3.3.4.
7288
     *
7289
     * @param string $subData
7290
     *
7291
     * @return string
7292
     */
7293 16
    private function readBIFF8CellRangeAddress($subData)
7294
    {
7295
        // todo: if cell range is just a single cell, should this funciton
7296
        // not just return e.g. 'A1' and not 'A1:A1' ?
7297
7298
        // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
7299 16
        $fr = self::getUInt2d($subData, 0) + 1;
7300
7301
        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
7302 16
        $lr = self::getUInt2d($subData, 2) + 1;
7303
7304
        // offset: 4; size: 2; index to first column or column offset + relative flags
7305
7306
        // bit: 7-0; mask 0x00FF; column index
7307 16
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);
7308
7309
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7310 16
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
7311 5
            $fc = '$' . $fc;
7312
        }
7313
7314
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7315 16
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
7316 5
            $fr = '$' . $fr;
7317
        }
7318
7319
        // offset: 6; size: 2; index to last column or column offset + relative flags
7320
7321
        // bit: 7-0; mask 0x00FF; column index
7322 16
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);
7323
7324
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7325 16
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
7326 5
            $lc = '$' . $lc;
7327
        }
7328
7329
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7330 16
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7331 5
            $lr = '$' . $lr;
7332
        }
7333
7334 16
        return "$fc$fr:$lc$lr";
7335
    }
7336
7337
    /**
7338
     * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column
7339
     * to indicate offsets from a base cell
7340
     * section 3.3.4.
7341
     *
7342
     * @param string $subData
7343
     * @param string $baseCell Base cell
7344
     *
7345
     * @return string Cell range address
7346
     */
7347
    private function readBIFF8CellRangeAddressB($subData, $baseCell = 'A1')
7348
    {
7349
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
7350
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
7351
7352
        // TODO: if cell range is just a single cell, should this funciton
7353
        // not just return e.g. 'A1' and not 'A1:A1' ?
7354
7355
        // offset: 0; size: 2; first row
7356
        $frIndex = self::getUInt2d($subData, 0); // adjust below
7357
7358
        // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767)
7359
        $lrIndex = self::getUInt2d($subData, 2); // adjust below
7360
7361
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7362
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
7363
            // absolute column index
7364
            // offset: 4; size: 2; first column with relative/absolute flags
7365
            // bit: 7-0; mask 0x00FF; column index
7366
            $fcIndex = 0x00FF & self::getUInt2d($subData, 4);
7367
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
7368
            $fc = '$' . $fc;
7369
        } else {
7370
            // column offset
7371
            // offset: 4; size: 2; first column with relative/absolute flags
7372
            // bit: 7-0; mask 0x00FF; column index
7373
            $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4);
7374
            $fcIndex = $baseCol + $relativeFcIndex;
7375
            $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256;
7376
            $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256;
7377
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
7378
        }
7379
7380
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7381
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
7382
            // absolute row index
7383
            $fr = $frIndex + 1;
7384
            $fr = '$' . $fr;
7385
        } else {
7386
            // row offset
7387
            $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536;
7388
            $fr = $baseRow + $frIndex;
7389
        }
7390
7391
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7392
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
7393
            // absolute column index
7394
            // offset: 6; size: 2; last column with relative/absolute flags
7395
            // bit: 7-0; mask 0x00FF; column index
7396
            $lcIndex = 0x00FF & self::getUInt2d($subData, 6);
7397
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
7398
            $lc = '$' . $lc;
7399
        } else {
7400
            // column offset
7401
            // offset: 4; size: 2; first column with relative/absolute flags
7402
            // bit: 7-0; mask 0x00FF; column index
7403
            $relativeLcIndex = 0x00FF & self::getInt2d($subData, 4);
7404
            $lcIndex = $baseCol + $relativeLcIndex;
7405
            $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256;
7406
            $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256;
7407
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
7408
        }
7409
7410
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7411
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7412
            // absolute row index
7413
            $lr = $lrIndex + 1;
7414
            $lr = '$' . $lr;
7415
        } else {
7416
            // row offset
7417
            $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536;
7418
            $lr = $baseRow + $lrIndex;
7419
        }
7420
7421
        return "$fc$fr:$lc$lr";
7422
    }
7423
7424
    /**
7425
     * Read BIFF8 cell range address list
7426
     * section 2.5.15.
7427
     *
7428
     * @param string $subData
7429
     *
7430
     * @return array
7431
     */
7432 13
    private function readBIFF8CellRangeAddressList($subData)
7433
    {
7434 13
        $cellRangeAddresses = [];
7435
7436
        // offset: 0; size: 2; number of the following cell range addresses
7437 13
        $nm = self::getUInt2d($subData, 0);
7438
7439 13
        $offset = 2;
7440
        // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses
7441 13
        for ($i = 0; $i < $nm; ++$i) {
7442 13
            $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8));
7443 13
            $offset += 8;
7444
        }
7445
7446
        return [
7447 13
            'size' => 2 + 8 * $nm,
7448 13
            'cellRangeAddresses' => $cellRangeAddresses,
7449
        ];
7450
    }
7451
7452
    /**
7453
     * Read BIFF5 cell range address list
7454
     * section 2.5.15.
7455
     *
7456
     * @param string $subData
7457
     *
7458
     * @return array
7459
     */
7460 33
    private function readBIFF5CellRangeAddressList($subData)
7461
    {
7462 33
        $cellRangeAddresses = [];
7463
7464
        // offset: 0; size: 2; number of the following cell range addresses
7465 33
        $nm = self::getUInt2d($subData, 0);
7466
7467 33
        $offset = 2;
7468
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
7469 33
        for ($i = 0; $i < $nm; ++$i) {
7470 33
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
7471 33
            $offset += 6;
7472
        }
7473
7474
        return [
7475 33
            'size' => 2 + 6 * $nm,
7476 33
            'cellRangeAddresses' => $cellRangeAddresses,
7477
        ];
7478
    }
7479
7480
    /**
7481
     * Get a sheet range like Sheet1:Sheet3 from REF index
7482
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
7483
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
7484
     * in which case an Exception is thrown.
7485
     *
7486
     * @param int $index
7487
     *
7488
     * @return false|string
7489
     */
7490 6
    private function readSheetRangeByRefIndex($index)
7491
    {
7492 6
        if (isset($this->ref[$index])) {
7493 6
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
7494
7495
            switch ($type) {
7496 6
                case 'internal':
7497
                    // check if we have a deleted 3d reference
7498 6
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
7499
                        throw new Exception('Deleted sheet reference');
7500
                    }
7501
7502
                    // we have normal sheet range (collapsed or uncollapsed)
7503 6
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
7504 6
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
7505
7506 6
                    if ($firstSheetName == $lastSheetName) {
7507
                        // collapsed sheet range
7508 6
                        $sheetRange = $firstSheetName;
7509
                    } else {
7510
                        $sheetRange = "$firstSheetName:$lastSheetName";
7511
                    }
7512
7513
                    // escape the single-quotes
7514 6
                    $sheetRange = str_replace("'", "''", $sheetRange);
7515
7516
                    // if there are special characters, we need to enclose the range in single-quotes
7517
                    // todo: check if we have identified the whole set of special characters
7518
                    // it seems that the following characters are not accepted for sheet names
7519
                    // and we may assume that they are not present: []*/:\?
7520 6
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
7521 1
                        $sheetRange = "'$sheetRange'";
7522
                    }
7523
7524 6
                    return $sheetRange;
7525
7526
                    break;
7527
                default:
7528
                    // TODO: external sheet support
7529
                    throw new Exception('Xls reader only supports internal sheets in formulas');
7530
7531
                    break;
7532
            }
7533
        }
7534
7535
        return false;
7536
    }
7537
7538
    /**
7539
     * read BIFF8 constant value array from array data
7540
     * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40]
7541
     * section 2.5.8.
7542
     *
7543
     * @param string $arrayData
7544
     *
7545
     * @return array
7546
     */
7547
    private static function readBIFF8ConstantArray($arrayData)
7548
    {
7549
        // offset: 0; size: 1; number of columns decreased by 1
7550
        $nc = ord($arrayData[0]);
7551
7552
        // offset: 1; size: 2; number of rows decreased by 1
7553
        $nr = self::getUInt2d($arrayData, 1);
7554
        $size = 3; // initialize
7555
        $arrayData = substr($arrayData, 3);
7556
7557
        // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values
7558
        $matrixChunks = [];
7559
        for ($r = 1; $r <= $nr + 1; ++$r) {
7560
            $items = [];
7561
            for ($c = 1; $c <= $nc + 1; ++$c) {
7562
                $constant = self::readBIFF8Constant($arrayData);
7563
                $items[] = $constant['value'];
7564
                $arrayData = substr($arrayData, $constant['size']);
7565
                $size += $constant['size'];
7566
            }
7567
            $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"'
7568
        }
7569
        $matrix = '{' . implode(';', $matrixChunks) . '}';
7570
7571
        return [
7572
            'value' => $matrix,
7573
            'size' => $size,
7574
        ];
7575
    }
7576
7577
    /**
7578
     * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value'
7579
     * section 2.5.7
7580
     * returns e.g. ['value' => '5', 'size' => 9].
7581
     *
7582
     * @param string $valueData
7583
     *
7584
     * @return array
7585
     */
7586
    private static function readBIFF8Constant($valueData)
7587
    {
7588
        // offset: 0; size: 1; identifier for type of constant
7589
        $identifier = ord($valueData[0]);
7590
7591
        switch ($identifier) {
7592
            case 0x00: // empty constant (what is this?)
7593
                $value = '';
7594
                $size = 9;
7595
7596
                break;
7597
            case 0x01: // number
7598
                // offset: 1; size: 8; IEEE 754 floating-point value
7599
                $value = self::extractNumber(substr($valueData, 1, 8));
7600
                $size = 9;
7601
7602
                break;
7603
            case 0x02: // string value
7604
                // offset: 1; size: var; Unicode string, 16-bit string length
7605
                $string = self::readUnicodeStringLong(substr($valueData, 1));
7606
                $value = '"' . $string['value'] . '"';
7607
                $size = 1 + $string['size'];
7608
7609
                break;
7610
            case 0x04: // boolean
7611
                // offset: 1; size: 1; 0 = FALSE, 1 = TRUE
7612
                if (ord($valueData[1])) {
7613
                    $value = 'TRUE';
7614
                } else {
7615
                    $value = 'FALSE';
7616
                }
7617
                $size = 9;
7618
7619
                break;
7620
            case 0x10: // error code
7621
                // offset: 1; size: 1; error code
7622
                $value = Xls\ErrorCode::lookup(ord($valueData[1]));
7623
                $size = 9;
7624
7625
                break;
7626
        }
7627
7628
        return [
7629
            'value' => $value,
7630
            'size' => $size,
7631
        ];
7632
    }
7633
7634
    /**
7635
     * Extract RGB color
7636
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4.
7637
     *
7638
     * @param string $rgb Encoded RGB value (4 bytes)
7639
     *
7640
     * @return array
7641
     */
7642 19
    private static function readRGB($rgb)
7643
    {
7644
        // offset: 0; size 1; Red component
7645 19
        $r = ord($rgb[0]);
7646
7647
        // offset: 1; size: 1; Green component
7648 19
        $g = ord($rgb[1]);
7649
7650
        // offset: 2; size: 1; Blue component
7651 19
        $b = ord($rgb[2]);
7652
7653
        // HEX notation, e.g. 'FF00FC'
7654 19
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);
7655
7656 19
        return ['rgb' => $rgb];
7657
    }
7658
7659
    /**
7660
     * Read byte string (8-bit string length)
7661
     * OpenOffice documentation: 2.5.2.
7662
     *
7663
     * @param string $subData
7664
     *
7665
     * @return array
7666
     */
7667
    private function readByteStringShort($subData)
7668
    {
7669
        // offset: 0; size: 1; length of the string (character count)
7670
        $ln = ord($subData[0]);
7671
7672
        // offset: 1: size: var; character array (8-bit characters)
7673
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7674
7675
        return [
7676
            'value' => $value,
7677
            'size' => 1 + $ln, // size in bytes of data structure
7678
        ];
7679
    }
7680
7681
    /**
7682
     * Read byte string (16-bit string length)
7683
     * OpenOffice documentation: 2.5.2.
7684
     *
7685
     * @param string $subData
7686
     *
7687
     * @return array
7688
     */
7689
    private function readByteStringLong($subData)
7690
    {
7691
        // offset: 0; size: 2; length of the string (character count)
7692
        $ln = self::getUInt2d($subData, 0);
7693
7694
        // offset: 2: size: var; character array (8-bit characters)
7695
        $value = $this->decodeCodepage(substr($subData, 2));
7696
7697
        //return $string;
7698
        return [
7699
            'value' => $value,
7700
            'size' => 2 + $ln, // size in bytes of data structure
7701
        ];
7702
    }
7703
7704
    /**
7705
     * Extracts an Excel Unicode short string (8-bit string length)
7706
     * OpenOffice documentation: 2.5.3
7707
     * function will automatically find out where the Unicode string ends.
7708
     *
7709
     * @param string $subData
7710
     *
7711
     * @return array
7712
     */
7713 39
    private static function readUnicodeStringShort($subData)
7714
    {
7715 39
        $value = '';
7716
7717
        // offset: 0: size: 1; length of the string (character count)
7718 39
        $characterCount = ord($subData[0]);
7719
7720 39
        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
7721
7722
        // add 1 for the string length
7723 39
        ++$string['size'];
7724
7725 39
        return $string;
7726
    }
7727
7728
    /**
7729
     * Extracts an Excel Unicode long string (16-bit string length)
7730
     * OpenOffice documentation: 2.5.3
7731
     * this function is under construction, needs to support rich text, and Asian phonetic settings.
7732
     *
7733
     * @param string $subData
7734
     *
7735
     * @return array
7736
     */
7737 35
    private static function readUnicodeStringLong($subData)
7738
    {
7739 35
        $value = '';
7740
7741
        // offset: 0: size: 2; length of the string (character count)
7742 35
        $characterCount = self::getUInt2d($subData, 0);
7743
7744 35
        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
7745
7746
        // add 2 for the string length
7747 35
        $string['size'] += 2;
7748
7749 35
        return $string;
7750
    }
7751
7752
    /**
7753
     * Read Unicode string with no string length field, but with known character count
7754
     * this function is under construction, needs to support rich text, and Asian phonetic settings
7755
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3.
7756
     *
7757
     * @param string $subData
7758
     * @param int $characterCount
7759
     *
7760
     * @return array
7761
     */
7762 39
    private static function readUnicodeString($subData, $characterCount)
7763
    {
7764 39
        $value = '';
7765
7766
        // offset: 0: size: 1; option flags
7767
        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
7768 39
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
7769
7770
        // bit: 2; mask: 0x04; Asian phonetic settings
7771 39
        $hasAsian = (0x04) & ord($subData[0]) >> 2;
7772
7773
        // bit: 3; mask: 0x08; Rich-Text settings
7774 39
        $hasRichText = (0x08) & ord($subData[0]) >> 3;
7775
7776
        // offset: 1: size: var; character array
7777
        // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
7778
        // needs to be fixed
7779 39
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
7780
7781
        return [
7782 39
            'value' => $value,
7783 39
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
7784
        ];
7785
    }
7786
7787
    /**
7788
     * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas.
7789
     * Example:  hello"world  -->  "hello""world".
7790
     *
7791
     * @param string $value UTF-8 encoded string
7792
     *
7793
     * @return string
7794
     */
7795 1
    private static function UTF8toExcelDoubleQuoted($value)
7796
    {
7797 1
        return '"' . str_replace('"', '""', $value) . '"';
7798
    }
7799
7800
    /**
7801
     * Reads first 8 bytes of a string and return IEEE 754 float.
7802
     *
7803
     * @param string $data Binary string that is at least 8 bytes long
7804
     *
7805
     * @return float
7806
     */
7807 36
    private static function extractNumber($data)
7808
    {
7809 36
        $rknumhigh = self::getInt4d($data, 4);
7810 36
        $rknumlow = self::getInt4d($data, 0);
7811 36
        $sign = ($rknumhigh & 0x80000000) >> 31;
7812 36
        $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
7813 36
        $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
7814 36
        $mantissalow1 = ($rknumlow & 0x80000000) >> 31;
7815 36
        $mantissalow2 = ($rknumlow & 0x7fffffff);
7816 36
        $value = $mantissa / 2 ** (20 - $exp);
7817
7818 36
        if ($mantissalow1 != 0) {
7819 16
            $value += 1 / 2 ** (21 - $exp);
7820
        }
7821
7822 36
        $value += $mantissalow2 / 2 ** (52 - $exp);
7823 36
        if ($sign) {
7824 10
            $value *= -1;
7825
        }
7826
7827 36
        return $value;
7828
    }
7829
7830
    /**
7831
     * @param int $rknum
7832
     *
7833
     * @return float
7834
     */
7835 16
    private static function getIEEE754($rknum)
7836
    {
7837 16
        if (($rknum & 0x02) != 0) {
7838 1
            $value = $rknum >> 2;
7839
        } else {
7840
            // changes by mmp, info on IEEE754 encoding from
7841
            // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
7842
            // The RK format calls for using only the most significant 30 bits
7843
            // of the 64 bit floating point value. The other 34 bits are assumed
7844
            // to be 0 so we use the upper 30 bits of $rknum as follows...
7845 15
            $sign = ($rknum & 0x80000000) >> 31;
7846 15
            $exp = ($rknum & 0x7ff00000) >> 20;
7847 15
            $mantissa = (0x100000 | ($rknum & 0x000ffffc));
7848 15
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
7849 15
            if ($sign) {
7850 10
                $value = -1 * $value;
7851
            }
7852
            //end of changes by mmp
7853
        }
7854 16
        if (($rknum & 0x01) != 0) {
7855 10
            $value /= 100;
7856
        }
7857
7858 16
        return $value;
7859
    }
7860
7861
    /**
7862
     * Get UTF-8 string from (compressed or uncompressed) UTF-16 string.
7863
     *
7864
     * @param string $string
7865
     * @param bool $compressed
7866
     *
7867
     * @return string
7868
     */
7869 39
    private static function encodeUTF16($string, $compressed = false)
7870
    {
7871 39
        if ($compressed) {
7872 24
            $string = self::uncompressByteString($string);
7873
        }
7874
7875 39
        return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE');
7876
    }
7877
7878
    /**
7879
     * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
7880
     *
7881
     * @param string $string
7882
     *
7883
     * @return string
7884
     */
7885 24
    private static function uncompressByteString($string)
7886
    {
7887 24
        $uncompressedString = '';
7888 24
        $strLen = strlen($string);
7889 24
        for ($i = 0; $i < $strLen; ++$i) {
7890 24
            $uncompressedString .= $string[$i] . "\0";
7891
        }
7892
7893 24
        return $uncompressedString;
7894
    }
7895
7896
    /**
7897
     * Convert string to UTF-8. Only used for BIFF5.
7898
     *
7899
     * @param string $string
7900
     *
7901
     * @return string
7902
     */
7903
    private function decodeCodepage($string)
7904
    {
7905
        return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage);
7906
    }
7907
7908
    /**
7909
     * Read 16-bit unsigned integer.
7910
     *
7911
     * @param string $data
7912
     * @param int $pos
7913
     *
7914
     * @return int
7915
     */
7916 39
    public static function getUInt2d($data, $pos)
7917
    {
7918 39
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
7919
    }
7920
7921
    /**
7922
     * Read 16-bit signed integer.
7923
     *
7924
     * @param string $data
7925
     * @param int $pos
7926
     *
7927
     * @return int
7928
     */
7929
    public static function getInt2d($data, $pos)
7930
    {
7931
        return unpack('s', $data[$pos] . $data[$pos + 1])[1];
7932
    }
7933
7934
    /**
7935
     * Read 32-bit signed integer.
7936
     *
7937
     * @param string $data
7938
     * @param int $pos
7939
     *
7940
     * @return int
7941
     */
7942 39
    public static function getInt4d($data, $pos)
7943
    {
7944
        // FIX: represent numbers correctly on 64-bit system
7945
        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
7946
        // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
7947 39
        $_or_24 = ord($data[$pos + 3]);
7948 39
        if ($_or_24 >= 128) {
7949
            // negative number
7950 16
            $_ord_24 = -abs((256 - $_or_24) << 24);
7951
        } else {
7952 39
            $_ord_24 = ($_or_24 & 127) << 24;
7953
        }
7954
7955 39
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
7956
    }
7957
7958 2
    private function parseRichText($is)
7959
    {
7960 2
        $value = new RichText();
7961 2
        $value->createText($is);
7962
7963 2
        return $value;
7964
    }
7965
}
7966