Passed
Push — master ( 8fc7d9...eeb858 )
by Mark
14:21
created

Xls::readDataValidation()   C

Complexity

Conditions 14
Paths 162

Size

Total Lines 130
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 66
CRAP Score 14.1132

Importance

Changes 0
Metric Value
cc 14
eloc 73
c 0
b 0
f 0
nc 162
nop 0
dl 0
loc 130
ccs 66
cts 72
cp 0.9167
crap 14.1132
rs 5.189

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Reader\Xls\ConditionalFormatting;
11
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
12
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern;
13
use PhpOffice\PhpSpreadsheet\RichText\RichText;
14
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
15
use PhpOffice\PhpSpreadsheet\Shared\Date;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher;
17
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
18
use PhpOffice\PhpSpreadsheet\Shared\File;
19
use PhpOffice\PhpSpreadsheet\Shared\OLE;
20
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
21
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
22
use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls;
23
use PhpOffice\PhpSpreadsheet\Spreadsheet;
24
use PhpOffice\PhpSpreadsheet\Style\Alignment;
25
use PhpOffice\PhpSpreadsheet\Style\Borders;
26
use PhpOffice\PhpSpreadsheet\Style\Conditional;
27
use PhpOffice\PhpSpreadsheet\Style\Fill;
28
use PhpOffice\PhpSpreadsheet\Style\Font;
29
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
30
use PhpOffice\PhpSpreadsheet\Style\Protection;
31
use PhpOffice\PhpSpreadsheet\Style\Style;
32
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
33
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
34
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
35
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
36
37
// Original file header of ParseXL (used as the base for this class):
38
// --------------------------------------------------------------------------------
39
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
40
// trex005, and mmp11 (SourceForge.net)
41
// https://sourceforge.net/projects/phpexcelreader/
42
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
43
//     Modelled moreso after Perl Excel Parse/Write modules
44
//     Added Parse_Excel_Spreadsheet object
45
//         Reads a whole worksheet or tab as row,column array or as
46
//         associated hash of indexed rows and named column fields
47
//     Added variables for worksheet (tab) indexes and names
48
//     Added an object call for loading individual woorksheets
49
//     Changed default indexing defaults to 0 based arrays
50
//     Fixed date/time and percent formats
51
//     Includes patches found at SourceForge...
52
//         unicode patch by nobody
53
//         unpack("d") machine depedency patch by matchy
54
//         boundsheet utf16 patch by bjaenichen
55
//     Renamed functions for shorter names
56
//     General code cleanup and rigor, including <80 column width
57
//     Included a testcase Excel file and PHP example calls
58
//     Code works for PHP 5.x
59
60
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
61
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
62
//     Decoding of formula conditions, results, and tokens.
63
//     Support for user-defined named cells added as an array "namedcells"
64
//         Patch code for user-defined named cells supports single cells only.
65
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
66
//         external sheet reference structure
67
class Xls extends BaseReader
68
{
69
    // ParseXL definitions
70
    const XLS_BIFF8 = 0x0600;
71
    const XLS_BIFF7 = 0x0500;
72
    const XLS_WORKBOOKGLOBALS = 0x0005;
73
    const XLS_WORKSHEET = 0x0010;
74
75
    // record identifiers
76
    const XLS_TYPE_FORMULA = 0x0006;
77
    const XLS_TYPE_EOF = 0x000a;
78
    const XLS_TYPE_PROTECT = 0x0012;
79
    const XLS_TYPE_OBJECTPROTECT = 0x0063;
80
    const XLS_TYPE_SCENPROTECT = 0x00dd;
81
    const XLS_TYPE_PASSWORD = 0x0013;
82
    const XLS_TYPE_HEADER = 0x0014;
83
    const XLS_TYPE_FOOTER = 0x0015;
84
    const XLS_TYPE_EXTERNSHEET = 0x0017;
85
    const XLS_TYPE_DEFINEDNAME = 0x0018;
86
    const XLS_TYPE_VERTICALPAGEBREAKS = 0x001a;
87
    const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001b;
88
    const XLS_TYPE_NOTE = 0x001c;
89
    const XLS_TYPE_SELECTION = 0x001d;
90
    const XLS_TYPE_DATEMODE = 0x0022;
91
    const XLS_TYPE_EXTERNNAME = 0x0023;
92
    const XLS_TYPE_LEFTMARGIN = 0x0026;
93
    const XLS_TYPE_RIGHTMARGIN = 0x0027;
94
    const XLS_TYPE_TOPMARGIN = 0x0028;
95
    const XLS_TYPE_BOTTOMMARGIN = 0x0029;
96
    const XLS_TYPE_PRINTGRIDLINES = 0x002b;
97
    const XLS_TYPE_FILEPASS = 0x002f;
98
    const XLS_TYPE_FONT = 0x0031;
99
    const XLS_TYPE_CONTINUE = 0x003c;
100
    const XLS_TYPE_PANE = 0x0041;
101
    const XLS_TYPE_CODEPAGE = 0x0042;
102
    const XLS_TYPE_DEFCOLWIDTH = 0x0055;
103
    const XLS_TYPE_OBJ = 0x005d;
104
    const XLS_TYPE_COLINFO = 0x007d;
105
    const XLS_TYPE_IMDATA = 0x007f;
106
    const XLS_TYPE_SHEETPR = 0x0081;
107
    const XLS_TYPE_HCENTER = 0x0083;
108
    const XLS_TYPE_VCENTER = 0x0084;
109
    const XLS_TYPE_SHEET = 0x0085;
110
    const XLS_TYPE_PALETTE = 0x0092;
111
    const XLS_TYPE_SCL = 0x00a0;
112
    const XLS_TYPE_PAGESETUP = 0x00a1;
113
    const XLS_TYPE_MULRK = 0x00bd;
114
    const XLS_TYPE_MULBLANK = 0x00be;
115
    const XLS_TYPE_DBCELL = 0x00d7;
116
    const XLS_TYPE_XF = 0x00e0;
117
    const XLS_TYPE_MERGEDCELLS = 0x00e5;
118
    const XLS_TYPE_MSODRAWINGGROUP = 0x00eb;
119
    const XLS_TYPE_MSODRAWING = 0x00ec;
120
    const XLS_TYPE_SST = 0x00fc;
121
    const XLS_TYPE_LABELSST = 0x00fd;
122
    const XLS_TYPE_EXTSST = 0x00ff;
123
    const XLS_TYPE_EXTERNALBOOK = 0x01ae;
124
    const XLS_TYPE_DATAVALIDATIONS = 0x01b2;
125
    const XLS_TYPE_TXO = 0x01b6;
126
    const XLS_TYPE_HYPERLINK = 0x01b8;
127
    const XLS_TYPE_DATAVALIDATION = 0x01be;
128
    const XLS_TYPE_DIMENSION = 0x0200;
129
    const XLS_TYPE_BLANK = 0x0201;
130
    const XLS_TYPE_NUMBER = 0x0203;
131
    const XLS_TYPE_LABEL = 0x0204;
132
    const XLS_TYPE_BOOLERR = 0x0205;
133
    const XLS_TYPE_STRING = 0x0207;
134
    const XLS_TYPE_ROW = 0x0208;
135
    const XLS_TYPE_INDEX = 0x020b;
136
    const XLS_TYPE_ARRAY = 0x0221;
137
    const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225;
138
    const XLS_TYPE_WINDOW2 = 0x023e;
139
    const XLS_TYPE_RK = 0x027e;
140
    const XLS_TYPE_STYLE = 0x0293;
141
    const XLS_TYPE_FORMAT = 0x041e;
142
    const XLS_TYPE_SHAREDFMLA = 0x04bc;
143
    const XLS_TYPE_BOF = 0x0809;
144
    const XLS_TYPE_SHEETPROTECTION = 0x0867;
145
    const XLS_TYPE_RANGEPROTECTION = 0x0868;
146
    const XLS_TYPE_SHEETLAYOUT = 0x0862;
147
    const XLS_TYPE_XFEXT = 0x087d;
148
    const XLS_TYPE_PAGELAYOUTVIEW = 0x088b;
149
    const XLS_TYPE_CFHEADER = 0x01b0;
150
    const XLS_TYPE_CFRULE = 0x01b1;
151
    const XLS_TYPE_UNKNOWN = 0xffff;
152
153
    // Encryption type
154
    const MS_BIFF_CRYPTO_NONE = 0;
155
    const MS_BIFF_CRYPTO_XOR = 1;
156
    const MS_BIFF_CRYPTO_RC4 = 2;
157
158
    // Size of stream blocks when using RC4 encryption
159
    const REKEY_BLOCK = 0x400;
160
161
    /**
162
     * Summary Information stream data.
163
     *
164
     * @var string
165
     */
166
    private $summaryInformation;
167
168
    /**
169
     * Extended Summary Information stream data.
170
     *
171
     * @var string
172
     */
173
    private $documentSummaryInformation;
174
175
    /**
176
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
177
     *
178
     * @var string
179
     */
180
    private $data;
181
182
    /**
183
     * Size in bytes of $this->data.
184
     *
185
     * @var int
186
     */
187
    private $dataSize;
188
189
    /**
190
     * Current position in stream.
191
     *
192
     * @var int
193
     */
194
    private $pos;
195
196
    /**
197
     * Workbook to be returned by the reader.
198
     *
199
     * @var Spreadsheet
200
     */
201
    private $spreadsheet;
202
203
    /**
204
     * Worksheet that is currently being built by the reader.
205
     *
206
     * @var Worksheet
207
     */
208
    private $phpSheet;
209
210
    /**
211
     * BIFF version.
212
     *
213
     * @var int
214
     */
215
    private $version;
216
217
    /**
218
     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
219
     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'.
220
     *
221
     * @var string
222
     */
223
    private $codepage;
224
225
    /**
226
     * Shared formats.
227
     *
228
     * @var array
229
     */
230
    private $formats;
231
232
    /**
233
     * Shared fonts.
234
     *
235
     * @var Font[]
236
     */
237
    private $objFonts;
238
239
    /**
240
     * Color palette.
241
     *
242
     * @var array
243
     */
244
    private $palette;
245
246
    /**
247
     * Worksheets.
248
     *
249
     * @var array
250
     */
251
    private $sheets;
252
253
    /**
254
     * External books.
255
     *
256
     * @var array
257
     */
258
    private $externalBooks;
259
260
    /**
261
     * REF structures. Only applies to BIFF8.
262
     *
263
     * @var array
264
     */
265
    private $ref;
266
267
    /**
268
     * External names.
269
     *
270
     * @var array
271
     */
272
    private $externalNames;
273
274
    /**
275
     * Defined names.
276
     *
277
     * @var array
278
     */
279
    private $definedname;
280
281
    /**
282
     * Shared strings. Only applies to BIFF8.
283
     *
284
     * @var array
285
     */
286
    private $sst;
287
288
    /**
289
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
290
     *
291
     * @var bool
292
     */
293
    private $frozen;
294
295
    /**
296
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
297
     *
298
     * @var bool
299
     */
300
    private $isFitToPages;
301
302
    /**
303
     * Objects. One OBJ record contributes with one entry.
304
     *
305
     * @var array
306
     */
307
    private $objs;
308
309
    /**
310
     * Text Objects. One TXO record corresponds with one entry.
311
     *
312
     * @var array
313
     */
314
    private $textObjects;
315
316
    /**
317
     * Cell Annotations (BIFF8).
318
     *
319
     * @var array
320
     */
321
    private $cellNotes;
322
323
    /**
324
     * The combined MSODRAWINGGROUP data.
325
     *
326
     * @var string
327
     */
328
    private $drawingGroupData;
329
330
    /**
331
     * The combined MSODRAWING data (per sheet).
332
     *
333
     * @var string
334
     */
335
    private $drawingData;
336
337
    /**
338
     * Keep track of XF index.
339
     *
340
     * @var int
341
     */
342
    private $xfIndex;
343
344
    /**
345
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
346
     *
347
     * @var array
348
     */
349
    private $mapCellXfIndex;
350
351
    /**
352
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
353
     *
354
     * @var array
355
     */
356
    private $mapCellStyleXfIndex;
357
358
    /**
359
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
360
     *
361
     * @var array
362
     */
363
    private $sharedFormulas;
364
365
    /**
366
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
367
     * refers to a shared formula.
368
     *
369
     * @var array
370
     */
371
    private $sharedFormulaParts;
372
373
    /**
374
     * The type of encryption in use.
375
     *
376
     * @var int
377
     */
378
    private $encryption = 0;
379
380
    /**
381
     * The position in the stream after which contents are encrypted.
382
     *
383
     * @var int
384
     */
385
    private $encryptionStartPos = 0;
386
387
    /**
388
     * The current RC4 decryption object.
389
     *
390
     * @var Xls\RC4
391
     */
392
    private $rc4Key;
393
394
    /**
395
     * The position in the stream that the RC4 decryption object was left at.
396
     *
397
     * @var int
398
     */
399
    private $rc4Pos = 0;
400
401
    /**
402
     * The current MD5 context state.
403
     *
404
     * @var string
405
     */
406
    private $md5Ctxt;
407
408
    /**
409
     * @var int
410
     */
411
    private $textObjRef;
412
413
    /**
414
     * @var string
415
     */
416
    private $baseCell;
417
418
    /**
419
     * Create a new Xls Reader instance.
420
     */
421 94
    public function __construct()
422
    {
423 94
        parent::__construct();
424
    }
425
426
    /**
427
     * Can the current IReader read the file?
428
     */
429 15
    public function canRead(string $filename): bool
430
    {
431 15
        if (!File::testFileNoThrow($filename)) {
432 1
            return false;
433
        }
434
435
        try {
436
            // Use ParseXL for the hard work.
437 14
            $ole = new OLERead();
438
439
            // get excel data
440 14
            $ole->read($filename);
441
442 8
            return true;
443 6
        } catch (PhpSpreadsheetException $e) {
444 6
            return false;
445
        }
446
    }
447
448
    public function setCodepage(string $codepage): void
449
    {
450
        if (!CodePage::validate($codepage)) {
451
            throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage);
452
        }
453
454
        $this->codepage = $codepage;
455
    }
456
457
    /**
458
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
459
     *
460
     * @param string $filename
461
     *
462
     * @return array
463
     */
464 2
    public function listWorksheetNames($filename)
465
    {
466 2
        File::assertFile($filename);
467
468 2
        $worksheetNames = [];
469
470
        // Read the OLE file
471 2
        $this->loadOLE($filename);
472
473
        // total byte size of Excel data (workbook global substream + sheet substreams)
474 2
        $this->dataSize = strlen($this->data);
475
476 2
        $this->pos = 0;
477 2
        $this->sheets = [];
478
479
        // Parse Workbook Global Substream
480 2
        while ($this->pos < $this->dataSize) {
481 2
            $code = self::getUInt2d($this->data, $this->pos);
482
483
            switch ($code) {
484 2
                case self::XLS_TYPE_BOF:
485 2
                    $this->readBof();
486
487 2
                    break;
488 2
                case self::XLS_TYPE_SHEET:
489 2
                    $this->readSheet();
490
491 2
                    break;
492 2
                case self::XLS_TYPE_EOF:
493 2
                    $this->readDefault();
494
495 2
                    break 2;
496
                default:
497 2
                    $this->readDefault();
498
499 2
                    break;
500
            }
501
        }
502
503 2
        foreach ($this->sheets as $sheet) {
504 2
            if ($sheet['sheetType'] != 0x00) {
505
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
506
                continue;
507
            }
508
509 2
            $worksheetNames[] = $sheet['name'];
510
        }
511
512 2
        return $worksheetNames;
513
    }
514
515
    /**
516
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
517
     *
518
     * @param string $filename
519
     *
520
     * @return array
521
     */
522 1
    public function listWorksheetInfo($filename)
523
    {
524 1
        File::assertFile($filename);
525
526 1
        $worksheetInfo = [];
527
528
        // Read the OLE file
529 1
        $this->loadOLE($filename);
530
531
        // total byte size of Excel data (workbook global substream + sheet substreams)
532 1
        $this->dataSize = strlen($this->data);
533
534
        // initialize
535 1
        $this->pos = 0;
536 1
        $this->sheets = [];
537
538
        // Parse Workbook Global Substream
539 1
        while ($this->pos < $this->dataSize) {
540 1
            $code = self::getUInt2d($this->data, $this->pos);
541
542
            switch ($code) {
543 1
                case self::XLS_TYPE_BOF:
544 1
                    $this->readBof();
545
546 1
                    break;
547 1
                case self::XLS_TYPE_SHEET:
548 1
                    $this->readSheet();
549
550 1
                    break;
551 1
                case self::XLS_TYPE_EOF:
552 1
                    $this->readDefault();
553
554 1
                    break 2;
555
                default:
556 1
                    $this->readDefault();
557
558 1
                    break;
559
            }
560
        }
561
562
        // Parse the individual sheets
563 1
        foreach ($this->sheets as $sheet) {
564 1
            if ($sheet['sheetType'] != 0x00) {
565
                // 0x00: Worksheet
566
                // 0x02: Chart
567
                // 0x06: Visual Basic module
568
                continue;
569
            }
570
571 1
            $tmpInfo = [];
572 1
            $tmpInfo['worksheetName'] = $sheet['name'];
573 1
            $tmpInfo['lastColumnLetter'] = 'A';
574 1
            $tmpInfo['lastColumnIndex'] = 0;
575 1
            $tmpInfo['totalRows'] = 0;
576 1
            $tmpInfo['totalColumns'] = 0;
577
578 1
            $this->pos = $sheet['offset'];
579
580 1
            while ($this->pos <= $this->dataSize - 4) {
581 1
                $code = self::getUInt2d($this->data, $this->pos);
582
583
                switch ($code) {
584 1
                    case self::XLS_TYPE_RK:
585 1
                    case self::XLS_TYPE_LABELSST:
586 1
                    case self::XLS_TYPE_NUMBER:
587 1
                    case self::XLS_TYPE_FORMULA:
588 1
                    case self::XLS_TYPE_BOOLERR:
589 1
                    case self::XLS_TYPE_LABEL:
590 1
                        $length = self::getUInt2d($this->data, $this->pos + 2);
591 1
                        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
592
593
                        // move stream pointer to next record
594 1
                        $this->pos += 4 + $length;
595
596 1
                        $rowIndex = self::getUInt2d($recordData, 0) + 1;
597 1
                        $columnIndex = self::getUInt2d($recordData, 2);
598
599 1
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
600 1
                        $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
601
602 1
                        break;
603 1
                    case self::XLS_TYPE_BOF:
604 1
                        $this->readBof();
605
606 1
                        break;
607 1
                    case self::XLS_TYPE_EOF:
608 1
                        $this->readDefault();
609
610 1
                        break 2;
611
                    default:
612 1
                        $this->readDefault();
613
614 1
                        break;
615
                }
616
            }
617
618 1
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
619 1
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
620
621 1
            $worksheetInfo[] = $tmpInfo;
622
        }
623
624 1
        return $worksheetInfo;
625
    }
626
627
    /**
628
     * Loads PhpSpreadsheet from file.
629
     */
630 80
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
631
    {
632
        // Read the OLE file
633 80
        $this->loadOLE($filename);
634
635
        // Initialisations
636 80
        $this->spreadsheet = new Spreadsheet();
637 80
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
638 80
        if (!$this->readDataOnly) {
639 79
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
640 79
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
641
        }
642
643
        // Read the summary information stream (containing meta data)
644 80
        $this->readSummaryInformation();
645
646
        // Read the Additional document summary information stream (containing application-specific meta data)
647 80
        $this->readDocumentSummaryInformation();
648
649
        // total byte size of Excel data (workbook global substream + sheet substreams)
650 80
        $this->dataSize = strlen($this->data);
651
652
        // initialize
653 80
        $this->pos = 0;
654 80
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
655 80
        $this->formats = [];
656 80
        $this->objFonts = [];
657 80
        $this->palette = [];
658 80
        $this->sheets = [];
659 80
        $this->externalBooks = [];
660 80
        $this->ref = [];
661 80
        $this->definedname = [];
662 80
        $this->sst = [];
663 80
        $this->drawingGroupData = '';
664 80
        $this->xfIndex = 0;
665 80
        $this->mapCellXfIndex = [];
666 80
        $this->mapCellStyleXfIndex = [];
667
668
        // Parse Workbook Global Substream
669 80
        while ($this->pos < $this->dataSize) {
670 80
            $code = self::getUInt2d($this->data, $this->pos);
671
672
            switch ($code) {
673 80
                case self::XLS_TYPE_BOF:
674 80
                    $this->readBof();
675
676 80
                    break;
677 80
                case self::XLS_TYPE_FILEPASS:
678
                    $this->readFilepass();
679
680
                    break;
681 80
                case self::XLS_TYPE_CODEPAGE:
682 77
                    $this->readCodepage();
683
684 77
                    break;
685 80
                case self::XLS_TYPE_DATEMODE:
686 79
                    $this->readDateMode();
687
688 79
                    break;
689 80
                case self::XLS_TYPE_FONT:
690 80
                    $this->readFont();
691
692 80
                    break;
693 80
                case self::XLS_TYPE_FORMAT:
694 45
                    $this->readFormat();
695
696 45
                    break;
697 80
                case self::XLS_TYPE_XF:
698 80
                    $this->readXf();
699
700 80
                    break;
701 80
                case self::XLS_TYPE_XFEXT:
702 19
                    $this->readXfExt();
703
704 19
                    break;
705 80
                case self::XLS_TYPE_STYLE:
706 80
                    $this->readStyle();
707
708 80
                    break;
709 80
                case self::XLS_TYPE_PALETTE:
710 53
                    $this->readPalette();
711
712 53
                    break;
713 80
                case self::XLS_TYPE_SHEET:
714 80
                    $this->readSheet();
715
716 80
                    break;
717 80
                case self::XLS_TYPE_EXTERNALBOOK:
718 54
                    $this->readExternalBook();
719
720 54
                    break;
721 80
                case self::XLS_TYPE_EXTERNNAME:
722
                    $this->readExternName();
723
724
                    break;
725 80
                case self::XLS_TYPE_EXTERNSHEET:
726 55
                    $this->readExternSheet();
727
728 55
                    break;
729 80
                case self::XLS_TYPE_DEFINEDNAME:
730 13
                    $this->readDefinedName();
731
732 13
                    break;
733 80
                case self::XLS_TYPE_MSODRAWINGGROUP:
734 17
                    $this->readMsoDrawingGroup();
735
736 17
                    break;
737 80
                case self::XLS_TYPE_SST:
738 76
                    $this->readSst();
739
740 76
                    break;
741 80
                case self::XLS_TYPE_EOF:
742 80
                    $this->readDefault();
743
744 80
                    break 2;
745
                default:
746 80
                    $this->readDefault();
747
748 80
                    break;
749
            }
750
        }
751
752
        // Resolve indexed colors for font, fill, and border colors
753
        // Cannot be resolved already in XF record, because PALETTE record comes afterwards
754 80
        if (!$this->readDataOnly) {
755 79
            foreach ($this->objFonts as $objFont) {
756 79
                if (isset($objFont->colorIndex)) {
757 79
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
758 79
                    $objFont->getColor()->setRGB($color['rgb']);
759
                }
760
            }
761
762 79
            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
763
                // fill start and end color
764 79
                $fill = $objStyle->getFill();
765
766 79
                if (isset($fill->startcolorIndex)) {
767 79
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
768 79
                    $fill->getStartColor()->setRGB($startColor['rgb']);
769
                }
770 79
                if (isset($fill->endcolorIndex)) {
771 79
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
772 79
                    $fill->getEndColor()->setRGB($endColor['rgb']);
773
                }
774
775
                // border colors
776 79
                $top = $objStyle->getBorders()->getTop();
777 79
                $right = $objStyle->getBorders()->getRight();
778 79
                $bottom = $objStyle->getBorders()->getBottom();
779 79
                $left = $objStyle->getBorders()->getLeft();
780 79
                $diagonal = $objStyle->getBorders()->getDiagonal();
781
782 79
                if (isset($top->colorIndex)) {
783 79
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
784 79
                    $top->getColor()->setRGB($borderTopColor['rgb']);
785
                }
786 79
                if (isset($right->colorIndex)) {
787 79
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
788 79
                    $right->getColor()->setRGB($borderRightColor['rgb']);
789
                }
790 79
                if (isset($bottom->colorIndex)) {
791 79
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
792 79
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
793
                }
794 79
                if (isset($left->colorIndex)) {
795 79
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
796 79
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
797
                }
798 79
                if (isset($diagonal->colorIndex)) {
799 78
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
800 78
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
801
                }
802
            }
803
        }
804
805
        // treat MSODRAWINGGROUP records, workbook-level Escher
806 80
        $escherWorkbook = null;
807 80
        if (!$this->readDataOnly && $this->drawingGroupData) {
808 17
            $escher = new Escher();
809 17
            $reader = new Xls\Escher($escher);
810 17
            $escherWorkbook = $reader->load($this->drawingGroupData);
811
        }
812
813
        // Parse the individual sheets
814 80
        foreach ($this->sheets as $sheet) {
815 80
            if ($sheet['sheetType'] != 0x00) {
816
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
817
                continue;
818
            }
819
820
            // check if sheet should be skipped
821 80
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
822 5
                continue;
823
            }
824
825
            // add sheet to PhpSpreadsheet object
826 80
            $this->phpSheet = $this->spreadsheet->createSheet();
827
            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
828
            //        cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
829
            //        name in line with the formula, not the reverse
830 80
            $this->phpSheet->setTitle($sheet['name'], false, false);
831 80
            $this->phpSheet->setSheetState($sheet['sheetState']);
832
833 80
            $this->pos = $sheet['offset'];
834
835
            // Initialize isFitToPages. May change after reading SHEETPR record.
836 80
            $this->isFitToPages = false;
837
838
            // Initialize drawingData
839 80
            $this->drawingData = '';
840
841
            // Initialize objs
842 80
            $this->objs = [];
843
844
            // Initialize shared formula parts
845 80
            $this->sharedFormulaParts = [];
846
847
            // Initialize shared formulas
848 80
            $this->sharedFormulas = [];
849
850
            // Initialize text objs
851 80
            $this->textObjects = [];
852
853
            // Initialize cell annotations
854 80
            $this->cellNotes = [];
855 80
            $this->textObjRef = -1;
856
857 80
            while ($this->pos <= $this->dataSize - 4) {
858 80
                $code = self::getUInt2d($this->data, $this->pos);
859
860
                switch ($code) {
861 80
                    case self::XLS_TYPE_BOF:
862 80
                        $this->readBof();
863
864 80
                        break;
865 80
                    case self::XLS_TYPE_PRINTGRIDLINES:
866 77
                        $this->readPrintGridlines();
867
868 77
                        break;
869 80
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
870 45
                        $this->readDefaultRowHeight();
871
872 45
                        break;
873 80
                    case self::XLS_TYPE_SHEETPR:
874 79
                        $this->readSheetPr();
875
876 79
                        break;
877 80
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
878 4
                        $this->readHorizontalPageBreaks();
879
880 4
                        break;
881 80
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
882 4
                        $this->readVerticalPageBreaks();
883
884 4
                        break;
885 80
                    case self::XLS_TYPE_HEADER:
886 77
                        $this->readHeader();
887
888 77
                        break;
889 80
                    case self::XLS_TYPE_FOOTER:
890 77
                        $this->readFooter();
891
892 77
                        break;
893 80
                    case self::XLS_TYPE_HCENTER:
894 77
                        $this->readHcenter();
895
896 77
                        break;
897 80
                    case self::XLS_TYPE_VCENTER:
898 77
                        $this->readVcenter();
899
900 77
                        break;
901 80
                    case self::XLS_TYPE_LEFTMARGIN:
902 62
                        $this->readLeftMargin();
903
904 62
                        break;
905 80
                    case self::XLS_TYPE_RIGHTMARGIN:
906 62
                        $this->readRightMargin();
907
908 62
                        break;
909 80
                    case self::XLS_TYPE_TOPMARGIN:
910 62
                        $this->readTopMargin();
911
912 62
                        break;
913 80
                    case self::XLS_TYPE_BOTTOMMARGIN:
914 62
                        $this->readBottomMargin();
915
916 62
                        break;
917 80
                    case self::XLS_TYPE_PAGESETUP:
918 79
                        $this->readPageSetup();
919
920 79
                        break;
921 80
                    case self::XLS_TYPE_PROTECT:
922 6
                        $this->readProtect();
923
924 6
                        break;
925 80
                    case self::XLS_TYPE_SCENPROTECT:
926
                        $this->readScenProtect();
927
928
                        break;
929 80
                    case self::XLS_TYPE_OBJECTPROTECT:
930 1
                        $this->readObjectProtect();
931
932 1
                        break;
933 80
                    case self::XLS_TYPE_PASSWORD:
934 2
                        $this->readPassword();
935
936 2
                        break;
937 80
                    case self::XLS_TYPE_DEFCOLWIDTH:
938 78
                        $this->readDefColWidth();
939
940 78
                        break;
941 80
                    case self::XLS_TYPE_COLINFO:
942 72
                        $this->readColInfo();
943
944 72
                        break;
945 80
                    case self::XLS_TYPE_DIMENSION:
946 80
                        $this->readDefault();
947
948 80
                        break;
949 80
                    case self::XLS_TYPE_ROW:
950 52
                        $this->readRow();
951
952 52
                        break;
953 80
                    case self::XLS_TYPE_DBCELL:
954 35
                        $this->readDefault();
955
956 35
                        break;
957 80
                    case self::XLS_TYPE_RK:
958 26
                        $this->readRk();
959
960 26
                        break;
961 80
                    case self::XLS_TYPE_LABELSST:
962 53
                        $this->readLabelSst();
963
964 53
                        break;
965 80
                    case self::XLS_TYPE_MULRK:
966 17
                        $this->readMulRk();
967
968 17
                        break;
969 80
                    case self::XLS_TYPE_NUMBER:
970 40
                        $this->readNumber();
971
972 40
                        break;
973 80
                    case self::XLS_TYPE_FORMULA:
974 22
                        $this->readFormula();
975
976 22
                        break;
977 80
                    case self::XLS_TYPE_SHAREDFMLA:
978
                        $this->readSharedFmla();
979
980
                        break;
981 80
                    case self::XLS_TYPE_BOOLERR:
982 9
                        $this->readBoolErr();
983
984 9
                        break;
985 80
                    case self::XLS_TYPE_MULBLANK:
986 24
                        $this->readMulBlank();
987
988 24
                        break;
989 80
                    case self::XLS_TYPE_LABEL:
990 2
                        $this->readLabel();
991
992 2
                        break;
993 80
                    case self::XLS_TYPE_BLANK:
994 21
                        $this->readBlank();
995
996 21
                        break;
997 80
                    case self::XLS_TYPE_MSODRAWING:
998 16
                        $this->readMsoDrawing();
999
1000 16
                        break;
1001 80
                    case self::XLS_TYPE_OBJ:
1002 12
                        $this->readObj();
1003
1004 12
                        break;
1005 80
                    case self::XLS_TYPE_WINDOW2:
1006 80
                        $this->readWindow2();
1007
1008 80
                        break;
1009 80
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
1010 53
                        $this->readPageLayoutView();
1011
1012 53
                        break;
1013 80
                    case self::XLS_TYPE_SCL:
1014 5
                        $this->readScl();
1015
1016 5
                        break;
1017 80
                    case self::XLS_TYPE_PANE:
1018 8
                        $this->readPane();
1019
1020 8
                        break;
1021 80
                    case self::XLS_TYPE_SELECTION:
1022 77
                        $this->readSelection();
1023
1024 77
                        break;
1025 80
                    case self::XLS_TYPE_MERGEDCELLS:
1026 18
                        $this->readMergedCells();
1027
1028 18
                        break;
1029 80
                    case self::XLS_TYPE_HYPERLINK:
1030 4
                        $this->readHyperLink();
1031
1032 4
                        break;
1033 80
                    case self::XLS_TYPE_DATAVALIDATIONS:
1034 2
                        $this->readDataValidations();
1035
1036 2
                        break;
1037 80
                    case self::XLS_TYPE_DATAVALIDATION:
1038 2
                        $this->readDataValidation();
1039
1040 2
                        break;
1041 80
                    case self::XLS_TYPE_CFHEADER:
1042 15
                        $cellRangeAddresses = $this->readCFHeader();
1043
1044 15
                        break;
1045 80
                    case self::XLS_TYPE_CFRULE:
1046 15
                        $this->readCFRule($cellRangeAddresses ?? []);
1047
1048 15
                        break;
1049 80
                    case self::XLS_TYPE_SHEETLAYOUT:
1050 5
                        $this->readSheetLayout();
1051
1052 5
                        break;
1053 80
                    case self::XLS_TYPE_SHEETPROTECTION:
1054 63
                        $this->readSheetProtection();
1055
1056 63
                        break;
1057 80
                    case self::XLS_TYPE_RANGEPROTECTION:
1058 1
                        $this->readRangeProtection();
1059
1060 1
                        break;
1061 80
                    case self::XLS_TYPE_NOTE:
1062 3
                        $this->readNote();
1063
1064 3
                        break;
1065 80
                    case self::XLS_TYPE_TXO:
1066 2
                        $this->readTextObject();
1067
1068 2
                        break;
1069 80
                    case self::XLS_TYPE_CONTINUE:
1070 1
                        $this->readContinue();
1071
1072 1
                        break;
1073 80
                    case self::XLS_TYPE_EOF:
1074 80
                        $this->readDefault();
1075
1076 80
                        break 2;
1077
                    default:
1078 79
                        $this->readDefault();
1079
1080 79
                        break;
1081
                }
1082
            }
1083
1084
            // treat MSODRAWING records, sheet-level Escher
1085 80
            if (!$this->readDataOnly && $this->drawingData) {
1086 16
                $escherWorksheet = new Escher();
1087 16
                $reader = new Xls\Escher($escherWorksheet);
1088 16
                $escherWorksheet = $reader->load($this->drawingData);
1089
1090
                // get all spContainers in one long array, so they can be mapped to OBJ records
1091 16
                $allSpContainers = $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers();
1092
            }
1093
1094
            // treat OBJ records
1095 80
            foreach ($this->objs as $n => $obj) {
1096
                // the first shape container never has a corresponding OBJ record, hence $n + 1
1097 11
                if (isset($allSpContainers[$n + 1]) && is_object($allSpContainers[$n + 1])) {
1098 11
                    $spContainer = $allSpContainers[$n + 1];
1099
1100
                    // we skip all spContainers that are a part of a group shape since we cannot yet handle those
1101 11
                    if ($spContainer->getNestingLevel() > 1) {
1102
                        continue;
1103
                    }
1104
1105
                    // calculate the width and height of the shape
1106 11
                    [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
1107 11
                    [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());
1108
1109 11
                    $startOffsetX = $spContainer->getStartOffsetX();
1110 11
                    $startOffsetY = $spContainer->getStartOffsetY();
1111 11
                    $endOffsetX = $spContainer->getEndOffsetX();
1112 11
                    $endOffsetY = $spContainer->getEndOffsetY();
1113
1114 11
                    $width = SharedXls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX);
1115 11
                    $height = SharedXls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY);
1116
1117
                    // calculate offsetX and offsetY of the shape
1118 11
                    $offsetX = (int) ($startOffsetX * SharedXls::sizeCol($this->phpSheet, $startColumn) / 1024);
1119 11
                    $offsetY = (int) ($startOffsetY * SharedXls::sizeRow($this->phpSheet, $startRow) / 256);
1120
1121 11
                    switch ($obj['otObjType']) {
1122 11
                        case 0x19:
1123
                            // Note
1124 2
                            if (isset($this->cellNotes[$obj['idObjID']])) {
1125 2
                                $cellNote = $this->cellNotes[$obj['idObjID']];
1126
1127 2
                                if (isset($this->textObjects[$obj['idObjID']])) {
1128 2
                                    $textObject = $this->textObjects[$obj['idObjID']];
1129 2
                                    $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject;
1130
                                }
1131
                            }
1132
1133 2
                            break;
1134 11
                        case 0x08:
1135
                            // picture
1136
                            // get index to BSE entry (1-based)
1137 11
                            $BSEindex = $spContainer->getOPT(0x0104);
1138
1139
                            // If there is no BSE Index, we will fail here and other fields are not read.
1140
                            // Fix by checking here.
1141
                            // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
1142
                            // More likely : a uncompatible picture
1143 11
                            if (!$BSEindex) {
1144
                                continue 2;
1145
                            }
1146
1147 11
                            if ($escherWorkbook) {
1148 11
                                $BSECollection = $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection();
1149 11
                                $BSE = $BSECollection[$BSEindex - 1];
1150 11
                                $blipType = $BSE->getBlipType();
1151
1152
                                // need check because some blip types are not supported by Escher reader such as EMF
1153 11
                                if ($blip = $BSE->getBlip()) {
1154 11
                                    $ih = imagecreatefromstring($blip->getData());
1155 11
                                    if ($ih !== false) {
1156 11
                                        $drawing = new MemoryDrawing();
1157 11
                                        $drawing->setImageResource($ih);
1158
1159
                                        // width, height, offsetX, offsetY
1160 11
                                        $drawing->setResizeProportional(false);
1161 11
                                        $drawing->setWidth($width);
1162 11
                                        $drawing->setHeight($height);
1163 11
                                        $drawing->setOffsetX($offsetX);
1164 11
                                        $drawing->setOffsetY($offsetY);
1165
1166
                                        switch ($blipType) {
1167 10
                                            case BSE::BLIPTYPE_JPEG:
1168 9
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG);
1169 9
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG);
1170
1171 9
                                                break;
1172 10
                                            case BSE::BLIPTYPE_PNG:
1173 11
                                                imagealphablending($ih, false);
1174 11
                                                imagesavealpha($ih, true);
1175 11
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG);
1176 11
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG);
1177
1178 11
                                                break;
1179
                                        }
1180
1181 11
                                        $drawing->setWorksheet($this->phpSheet);
1182 11
                                        $drawing->setCoordinates($spContainer->getStartCoordinates());
1183
                                    }
1184
                                }
1185
                            }
1186
1187 11
                            break;
1188
                        default:
1189
                            // other object type
1190
                            break;
1191
                    }
1192
                }
1193
            }
1194
1195
            // treat SHAREDFMLA records
1196 80
            if ($this->version == self::XLS_BIFF8) {
1197 79
                foreach ($this->sharedFormulaParts as $cell => $baseCell) {
1198
                    [$column, $row] = Coordinate::coordinateFromString($cell);
1199
                    if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
1200
                        $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell);
1201
                        $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
1202
                    }
1203
                }
1204
            }
1205
1206 80
            if (!empty($this->cellNotes)) {
1207 2
                foreach ($this->cellNotes as $note => $noteDetails) {
1208 2
                    if (!isset($noteDetails['objTextData'])) {
1209
                        if (isset($this->textObjects[$note])) {
1210
                            $textObject = $this->textObjects[$note];
1211
                            $noteDetails['objTextData'] = $textObject;
1212
                        } else {
1213
                            $noteDetails['objTextData']['text'] = '';
1214
                        }
1215
                    }
1216 2
                    $cellAddress = str_replace('$', '', $noteDetails['cellRef']);
1217 2
                    $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text']));
1218
                }
1219
            }
1220
        }
1221
1222
        // add the named ranges (defined names)
1223 80
        foreach ($this->definedname as $definedName) {
1224 12
            if ($definedName['isBuiltInName']) {
1225 5
                switch ($definedName['name']) {
1226 5
                    case pack('C', 0x06):
1227
                        // print area
1228
                        //    in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2
1229 5
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1230
1231 5
                        $extractedRanges = [];
1232 5
                        foreach ($ranges as $range) {
1233
                            // $range should look like one of these
1234
                            //        Foo!$C$7:$J$66
1235
                            //        Bar!$A$1:$IV$2
1236 5
                            $explodes = Worksheet::extractSheetTitle($range, true);
1237 5
                            $sheetName = trim($explodes[0], "'");
1238 5
                            if (count($explodes) == 2) {
1239 5
                                if (strpos($explodes[1], ':') === false) {
1240
                                    $explodes[1] = $explodes[1] . ':' . $explodes[1];
1241
                                }
1242 5
                                $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66
1243
                            }
1244
                        }
1245 5
                        if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) {
1246 5
                            $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2
1247
                        }
1248
1249 5
                        break;
1250
                    case pack('C', 0x07):
1251
                        // print titles (repeating rows)
1252
                        // Assuming BIFF8, there are 3 cases
1253
                        // 1. repeating rows
1254
                        //        formula looks like this: Sheet!$A$1:$IV$2
1255
                        //        rows 1-2 repeat
1256
                        // 2. repeating columns
1257
                        //        formula looks like this: Sheet!$A$1:$B$65536
1258
                        //        columns A-B repeat
1259
                        // 3. both repeating rows and repeating columns
1260
                        //        formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2
1261
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1262
                        foreach ($ranges as $range) {
1263
                            // $range should look like this one of these
1264
                            //        Sheet!$A$1:$B$65536
1265
                            //        Sheet!$A$1:$IV$2
1266
                            if (strpos($range, '!') !== false) {
1267
                                $explodes = Worksheet::extractSheetTitle($range, true);
1268
                                if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) {
1269
                                    $extractedRange = $explodes[1];
1270
                                    $extractedRange = str_replace('$', '', $extractedRange);
1271
1272
                                    $coordinateStrings = explode(':', $extractedRange);
1273
                                    if (count($coordinateStrings) == 2) {
1274
                                        [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]);
1275
                                        [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]);
1276
1277
                                        if ($firstColumn == 'A' && $lastColumn == 'IV') {
1278
                                            // then we have repeating rows
1279
                                            $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
1280
                                        } elseif ($firstRow == 1 && $lastRow == 65536) {
1281
                                            // then we have repeating columns
1282
                                            $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
1283
                                        }
1284
                                    }
1285
                                }
1286
                            }
1287
                        }
1288
1289 5
                        break;
1290
                }
1291
            } else {
1292
                // Extract range
1293 7
                if (strpos($definedName['formula'], '!') !== false) {
1294 5
                    $explodes = Worksheet::extractSheetTitle($definedName['formula'], true);
1295
                    if (
1296 5
                        ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) ||
1297 5
                        ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'")))
1298
                    ) {
1299 5
                        $extractedRange = $explodes[1];
1300
1301 5
                        $localOnly = ($definedName['scope'] === 0) ? false : true;
1302
1303 5
                        $scope = ($definedName['scope'] === 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']);
1304
1305 5
                        $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope));
1306
                    }
1307
                }
1308
                //    Named Value
1309
                    //    TODO Provide support for named values
1310
            }
1311
        }
1312 80
        $this->data = '';
1313
1314 80
        return $this->spreadsheet;
1315
    }
1316
1317
    /**
1318
     * Read record data from stream, decrypting as required.
1319
     *
1320
     * @param string $data Data stream to read from
1321
     * @param int $pos Position to start reading from
1322
     * @param int $len Record data length
1323
     *
1324
     * @return string Record data
1325
     */
1326 83
    private function readRecordData($data, $pos, $len)
1327
    {
1328 83
        $data = substr($data, $pos, $len);
1329
1330
        // File not encrypted, or record before encryption start point
1331 83
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
1332 83
            return $data;
1333
        }
1334
1335
        $recordData = '';
1336
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
1337
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
1338
            $block = floor($pos / self::REKEY_BLOCK);
1339
            $endBlock = floor(($pos + $len) / self::REKEY_BLOCK);
1340
1341
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
1342
            // at a point earlier in the current block, re-use it as we can save some time.
1343
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
1344
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1345
                $step = $pos % self::REKEY_BLOCK;
1346
            } else {
1347
                $step = $pos - $this->rc4Pos;
1348
            }
1349
            $this->rc4Key->RC4(str_repeat("\0", $step));
1350
1351
            // Decrypt record data (re-keying at the end of every block)
1352
            while ($block != $endBlock) {
1353
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1354
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1355
                $data = substr($data, $step);
1356
                $pos += $step;
1357
                $len -= $step;
1358
                ++$block;
1359
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1360
            }
1361
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
1362
1363
            // Keep track of the position of this decryptor.
1364
            // We'll try and re-use it later if we can to speed things up
1365
            $this->rc4Pos = $pos + $len;
1366
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
1367
            throw new Exception('XOr encryption not supported');
1368
        }
1369
1370
        return $recordData;
1371
    }
1372
1373
    /**
1374
     * Use OLE reader to extract the relevant data streams from the OLE file.
1375
     *
1376
     * @param string $filename
1377
     */
1378 83
    private function loadOLE($filename): void
1379
    {
1380
        // OLE reader
1381 83
        $ole = new OLERead();
1382
        // get excel data,
1383 83
        $ole->read($filename);
1384
        // Get workbook data: workbook stream + sheet streams
1385 83
        $this->data = $ole->getStream($ole->wrkbook);
1386
        // Get summary information data
1387 83
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
1388
        // Get additional document summary information data
1389 83
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
1390
    }
1391
1392
    /**
1393
     * Read summary information.
1394
     */
1395 80
    private function readSummaryInformation(): void
1396
    {
1397 80
        if (!isset($this->summaryInformation)) {
1398 3
            return;
1399
        }
1400
1401
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
1402
        // offset: 2; size: 2;
1403
        // offset: 4; size: 2; OS version
1404
        // offset: 6; size: 2; OS indicator
1405
        // offset: 8; size: 16
1406
        // offset: 24; size: 4; section count
1407 77
        $secCount = self::getInt4d($this->summaryInformation, 24);
1408
1409
        // 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
1410
        // offset: 44; size: 4
1411 77
        $secOffset = self::getInt4d($this->summaryInformation, 44);
1412
1413
        // section header
1414
        // offset: $secOffset; size: 4; section length
1415 77
        $secLength = self::getInt4d($this->summaryInformation, $secOffset);
1416
1417
        // offset: $secOffset+4; size: 4; property count
1418 77
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
1419
1420
        // initialize code page (used to resolve string values)
1421 77
        $codePage = 'CP1252';
1422
1423
        // offset: ($secOffset+8); size: var
1424
        // loop through property decarations and properties
1425 77
        for ($i = 0; $i < $countProperties; ++$i) {
1426
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
1427 77
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
1428
1429
            // Use value of property id as appropriate
1430
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
1431 77
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
1432
1433 77
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
1434
1435
            // initialize property value
1436 77
            $value = null;
1437
1438
            // extract property value based on property type
1439
            switch ($type) {
1440 77
                case 0x02: // 2 byte signed integer
1441 77
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
1442
1443 77
                    break;
1444 77
                case 0x03: // 4 byte signed integer
1445 67
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1446
1447 67
                    break;
1448 77
                case 0x13: // 4 byte unsigned integer
1449
                    // not needed yet, fix later if necessary
1450 1
                    break;
1451 77
                case 0x1E: // null-terminated string prepended by dword string length
1452 75
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1453 75
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
1454 75
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1455 75
                    $value = rtrim($value);
1456
1457 75
                    break;
1458 77
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1459
                    // PHP-time
1460 77
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
1461
1462 77
                    break;
1463 2
                case 0x47: // Clipboard format
1464
                    // not needed yet, fix later if necessary
1465
                    break;
1466
            }
1467
1468
            switch ($id) {
1469 77
                case 0x01:    //    Code Page
1470 77
                    $codePage = CodePage::numberToName((int) $value);
1471
1472 77
                    break;
1473 77
                case 0x02:    //    Title
1474 43
                    $this->spreadsheet->getProperties()->setTitle("$value");
1475
1476 43
                    break;
1477 77
                case 0x03:    //    Subject
1478 13
                    $this->spreadsheet->getProperties()->setSubject("$value");
1479
1480 13
                    break;
1481 77
                case 0x04:    //    Author (Creator)
1482 70
                    $this->spreadsheet->getProperties()->setCreator("$value");
1483
1484 70
                    break;
1485 77
                case 0x05:    //    Keywords
1486 13
                    $this->spreadsheet->getProperties()->setKeywords("$value");
1487
1488 13
                    break;
1489 77
                case 0x06:    //    Comments (Description)
1490 13
                    $this->spreadsheet->getProperties()->setDescription("$value");
1491
1492 13
                    break;
1493 77
                case 0x07:    //    Template
1494
                    //    Not supported by PhpSpreadsheet
1495
                    break;
1496 77
                case 0x08:    //    Last Saved By (LastModifiedBy)
1497 69
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
1498
1499 69
                    break;
1500 77
                case 0x09:    //    Revision
1501
                    //    Not supported by PhpSpreadsheet
1502 9
                    break;
1503 77
                case 0x0A:    //    Total Editing Time
1504
                    //    Not supported by PhpSpreadsheet
1505 9
                    break;
1506 77
                case 0x0B:    //    Last Printed
1507
                    //    Not supported by PhpSpreadsheet
1508 13
                    break;
1509 77
                case 0x0C:    //    Created Date/Time
1510 77
                    $this->spreadsheet->getProperties()->setCreated($value);
1511
1512 77
                    break;
1513 77
                case 0x0D:    //    Modified Date/Time
1514 77
                    $this->spreadsheet->getProperties()->setModified($value);
1515
1516 77
                    break;
1517 68
                case 0x0E:    //    Number of Pages
1518
                    //    Not supported by PhpSpreadsheet
1519
                    break;
1520 68
                case 0x0F:    //    Number of Words
1521
                    //    Not supported by PhpSpreadsheet
1522
                    break;
1523 68
                case 0x10:    //    Number of Characters
1524
                    //    Not supported by PhpSpreadsheet
1525
                    break;
1526 68
                case 0x11:    //    Thumbnail
1527
                    //    Not supported by PhpSpreadsheet
1528
                    break;
1529 68
                case 0x12:    //    Name of creating application
1530
                    //    Not supported by PhpSpreadsheet
1531 26
                    break;
1532 68
                case 0x13:    //    Security
1533
                    //    Not supported by PhpSpreadsheet
1534 67
                    break;
1535
            }
1536
        }
1537
    }
1538
1539
    /**
1540
     * Read additional document summary information.
1541
     */
1542 80
    private function readDocumentSummaryInformation(): void
1543
    {
1544 80
        if (!isset($this->documentSummaryInformation)) {
1545 3
            return;
1546
        }
1547
1548
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
1549
        //    offset: 2;    size: 2;
1550
        //    offset: 4;    size: 2;    OS version
1551
        //    offset: 6;    size: 2;    OS indicator
1552
        //    offset: 8;    size: 16
1553
        //    offset: 24;    size: 4;    section count
1554 77
        $secCount = self::getInt4d($this->documentSummaryInformation, 24);
1555
1556
        // 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
1557
        // offset: 44;    size: 4;    first section offset
1558 77
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
1559
1560
        //    section header
1561
        //    offset: $secOffset;    size: 4;    section length
1562 77
        $secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
1563
1564
        //    offset: $secOffset+4;    size: 4;    property count
1565 77
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
1566
1567
        // initialize code page (used to resolve string values)
1568 77
        $codePage = 'CP1252';
1569
1570
        //    offset: ($secOffset+8);    size: var
1571
        //    loop through property decarations and properties
1572 77
        for ($i = 0; $i < $countProperties; ++$i) {
1573
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
1574 77
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
1575
1576
            // Use value of property id as appropriate
1577
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
1578 77
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
1579
1580 77
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
1581
1582
            // initialize property value
1583 77
            $value = null;
1584
1585
            // extract property value based on property type
1586
            switch ($type) {
1587 77
                case 0x02:    //    2 byte signed integer
1588 77
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1589
1590 77
                    break;
1591 68
                case 0x03:    //    4 byte signed integer
1592 66
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1593
1594 66
                    break;
1595 68
                case 0x0B:  // Boolean
1596 68
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1597 68
                    $value = ($value == 0 ? false : true);
1598
1599 68
                    break;
1600 67
                case 0x13:    //    4 byte unsigned integer
1601
                    // not needed yet, fix later if necessary
1602 1
                    break;
1603 66
                case 0x1E:    //    null-terminated string prepended by dword string length
1604 30
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1605 30
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
1606 30
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1607 30
                    $value = rtrim($value);
1608
1609 30
                    break;
1610 66
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1611
                    // PHP-Time
1612
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
1613
1614
                    break;
1615 66
                case 0x47:    //    Clipboard format
1616
                    // not needed yet, fix later if necessary
1617
                    break;
1618
            }
1619
1620
            switch ($id) {
1621 77
                case 0x01:    //    Code Page
1622 77
                    $codePage = CodePage::numberToName((int) $value);
1623
1624 77
                    break;
1625 68
                case 0x02:    //    Category
1626 13
                    $this->spreadsheet->getProperties()->setCategory("$value");
1627
1628 13
                    break;
1629 68
                case 0x03:    //    Presentation Target
1630
                    //    Not supported by PhpSpreadsheet
1631
                    break;
1632 68
                case 0x04:    //    Bytes
1633
                    //    Not supported by PhpSpreadsheet
1634
                    break;
1635 68
                case 0x05:    //    Lines
1636
                    //    Not supported by PhpSpreadsheet
1637
                    break;
1638 68
                case 0x06:    //    Paragraphs
1639
                    //    Not supported by PhpSpreadsheet
1640
                    break;
1641 68
                case 0x07:    //    Slides
1642
                    //    Not supported by PhpSpreadsheet
1643
                    break;
1644 68
                case 0x08:    //    Notes
1645
                    //    Not supported by PhpSpreadsheet
1646
                    break;
1647 68
                case 0x09:    //    Hidden Slides
1648
                    //    Not supported by PhpSpreadsheet
1649
                    break;
1650 68
                case 0x0A:    //    MM Clips
1651
                    //    Not supported by PhpSpreadsheet
1652
                    break;
1653 68
                case 0x0B:    //    Scale Crop
1654
                    //    Not supported by PhpSpreadsheet
1655 68
                    break;
1656 68
                case 0x0C:    //    Heading Pairs
1657
                    //    Not supported by PhpSpreadsheet
1658 66
                    break;
1659 68
                case 0x0D:    //    Titles of Parts
1660
                    //    Not supported by PhpSpreadsheet
1661 66
                    break;
1662 68
                case 0x0E:    //    Manager
1663 2
                    $this->spreadsheet->getProperties()->setManager("$value");
1664
1665 2
                    break;
1666 68
                case 0x0F:    //    Company
1667 22
                    $this->spreadsheet->getProperties()->setCompany("$value");
1668
1669 22
                    break;
1670 68
                case 0x10:    //    Links up-to-date
1671
                    //    Not supported by PhpSpreadsheet
1672 68
                    break;
1673
            }
1674
        }
1675
    }
1676
1677
    /**
1678
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
1679
     */
1680 83
    private function readDefault(): void
1681
    {
1682 83
        $length = self::getUInt2d($this->data, $this->pos + 2);
1683
1684
        // move stream pointer to next record
1685 83
        $this->pos += 4 + $length;
1686
    }
1687
1688
    /**
1689
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
1690
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
1691
     */
1692 3
    private function readNote(): void
1693
    {
1694 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
1695 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1696
1697
        // move stream pointer to next record
1698 3
        $this->pos += 4 + $length;
1699
1700 3
        if ($this->readDataOnly) {
1701
            return;
1702
        }
1703
1704 3
        $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4));
1705 3
        if ($this->version == self::XLS_BIFF8) {
1706 2
            $noteObjID = self::getUInt2d($recordData, 6);
1707 2
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
1708 2
            $noteAuthor = $noteAuthor['value'];
1709 2
            $this->cellNotes[$noteObjID] = [
1710 2
                'cellRef' => $cellAddress,
1711 2
                'objectID' => $noteObjID,
1712 2
                'author' => $noteAuthor,
1713 2
            ];
1714
        } else {
1715 1
            $extension = false;
1716 1
            if ($cellAddress == '$B$65536') {
1717
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
1718
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
1719
                //        max 2048 bytes will probably throw a wobbly.
1720
                $row = self::getUInt2d($recordData, 0);
1721
                $extension = true;
1722
                $arrayKeys = array_keys($this->phpSheet->getComments());
1723
                $cellAddress = array_pop($arrayKeys);
1724
            }
1725
1726 1
            $cellAddress = str_replace('$', '', $cellAddress);
1727 1
            $noteLength = self::getUInt2d($recordData, 4);
1728 1
            $noteText = trim(substr($recordData, 6));
1729
1730 1
            if ($extension) {
1731
                //    Concatenate this extension with the currently set comment for the cell
1732
                $comment = $this->phpSheet->getComment($cellAddress);
1733
                $commentText = $comment->getText()->getPlainText();
1734
                $comment->setText($this->parseRichText($commentText . $noteText));
1735
            } else {
1736
                //    Set comment for the cell
1737 1
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1738
//                                                    ->setAuthor($author)
1739
            }
1740
        }
1741
    }
1742
1743
    /**
1744
     * The TEXT Object record contains the text associated with a cell annotation.
1745
     */
1746 2
    private function readTextObject(): void
1747
    {
1748 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
1749 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1750
1751
        // move stream pointer to next record
1752 2
        $this->pos += 4 + $length;
1753
1754 2
        if ($this->readDataOnly) {
1755
            return;
1756
        }
1757
1758
        // recordData consists of an array of subrecords looking like this:
1759
        //    grbit: 2 bytes; Option Flags
1760
        //    rot: 2 bytes; rotation
1761
        //    cchText: 2 bytes; length of the text (in the first continue record)
1762
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
1763
        // followed by the continuation records containing the actual text and formatting
1764 2
        $grbitOpts = self::getUInt2d($recordData, 0);
1765 2
        $rot = self::getUInt2d($recordData, 2);
1766 2
        $cchText = self::getUInt2d($recordData, 10);
1767 2
        $cbRuns = self::getUInt2d($recordData, 12);
1768 2
        $text = $this->getSplicedRecordData();
1769
1770 2
        $textByte = $text['spliceOffsets'][1] - $text['spliceOffsets'][0] - 1;
1771 2
        $textStr = substr($text['recordData'], $text['spliceOffsets'][0] + 1, $textByte);
1772
        // get 1 byte
1773 2
        $is16Bit = ord($text['recordData'][0]);
1774
        // it is possible to use a compressed format,
1775
        // which omits the high bytes of all characters, if they are all zero
1776 2
        if (($is16Bit & 0x01) === 0) {
1777 2
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
1778
        } else {
1779
            $textStr = $this->decodeCodepage($textStr);
1780
        }
1781
1782 2
        $this->textObjects[$this->textObjRef] = [
1783 2
            'text' => $textStr,
1784 2
            'format' => substr($text['recordData'], $text['spliceOffsets'][1], $cbRuns),
1785 2
            'alignment' => $grbitOpts,
1786 2
            'rotation' => $rot,
1787 2
        ];
1788
    }
1789
1790
    /**
1791
     * Read BOF.
1792
     */
1793 83
    private function readBof(): void
1794
    {
1795 83
        $length = self::getUInt2d($this->data, $this->pos + 2);
1796 83
        $recordData = substr($this->data, $this->pos + 4, $length);
1797
1798
        // move stream pointer to next record
1799 83
        $this->pos += 4 + $length;
1800
1801
        // offset: 2; size: 2; type of the following data
1802 83
        $substreamType = self::getUInt2d($recordData, 2);
1803
1804
        switch ($substreamType) {
1805 83
            case self::XLS_WORKBOOKGLOBALS:
1806 83
                $version = self::getUInt2d($recordData, 0);
1807 83
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
1808
                    throw new Exception('Cannot read this Excel file. Version is too old.');
1809
                }
1810 83
                $this->version = $version;
1811
1812 83
                break;
1813 81
            case self::XLS_WORKSHEET:
1814
                // do not use this version information for anything
1815
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
1816 81
                break;
1817
            default:
1818
                // substream, e.g. chart
1819
                // just skip the entire substream
1820
                do {
1821
                    $code = self::getUInt2d($this->data, $this->pos);
1822
                    $this->readDefault();
1823
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
1824
1825
                break;
1826
        }
1827
    }
1828
1829
    /**
1830
     * FILEPASS.
1831
     *
1832
     * This record is part of the File Protection Block. It
1833
     * contains information about the read/write password of the
1834
     * file. All record contents following this record will be
1835
     * encrypted.
1836
     *
1837
     * --    "OpenOffice.org's Documentation of the Microsoft
1838
     *         Excel File Format"
1839
     *
1840
     * The decryption functions and objects used from here on in
1841
     * are based on the source of Spreadsheet-ParseExcel:
1842
     * https://metacpan.org/release/Spreadsheet-ParseExcel
1843
     */
1844
    private function readFilepass(): void
1845
    {
1846
        $length = self::getUInt2d($this->data, $this->pos + 2);
1847
1848
        if ($length != 54) {
1849
            throw new Exception('Unexpected file pass record length');
1850
        }
1851
1852
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1853
1854
        // move stream pointer to next record
1855
        $this->pos += 4 + $length;
1856
1857
        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
1858
            throw new Exception('Decryption password incorrect');
1859
        }
1860
1861
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
1862
1863
        // Decryption required from the record after next onwards
1864
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
1865
    }
1866
1867
    /**
1868
     * Make an RC4 decryptor for the given block.
1869
     *
1870
     * @param int $block Block for which to create decrypto
1871
     * @param string $valContext MD5 context state
1872
     *
1873
     * @return Xls\RC4
1874
     */
1875
    private function makeKey($block, $valContext)
1876
    {
1877
        $pwarray = str_repeat("\0", 64);
1878
1879
        for ($i = 0; $i < 5; ++$i) {
1880
            $pwarray[$i] = $valContext[$i];
1881
        }
1882
1883
        $pwarray[5] = chr($block & 0xff);
1884
        $pwarray[6] = chr(($block >> 8) & 0xff);
1885
        $pwarray[7] = chr(($block >> 16) & 0xff);
1886
        $pwarray[8] = chr(($block >> 24) & 0xff);
1887
1888
        $pwarray[9] = "\x80";
1889
        $pwarray[56] = "\x48";
1890
1891
        $md5 = new Xls\MD5();
1892
        $md5->add($pwarray);
1893
1894
        $s = $md5->getContext();
1895
1896
        return new Xls\RC4($s);
1897
    }
1898
1899
    /**
1900
     * Verify RC4 file password.
1901
     *
1902
     * @param string $password Password to check
1903
     * @param string $docid Document id
1904
     * @param string $salt_data Salt data
1905
     * @param string $hashedsalt_data Hashed salt data
1906
     * @param string $valContext Set to the MD5 context of the value
1907
     *
1908
     * @return bool Success
1909
     */
1910
    private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext)
1911
    {
1912
        $pwarray = str_repeat("\0", 64);
1913
1914
        $iMax = strlen($password);
1915
        for ($i = 0; $i < $iMax; ++$i) {
1916
            $o = ord(substr($password, $i, 1));
1917
            $pwarray[2 * $i] = chr($o & 0xff);
1918
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xff);
1919
        }
1920
        $pwarray[2 * $i] = chr(0x80);
1921
        $pwarray[56] = chr(($i << 4) & 0xff);
1922
1923
        $md5 = new Xls\MD5();
1924
        $md5->add($pwarray);
1925
1926
        $mdContext1 = $md5->getContext();
1927
1928
        $offset = 0;
1929
        $keyoffset = 0;
1930
        $tocopy = 5;
1931
1932
        $md5->reset();
1933
1934
        while ($offset != 16) {
1935
            if ((64 - $offset) < 5) {
1936
                $tocopy = 64 - $offset;
1937
            }
1938
            for ($i = 0; $i <= $tocopy; ++$i) {
1939
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
1940
            }
1941
            $offset += $tocopy;
1942
1943
            if ($offset == 64) {
1944
                $md5->add($pwarray);
1945
                $keyoffset = $tocopy;
1946
                $tocopy = 5 - $tocopy;
1947
                $offset = 0;
1948
1949
                continue;
1950
            }
1951
1952
            $keyoffset = 0;
1953
            $tocopy = 5;
1954
            for ($i = 0; $i < 16; ++$i) {
1955
                $pwarray[$offset + $i] = $docid[$i];
1956
            }
1957
            $offset += 16;
1958
        }
1959
1960
        $pwarray[16] = "\x80";
1961
        for ($i = 0; $i < 47; ++$i) {
1962
            $pwarray[17 + $i] = "\0";
1963
        }
1964
        $pwarray[56] = "\x80";
1965
        $pwarray[57] = "\x0a";
1966
1967
        $md5->add($pwarray);
1968
        $valContext = $md5->getContext();
1969
1970
        $key = $this->makeKey(0, $valContext);
1971
1972
        $salt = $key->RC4($salt_data);
1973
        $hashedsalt = $key->RC4($hashedsalt_data);
1974
1975
        $salt .= "\x80" . str_repeat("\0", 47);
1976
        $salt[56] = "\x80";
1977
1978
        $md5->reset();
1979
        $md5->add($salt);
1980
        $mdContext2 = $md5->getContext();
1981
1982
        return $mdContext2 == $hashedsalt;
1983
    }
1984
1985
    /**
1986
     * CODEPAGE.
1987
     *
1988
     * This record stores the text encoding used to write byte
1989
     * strings, stored as MS Windows code page identifier.
1990
     *
1991
     * --    "OpenOffice.org's Documentation of the Microsoft
1992
     *         Excel File Format"
1993
     */
1994 77
    private function readCodepage(): void
1995
    {
1996 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
1997 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1998
1999
        // move stream pointer to next record
2000 77
        $this->pos += 4 + $length;
2001
2002
        // offset: 0; size: 2; code page identifier
2003 77
        $codepage = self::getUInt2d($recordData, 0);
2004
2005 77
        $this->codepage = CodePage::numberToName($codepage);
2006
    }
2007
2008
    /**
2009
     * DATEMODE.
2010
     *
2011
     * This record specifies the base date for displaying date
2012
     * values. All dates are stored as count of days past this
2013
     * base date. In BIFF2-BIFF4 this record is part of the
2014
     * Calculation Settings Block. In BIFF5-BIFF8 it is
2015
     * stored in the Workbook Globals Substream.
2016
     *
2017
     * --    "OpenOffice.org's Documentation of the Microsoft
2018
     *         Excel File Format"
2019
     */
2020 79
    private function readDateMode(): void
2021
    {
2022 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
2023 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2024
2025
        // move stream pointer to next record
2026 79
        $this->pos += 4 + $length;
2027
2028
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
2029 79
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
2030 79
        if (ord($recordData[0]) == 1) {
2031
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
2032
        }
2033
    }
2034
2035
    /**
2036
     * Read a FONT record.
2037
     */
2038 80
    private function readFont(): void
2039
    {
2040 80
        $length = self::getUInt2d($this->data, $this->pos + 2);
2041 80
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2042
2043
        // move stream pointer to next record
2044 80
        $this->pos += 4 + $length;
2045
2046 80
        if (!$this->readDataOnly) {
2047 79
            $objFont = new Font();
2048
2049
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
2050 79
            $size = self::getUInt2d($recordData, 0);
2051 79
            $objFont->setSize($size / 20);
2052
2053
            // offset: 2; size: 2; option flags
2054
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
2055
            // bit: 1; mask 0x0002; italic
2056 79
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
2057 79
            if ($isItalic) {
2058 27
                $objFont->setItalic(true);
2059
            }
2060
2061
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
2062
            // bit: 3; mask 0x0008; strikethrough
2063 79
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
2064 79
            if ($isStrike) {
2065
                $objFont->setStrikethrough(true);
2066
            }
2067
2068
            // offset: 4; size: 2; colour index
2069 79
            $colorIndex = self::getUInt2d($recordData, 4);
2070 79
            $objFont->colorIndex = $colorIndex;
2071
2072
            // offset: 6; size: 2; font weight
2073 79
            $weight = self::getUInt2d($recordData, 6);
2074
            switch ($weight) {
2075 79
                case 0x02BC:
2076 46
                    $objFont->setBold(true);
2077
2078 46
                    break;
2079
            }
2080
2081
            // offset: 8; size: 2; escapement type
2082 79
            $escapement = self::getUInt2d($recordData, 8);
2083 79
            CellFont::escapement($objFont, $escapement);
2084
2085
            // offset: 10; size: 1; underline type
2086 79
            $underlineType = ord($recordData[10]);
2087 79
            CellFont::underline($objFont, $underlineType);
2088
2089
            // offset: 11; size: 1; font family
2090
            // offset: 12; size: 1; character set
2091
            // offset: 13; size: 1; not used
2092
            // offset: 14; size: var; font name
2093 79
            if ($this->version == self::XLS_BIFF8) {
2094 78
                $string = self::readUnicodeStringShort(substr($recordData, 14));
2095
            } else {
2096 1
                $string = $this->readByteStringShort(substr($recordData, 14));
2097
            }
2098 79
            $objFont->setName($string['value']);
2099
2100 79
            $this->objFonts[] = $objFont;
2101
        }
2102
    }
2103
2104
    /**
2105
     * FORMAT.
2106
     *
2107
     * This record contains information about a number format.
2108
     * All FORMAT records occur together in a sequential list.
2109
     *
2110
     * In BIFF2-BIFF4 other records referencing a FORMAT record
2111
     * contain a zero-based index into this list. From BIFF5 on
2112
     * the FORMAT record contains the index itself that will be
2113
     * used by other records.
2114
     *
2115
     * --    "OpenOffice.org's Documentation of the Microsoft
2116
     *         Excel File Format"
2117
     */
2118 45
    private function readFormat(): void
2119
    {
2120 45
        $length = self::getUInt2d($this->data, $this->pos + 2);
2121 45
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2122
2123
        // move stream pointer to next record
2124 45
        $this->pos += 4 + $length;
2125
2126 45
        if (!$this->readDataOnly) {
2127 44
            $indexCode = self::getUInt2d($recordData, 0);
2128
2129 44
            if ($this->version == self::XLS_BIFF8) {
2130 43
                $string = self::readUnicodeStringLong(substr($recordData, 2));
2131
            } else {
2132
                // BIFF7
2133 1
                $string = $this->readByteStringShort(substr($recordData, 2));
2134
            }
2135
2136 44
            $formatString = $string['value'];
2137
            // Apache Open Office sets wrong case writing to xls - issue 2239
2138 44
            if ($formatString === 'GENERAL') {
2139 1
                $formatString = NumberFormat::FORMAT_GENERAL;
2140
            }
2141 44
            $this->formats[$indexCode] = $formatString;
2142
        }
2143
    }
2144
2145
    /**
2146
     * XF - Extended Format.
2147
     *
2148
     * This record contains formatting information for cells, rows, columns or styles.
2149
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
2150
     * and 1 cell XF.
2151
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
2152
     * and XF record 15 is a cell XF
2153
     * We only read the first cell style XF and skip the remaining cell style XF records
2154
     * We read all cell XF records.
2155
     *
2156
     * --    "OpenOffice.org's Documentation of the Microsoft
2157
     *         Excel File Format"
2158
     */
2159 80
    private function readXf(): void
2160
    {
2161 80
        $length = self::getUInt2d($this->data, $this->pos + 2);
2162 80
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2163
2164
        // move stream pointer to next record
2165 80
        $this->pos += 4 + $length;
2166
2167 80
        $objStyle = new Style();
2168
2169 80
        if (!$this->readDataOnly) {
2170
            // offset:  0; size: 2; Index to FONT record
2171 79
            if (self::getUInt2d($recordData, 0) < 4) {
2172 79
                $fontIndex = self::getUInt2d($recordData, 0);
2173
            } else {
2174
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
2175
                // check the OpenOffice documentation of the FONT record
2176 43
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
2177
            }
2178 79
            $objStyle->setFont($this->objFonts[$fontIndex]);
2179
2180
            // offset:  2; size: 2; Index to FORMAT record
2181 79
            $numberFormatIndex = self::getUInt2d($recordData, 2);
2182 79
            if (isset($this->formats[$numberFormatIndex])) {
2183
                // then we have user-defined format code
2184 39
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
2185 79
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
2186
                // then we have built-in format code
2187 79
                $numberFormat = ['formatCode' => $code];
2188
            } else {
2189
                // we set the general format code
2190 10
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
2191
            }
2192 79
            $objStyle->getNumberFormat()->setFormatCode($numberFormat['formatCode']);
2193
2194
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
2195
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
2196 79
            $xfTypeProt = self::getUInt2d($recordData, 4);
2197
            // bit 0; mask 0x01; 1 = cell is locked
2198 79
            $isLocked = (0x01 & $xfTypeProt) >> 0;
2199 79
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
2200
2201
            // bit 1; mask 0x02; 1 = Formula is hidden
2202 79
            $isHidden = (0x02 & $xfTypeProt) >> 1;
2203 79
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
2204
2205
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
2206 79
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
2207
2208
            // offset:  6; size: 1; Alignment and text break
2209
            // bit 2-0, mask 0x07; horizontal alignment
2210 79
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
2211 79
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
2212
2213
            // bit 3, mask 0x08; wrap text
2214 79
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
2215 79
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
2216
2217
            // bit 6-4, mask 0x70; vertical alignment
2218 79
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
2219 79
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
2220
2221 79
            if ($this->version == self::XLS_BIFF8) {
2222
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
2223 78
                $angle = ord($recordData[7]);
2224 78
                $rotation = 0;
2225 78
                if ($angle <= 90) {
2226 78
                    $rotation = $angle;
2227 2
                } elseif ($angle <= 180) {
2228
                    $rotation = 90 - $angle;
2229 2
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
2230 2
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
2231
                }
2232 78
                $objStyle->getAlignment()->setTextRotation($rotation);
2233
2234
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
2235
                // bit: 3-0; mask: 0x0F; indent level
2236 78
                $indent = (0x0F & ord($recordData[8])) >> 0;
2237 78
                $objStyle->getAlignment()->setIndent($indent);
2238
2239
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
2240 78
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
2241
                switch ($shrinkToFit) {
2242 78
                    case 0:
2243 78
                        $objStyle->getAlignment()->setShrinkToFit(false);
2244
2245 78
                        break;
2246 1
                    case 1:
2247 1
                        $objStyle->getAlignment()->setShrinkToFit(true);
2248
2249 1
                        break;
2250
                }
2251
2252
                // offset:  9; size: 1; Flags used for attribute groups
2253
2254
                // offset: 10; size: 4; Cell border lines and background area
2255
                // bit: 3-0; mask: 0x0000000F; left style
2256 78
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
2257 78
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
2258
                }
2259
                // bit: 7-4; mask: 0x000000F0; right style
2260 78
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
2261 78
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
2262
                }
2263
                // bit: 11-8; mask: 0x00000F00; top style
2264 78
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
2265 78
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
2266
                }
2267
                // bit: 15-12; mask: 0x0000F000; bottom style
2268 78
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
2269 78
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
2270
                }
2271
                // bit: 22-16; mask: 0x007F0000; left color
2272 78
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
2273
2274
                // bit: 29-23; mask: 0x3F800000; right color
2275 78
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
2276
2277
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
2278 78
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
2279
2280
                // bit: 31; mask: 0x80000000; 1 = diagonal line from bottom left to top right
2281 78
                $diagonalUp = ((int) 0x80000000 & self::getInt4d($recordData, 10)) >> 31 ? true : false;
2282
2283 78
                if ($diagonalUp === false) {
2284 78
                    if ($diagonalDown == false) {
2285 78
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
2286
                    } else {
2287 78
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
2288
                    }
2289 1
                } elseif ($diagonalDown == false) {
2290 1
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
2291
                } else {
2292 1
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
2293
                }
2294
2295
                // offset: 14; size: 4;
2296
                // bit: 6-0; mask: 0x0000007F; top color
2297 78
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
2298
2299
                // bit: 13-7; mask: 0x00003F80; bottom color
2300 78
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
2301
2302
                // bit: 20-14; mask: 0x001FC000; diagonal color
2303 78
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
2304
2305
                // bit: 24-21; mask: 0x01E00000; diagonal style
2306 78
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
2307 78
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
2308
                }
2309
2310
                // bit: 31-26; mask: 0xFC000000 fill pattern
2311 78
                if ($fillType = Xls\Style\FillPattern::lookup(((int) 0xFC000000 & self::getInt4d($recordData, 14)) >> 26)) {
2312 78
                    $objStyle->getFill()->setFillType($fillType);
2313
                }
2314
                // offset: 18; size: 2; pattern and background colour
2315
                // bit: 6-0; mask: 0x007F; color index for pattern color
2316 78
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
2317
2318
                // bit: 13-7; mask: 0x3F80; color index for pattern background
2319 78
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
2320
            } else {
2321
                // BIFF5
2322
2323
                // offset: 7; size: 1; Text orientation and flags
2324 1
                $orientationAndFlags = ord($recordData[7]);
2325
2326
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
2327 1
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2328
                switch ($xfOrientation) {
2329 1
                    case 0:
2330 1
                        $objStyle->getAlignment()->setTextRotation(0);
2331
2332 1
                        break;
2333 1
                    case 1:
2334 1
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
2335
2336 1
                        break;
2337
                    case 2:
2338
                        $objStyle->getAlignment()->setTextRotation(90);
2339
2340
                        break;
2341
                    case 3:
2342
                        $objStyle->getAlignment()->setTextRotation(-90);
2343
2344
                        break;
2345
                }
2346
2347
                // offset: 8; size: 4; cell border lines and background area
2348 1
                $borderAndBackground = self::getInt4d($recordData, 8);
2349
2350
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
2351 1
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2352
2353
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
2354 1
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2355
2356
                // bit: 21-16; mask: 0x003F0000; fill pattern
2357 1
                $objStyle->getFill()->setFillType(Xls\Style\FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2358
2359
                // bit: 24-22; mask: 0x01C00000; bottom line style
2360 1
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2361
2362
                // bit: 31-25; mask: 0xFE000000; bottom line color
2363 1
                $objStyle->getBorders()->getBottom()->colorIndex = ((int) 0xFE000000 & $borderAndBackground) >> 25;
2364
2365
                // offset: 12; size: 4; cell border lines
2366 1
                $borderLines = self::getInt4d($recordData, 12);
2367
2368
                // bit: 2-0; mask: 0x00000007; top line style
2369 1
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2370
2371
                // bit: 5-3; mask: 0x00000038; left line style
2372 1
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2373
2374
                // bit: 8-6; mask: 0x000001C0; right line style
2375 1
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2376
2377
                // bit: 15-9; mask: 0x0000FE00; top line color index
2378 1
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2379
2380
                // bit: 22-16; mask: 0x007F0000; left line color index
2381 1
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2382
2383
                // bit: 29-23; mask: 0x3F800000; right line color index
2384 1
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2385
            }
2386
2387
            // add cellStyleXf or cellXf and update mapping
2388 79
            if ($isCellStyleXf) {
2389
                // we only read one style XF record which is always the first
2390 79
                if ($this->xfIndex == 0) {
2391 79
                    $this->spreadsheet->addCellStyleXf($objStyle);
2392 79
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
2393
                }
2394
            } else {
2395
                // we read all cell XF records
2396 79
                $this->spreadsheet->addCellXf($objStyle);
2397 79
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
2398
            }
2399
2400
            // update XF index for when we read next record
2401 79
            ++$this->xfIndex;
2402
        }
2403
    }
2404
2405 19
    private function readXfExt(): void
2406
    {
2407 19
        $length = self::getUInt2d($this->data, $this->pos + 2);
2408 19
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2409
2410
        // move stream pointer to next record
2411 19
        $this->pos += 4 + $length;
2412
2413 19
        if (!$this->readDataOnly) {
2414
            // offset: 0; size: 2; 0x087D = repeated header
2415
2416
            // offset: 2; size: 2
2417
2418
            // offset: 4; size: 8; not used
2419
2420
            // offset: 12; size: 2; record version
2421
2422
            // offset: 14; size: 2; index to XF record which this record modifies
2423 19
            $ixfe = self::getUInt2d($recordData, 14);
2424
2425
            // offset: 16; size: 2; not used
2426
2427
            // offset: 18; size: 2; number of extension properties that follow
2428 19
            $cexts = self::getUInt2d($recordData, 18);
2429
2430
            // start reading the actual extension data
2431 19
            $offset = 20;
2432 19
            while ($offset < $length) {
2433
                // extension type
2434 19
                $extType = self::getUInt2d($recordData, $offset);
2435
2436
                // extension length
2437 19
                $cb = self::getUInt2d($recordData, $offset + 2);
2438
2439
                // extension data
2440 19
                $extData = substr($recordData, $offset + 4, $cb);
2441
2442
                switch ($extType) {
2443 19
                    case 4:        // fill start color
2444 19
                        $xclfType = self::getUInt2d($extData, 0); // color type
2445 19
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2446
2447 19
                        if ($xclfType == 2) {
2448 17
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2449
2450
                            // modify the relevant style property
2451 17
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2452 5
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2453 5
                                $fill->getStartColor()->setRGB($rgb);
2454 5
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
2455
                            }
2456
                        }
2457
2458 19
                        break;
2459 17
                    case 5:        // fill end color
2460 3
                        $xclfType = self::getUInt2d($extData, 0); // color type
2461 3
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2462
2463 3
                        if ($xclfType == 2) {
2464 3
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2465
2466
                            // modify the relevant style property
2467 3
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2468 3
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2469 3
                                $fill->getEndColor()->setRGB($rgb);
2470 3
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
2471
                            }
2472
                        }
2473
2474 3
                        break;
2475 17
                    case 7:        // border color top
2476 17
                        $xclfType = self::getUInt2d($extData, 0); // color type
2477 17
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2478
2479 17
                        if ($xclfType == 2) {
2480 17
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2481
2482
                            // modify the relevant style property
2483 17
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2484 2
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
2485 2
                                $top->getColor()->setRGB($rgb);
2486 2
                                $top->colorIndex = null; // normal color index does not apply, discard
2487
                            }
2488
                        }
2489
2490 17
                        break;
2491 17
                    case 8:        // border color bottom
2492 17
                        $xclfType = self::getUInt2d($extData, 0); // color type
2493 17
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2494
2495 17
                        if ($xclfType == 2) {
2496 17
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2497
2498
                            // modify the relevant style property
2499 17
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2500 3
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
2501 3
                                $bottom->getColor()->setRGB($rgb);
2502 3
                                $bottom->colorIndex = null; // normal color index does not apply, discard
2503
                            }
2504
                        }
2505
2506 17
                        break;
2507 17
                    case 9:        // border color left
2508 17
                        $xclfType = self::getUInt2d($extData, 0); // color type
2509 17
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2510
2511 17
                        if ($xclfType == 2) {
2512 17
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2513
2514
                            // modify the relevant style property
2515 17
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2516 2
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
2517 2
                                $left->getColor()->setRGB($rgb);
2518 2
                                $left->colorIndex = null; // normal color index does not apply, discard
2519
                            }
2520
                        }
2521
2522 17
                        break;
2523 17
                    case 10:        // border color right
2524 17
                        $xclfType = self::getUInt2d($extData, 0); // color type
2525 17
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2526
2527 17
                        if ($xclfType == 2) {
2528 17
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2529
2530
                            // modify the relevant style property
2531 17
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2532 2
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
2533 2
                                $right->getColor()->setRGB($rgb);
2534 2
                                $right->colorIndex = null; // normal color index does not apply, discard
2535
                            }
2536
                        }
2537
2538 17
                        break;
2539 17
                    case 11:        // border color diagonal
2540
                        $xclfType = self::getUInt2d($extData, 0); // color type
2541
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2542
2543
                        if ($xclfType == 2) {
2544
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2545
2546
                            // modify the relevant style property
2547
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2548
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
2549
                                $diagonal->getColor()->setRGB($rgb);
2550
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
2551
                            }
2552
                        }
2553
2554
                        break;
2555 17
                    case 13:    // font color
2556 17
                        $xclfType = self::getUInt2d($extData, 0); // color type
2557 17
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2558
2559 17
                        if ($xclfType == 2) {
2560 17
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2561
2562
                            // modify the relevant style property
2563 17
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2564 4
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
2565 4
                                $font->getColor()->setRGB($rgb);
2566 4
                                $font->colorIndex = null; // normal color index does not apply, discard
2567
                            }
2568
                        }
2569
2570 17
                        break;
2571
                }
2572
2573 19
                $offset += $cb;
2574
            }
2575
        }
2576
    }
2577
2578
    /**
2579
     * Read STYLE record.
2580
     */
2581 80
    private function readStyle(): void
2582
    {
2583 80
        $length = self::getUInt2d($this->data, $this->pos + 2);
2584 80
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2585
2586
        // move stream pointer to next record
2587 80
        $this->pos += 4 + $length;
2588
2589 80
        if (!$this->readDataOnly) {
2590
            // offset: 0; size: 2; index to XF record and flag for built-in style
2591 79
            $ixfe = self::getUInt2d($recordData, 0);
2592
2593
            // bit: 11-0; mask 0x0FFF; index to XF record
2594 79
            $xfIndex = (0x0FFF & $ixfe) >> 0;
2595
2596
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
2597 79
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
2598
2599 79
            if ($isBuiltIn) {
2600
                // offset: 2; size: 1; identifier for built-in style
2601 79
                $builtInId = ord($recordData[2]);
2602
2603
                switch ($builtInId) {
2604 79
                    case 0x00:
2605
                        // currently, we are not using this for anything
2606 79
                        break;
2607
                    default:
2608 38
                        break;
2609
                }
2610
            }
2611
            // user-defined; not supported by PhpSpreadsheet
2612
        }
2613
    }
2614
2615
    /**
2616
     * Read PALETTE record.
2617
     */
2618 53
    private function readPalette(): void
2619
    {
2620 53
        $length = self::getUInt2d($this->data, $this->pos + 2);
2621 53
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2622
2623
        // move stream pointer to next record
2624 53
        $this->pos += 4 + $length;
2625
2626 53
        if (!$this->readDataOnly) {
2627
            // offset: 0; size: 2; number of following colors
2628 53
            $nm = self::getUInt2d($recordData, 0);
2629
2630
            // list of RGB colors
2631 53
            for ($i = 0; $i < $nm; ++$i) {
2632 53
                $rgb = substr($recordData, 2 + 4 * $i, 4);
2633 53
                $this->palette[] = self::readRGB($rgb);
2634
            }
2635
        }
2636
    }
2637
2638
    /**
2639
     * SHEET.
2640
     *
2641
     * This record is  located in the  Workbook Globals
2642
     * Substream  and represents a sheet inside the workbook.
2643
     * One SHEET record is written for each sheet. It stores the
2644
     * sheet name and a stream offset to the BOF record of the
2645
     * respective Sheet Substream within the Workbook Stream.
2646
     *
2647
     * --    "OpenOffice.org's Documentation of the Microsoft
2648
     *         Excel File Format"
2649
     */
2650 83
    private function readSheet(): void
2651
    {
2652 83
        $length = self::getUInt2d($this->data, $this->pos + 2);
2653 83
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2654
2655
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
2656
        // NOTE: not encrypted
2657 83
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
2658
2659
        // move stream pointer to next record
2660 83
        $this->pos += 4 + $length;
2661
2662
        // offset: 4; size: 1; sheet state
2663 83
        switch (ord($recordData[4])) {
2664 83
            case 0x00:
2665 83
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2666
2667 83
                break;
2668 2
            case 0x01:
2669 2
                $sheetState = Worksheet::SHEETSTATE_HIDDEN;
2670
2671 2
                break;
2672 1
            case 0x02:
2673 1
                $sheetState = Worksheet::SHEETSTATE_VERYHIDDEN;
2674
2675 1
                break;
2676
            default:
2677
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2678
2679
                break;
2680
        }
2681
2682
        // offset: 5; size: 1; sheet type
2683 83
        $sheetType = ord($recordData[5]);
2684
2685
        // offset: 6; size: var; sheet name
2686 83
        $rec_name = null;
2687 83
        if ($this->version == self::XLS_BIFF8) {
2688 82
            $string = self::readUnicodeStringShort(substr($recordData, 6));
2689 82
            $rec_name = $string['value'];
2690 1
        } elseif ($this->version == self::XLS_BIFF7) {
2691 1
            $string = $this->readByteStringShort(substr($recordData, 6));
2692 1
            $rec_name = $string['value'];
2693
        }
2694
2695 83
        $this->sheets[] = [
2696 83
            'name' => $rec_name,
2697 83
            'offset' => $rec_offset,
2698 83
            'sheetState' => $sheetState,
2699 83
            'sheetType' => $sheetType,
2700 83
        ];
2701
    }
2702
2703
    /**
2704
     * Read EXTERNALBOOK record.
2705
     */
2706 54
    private function readExternalBook(): void
2707
    {
2708 54
        $length = self::getUInt2d($this->data, $this->pos + 2);
2709 54
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2710
2711
        // move stream pointer to next record
2712 54
        $this->pos += 4 + $length;
2713
2714
        // offset within record data
2715 54
        $offset = 0;
2716
2717
        // there are 4 types of records
2718 54
        if (strlen($recordData) > 4) {
2719
            // external reference
2720
            // offset: 0; size: 2; number of sheet names ($nm)
2721
            $nm = self::getUInt2d($recordData, 0);
2722
            $offset += 2;
2723
2724
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
2725
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
2726
            $offset += $encodedUrlString['size'];
2727
2728
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
2729
            $externalSheetNames = [];
2730
            for ($i = 0; $i < $nm; ++$i) {
2731
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
2732
                $externalSheetNames[] = $externalSheetNameString['value'];
2733
                $offset += $externalSheetNameString['size'];
2734
            }
2735
2736
            // store the record data
2737
            $this->externalBooks[] = [
2738
                'type' => 'external',
2739
                'encodedUrl' => $encodedUrlString['value'],
2740
                'externalSheetNames' => $externalSheetNames,
2741
            ];
2742 54
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
2743
            // internal reference
2744
            // offset: 0; size: 2; number of sheet in this document
2745
            // offset: 2; size: 2; 0x01 0x04
2746 54
            $this->externalBooks[] = [
2747 54
                'type' => 'internal',
2748 54
            ];
2749
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
2750
            // add-in function
2751
            // offset: 0; size: 2; 0x0001
2752
            $this->externalBooks[] = [
2753
                'type' => 'addInFunction',
2754
            ];
2755
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
2756
            // DDE links, OLE links
2757
            // offset: 0; size: 2; 0x0000
2758
            // offset: 2; size: var; encoded source document name
2759
            $this->externalBooks[] = [
2760
                'type' => 'DDEorOLE',
2761
            ];
2762
        }
2763
    }
2764
2765
    /**
2766
     * Read EXTERNNAME record.
2767
     */
2768
    private function readExternName(): void
2769
    {
2770
        $length = self::getUInt2d($this->data, $this->pos + 2);
2771
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2772
2773
        // move stream pointer to next record
2774
        $this->pos += 4 + $length;
2775
2776
        // external sheet references provided for named cells
2777
        if ($this->version == self::XLS_BIFF8) {
2778
            // offset: 0; size: 2; options
2779
            $options = self::getUInt2d($recordData, 0);
2780
2781
            // offset: 2; size: 2;
2782
2783
            // offset: 4; size: 2; not used
2784
2785
            // offset: 6; size: var
2786
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
2787
2788
            // offset: var; size: var; formula data
2789
            $offset = 6 + $nameString['size'];
2790
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
2791
2792
            $this->externalNames[] = [
2793
                'name' => $nameString['value'],
2794
                'formula' => $formula,
2795
            ];
2796
        }
2797
    }
2798
2799
    /**
2800
     * Read EXTERNSHEET record.
2801
     */
2802 55
    private function readExternSheet(): void
2803
    {
2804 55
        $length = self::getUInt2d($this->data, $this->pos + 2);
2805 55
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2806
2807
        // move stream pointer to next record
2808 55
        $this->pos += 4 + $length;
2809
2810
        // external sheet references provided for named cells
2811 55
        if ($this->version == self::XLS_BIFF8) {
2812
            // offset: 0; size: 2; number of following ref structures
2813 54
            $nm = self::getUInt2d($recordData, 0);
2814 54
            for ($i = 0; $i < $nm; ++$i) {
2815 52
                $this->ref[] = [
2816
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
2817 52
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
2818
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
2819 52
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
2820
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
2821 52
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
2822 52
                ];
2823
            }
2824
        }
2825
    }
2826
2827
    /**
2828
     * DEFINEDNAME.
2829
     *
2830
     * This record is part of a Link Table. It contains the name
2831
     * and the token array of an internal defined name. Token
2832
     * arrays of defined names contain tokens with aberrant
2833
     * token classes.
2834
     *
2835
     * --    "OpenOffice.org's Documentation of the Microsoft
2836
     *         Excel File Format"
2837
     */
2838 13
    private function readDefinedName(): void
2839
    {
2840 13
        $length = self::getUInt2d($this->data, $this->pos + 2);
2841 13
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2842
2843
        // move stream pointer to next record
2844 13
        $this->pos += 4 + $length;
2845
2846 13
        if ($this->version == self::XLS_BIFF8) {
2847
            // retrieves named cells
2848
2849
            // offset: 0; size: 2; option flags
2850 12
            $opts = self::getUInt2d($recordData, 0);
2851
2852
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
2853 12
            $isBuiltInName = (0x0020 & $opts) >> 5;
2854
2855
            // offset: 2; size: 1; keyboard shortcut
2856
2857
            // offset: 3; size: 1; length of the name (character count)
2858 12
            $nlen = ord($recordData[3]);
2859
2860
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
2861
            // note: there can also be additional data, this is not included in $flen
2862 12
            $flen = self::getUInt2d($recordData, 4);
2863
2864
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
2865 12
            $scope = self::getUInt2d($recordData, 8);
2866
2867
            // offset: 14; size: var; Name (Unicode string without length field)
2868 12
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
2869
2870
            // offset: var; size: $flen; formula data
2871 12
            $offset = 14 + $string['size'];
2872 12
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
2873
2874
            try {
2875 12
                $formula = $this->getFormulaFromStructure($formulaStructure);
2876 1
            } catch (PhpSpreadsheetException $e) {
2877 1
                $formula = '';
2878
            }
2879
2880 12
            $this->definedname[] = [
2881 12
                'isBuiltInName' => $isBuiltInName,
2882 12
                'name' => $string['value'],
2883 12
                'formula' => $formula,
2884 12
                'scope' => $scope,
2885 12
            ];
2886
        }
2887
    }
2888
2889
    /**
2890
     * Read MSODRAWINGGROUP record.
2891
     */
2892 17
    private function readMsoDrawingGroup(): void
2893
    {
2894 17
        $length = self::getUInt2d($this->data, $this->pos + 2);
2895
2896
        // get spliced record data
2897 17
        $splicedRecordData = $this->getSplicedRecordData();
2898 17
        $recordData = $splicedRecordData['recordData'];
2899
2900 17
        $this->drawingGroupData .= $recordData;
2901
    }
2902
2903
    /**
2904
     * SST - Shared String Table.
2905
     *
2906
     * This record contains a list of all strings used anywhere
2907
     * in the workbook. Each string occurs only once. The
2908
     * workbook uses indexes into the list to reference the
2909
     * strings.
2910
     *
2911
     * --    "OpenOffice.org's Documentation of the Microsoft
2912
     *         Excel File Format"
2913
     */
2914 76
    private function readSst(): void
2915
    {
2916
        // offset within (spliced) record data
2917 76
        $pos = 0;
2918
2919
        // Limit global SST position, further control for bad SST Length in BIFF8 data
2920 76
        $limitposSST = 0;
2921
2922
        // get spliced record data
2923 76
        $splicedRecordData = $this->getSplicedRecordData();
2924
2925 76
        $recordData = $splicedRecordData['recordData'];
2926 76
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
2927
2928
        // offset: 0; size: 4; total number of strings in the workbook
2929 76
        $pos += 4;
2930
2931
        // offset: 4; size: 4; number of following strings ($nm)
2932 76
        $nm = self::getInt4d($recordData, 4);
2933 76
        $pos += 4;
2934
2935
        // look up limit position
2936 76
        foreach ($spliceOffsets as $spliceOffset) {
2937
            // it can happen that the string is empty, therefore we need
2938
            // <= and not just <
2939 76
            if ($pos <= $spliceOffset) {
2940 76
                $limitposSST = $spliceOffset;
2941
            }
2942
        }
2943
2944
        // loop through the Unicode strings (16-bit length)
2945 76
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
2946
            // number of characters in the Unicode string
2947 54
            $numChars = self::getUInt2d($recordData, $pos);
2948 54
            $pos += 2;
2949
2950
            // option flags
2951 54
            $optionFlags = ord($recordData[$pos]);
2952 54
            ++$pos;
2953
2954
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
2955 54
            $isCompressed = (($optionFlags & 0x01) == 0);
2956
2957
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
2958 54
            $hasAsian = (($optionFlags & 0x04) != 0);
2959
2960
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
2961 54
            $hasRichText = (($optionFlags & 0x08) != 0);
2962
2963 54
            $formattingRuns = 0;
2964 54
            if ($hasRichText) {
2965
                // number of Rich-Text formatting runs
2966 5
                $formattingRuns = self::getUInt2d($recordData, $pos);
2967 5
                $pos += 2;
2968
            }
2969
2970 54
            $extendedRunLength = 0;
2971 54
            if ($hasAsian) {
2972
                // size of Asian phonetic setting
2973
                $extendedRunLength = self::getInt4d($recordData, $pos);
2974
                $pos += 4;
2975
            }
2976
2977
            // expected byte length of character array if not split
2978 54
            $len = ($isCompressed) ? $numChars : $numChars * 2;
2979
2980
            // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
2981 54
            $limitpos = null;
2982 54
            foreach ($spliceOffsets as $spliceOffset) {
2983
                // it can happen that the string is empty, therefore we need
2984
                // <= and not just <
2985 54
                if ($pos <= $spliceOffset) {
2986 54
                    $limitpos = $spliceOffset;
2987
2988 54
                    break;
2989
                }
2990
            }
2991
2992 54
            if ($pos + $len <= $limitpos) {
2993
                // character array is not split between records
2994
2995 54
                $retstr = substr($recordData, $pos, $len);
2996 54
                $pos += $len;
2997
            } else {
2998
                // character array is split between records
2999
3000
                // first part of character array
3001 1
                $retstr = substr($recordData, $pos, $limitpos - $pos);
3002
3003 1
                $bytesRead = $limitpos - $pos;
3004
3005
                // remaining characters in Unicode string
3006 1
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
3007
3008 1
                $pos = $limitpos;
3009
3010
                // keep reading the characters
3011 1
                while ($charsLeft > 0) {
3012
                    // look up next limit position, in case the string span more than one continue record
3013 1
                    foreach ($spliceOffsets as $spliceOffset) {
3014 1
                        if ($pos < $spliceOffset) {
3015 1
                            $limitpos = $spliceOffset;
3016
3017 1
                            break;
3018
                        }
3019
                    }
3020
3021
                    // repeated option flags
3022
                    // OpenOffice.org documentation 5.21
3023 1
                    $option = ord($recordData[$pos]);
3024 1
                    ++$pos;
3025
3026 1
                    if ($isCompressed && ($option == 0)) {
3027
                        // 1st fragment compressed
3028
                        // this fragment compressed
3029
                        $len = min($charsLeft, $limitpos - $pos);
3030
                        $retstr .= substr($recordData, $pos, $len);
3031
                        $charsLeft -= $len;
3032
                        $isCompressed = true;
3033 1
                    } elseif (!$isCompressed && ($option != 0)) {
3034
                        // 1st fragment uncompressed
3035
                        // this fragment uncompressed
3036 1
                        $len = min($charsLeft * 2, $limitpos - $pos);
3037 1
                        $retstr .= substr($recordData, $pos, $len);
3038 1
                        $charsLeft -= $len / 2;
3039 1
                        $isCompressed = false;
3040
                    } elseif (!$isCompressed && ($option == 0)) {
3041
                        // 1st fragment uncompressed
3042
                        // this fragment compressed
3043
                        $len = min($charsLeft, $limitpos - $pos);
3044
                        for ($j = 0; $j < $len; ++$j) {
3045
                            $retstr .= $recordData[$pos + $j]
3046
                            . chr(0);
3047
                        }
3048
                        $charsLeft -= $len;
3049
                        $isCompressed = false;
3050
                    } else {
3051
                        // 1st fragment compressed
3052
                        // this fragment uncompressed
3053
                        $newstr = '';
3054
                        $jMax = strlen($retstr);
3055
                        for ($j = 0; $j < $jMax; ++$j) {
3056
                            $newstr .= $retstr[$j] . chr(0);
3057
                        }
3058
                        $retstr = $newstr;
3059
                        $len = min($charsLeft * 2, $limitpos - $pos);
3060
                        $retstr .= substr($recordData, $pos, $len);
3061
                        $charsLeft -= $len / 2;
3062
                        $isCompressed = false;
3063
                    }
3064
3065 1
                    $pos += $len;
3066
                }
3067
            }
3068
3069
            // convert to UTF-8
3070 54
            $retstr = self::encodeUTF16($retstr, $isCompressed);
3071
3072
            // read additional Rich-Text information, if any
3073 54
            $fmtRuns = [];
3074 54
            if ($hasRichText) {
3075
                // list of formatting runs
3076 5
                for ($j = 0; $j < $formattingRuns; ++$j) {
3077
                    // first formatted character; zero-based
3078 5
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
3079
3080
                    // index to font record
3081 5
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
3082
3083 5
                    $fmtRuns[] = [
3084 5
                        'charPos' => $charPos,
3085 5
                        'fontIndex' => $fontIndex,
3086 5
                    ];
3087
                }
3088 5
                $pos += 4 * $formattingRuns;
3089
            }
3090
3091
            // read additional Asian phonetics information, if any
3092 54
            if ($hasAsian) {
3093
                // For Asian phonetic settings, we skip the extended string data
3094
                $pos += $extendedRunLength;
3095
            }
3096
3097
            // store the shared sting
3098 54
            $this->sst[] = [
3099 54
                'value' => $retstr,
3100 54
                'fmtRuns' => $fmtRuns,
3101 54
            ];
3102
        }
3103
3104
        // getSplicedRecordData() takes care of moving current position in data stream
3105
    }
3106
3107
    /**
3108
     * Read PRINTGRIDLINES record.
3109
     */
3110 77
    private function readPrintGridlines(): void
3111
    {
3112 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
3113 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3114
3115
        // move stream pointer to next record
3116 77
        $this->pos += 4 + $length;
3117
3118 77
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3119
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
3120 75
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
3121 75
            $this->phpSheet->setPrintGridlines($printGridlines);
3122
        }
3123
    }
3124
3125
    /**
3126
     * Read DEFAULTROWHEIGHT record.
3127
     */
3128 45
    private function readDefaultRowHeight(): void
3129
    {
3130 45
        $length = self::getUInt2d($this->data, $this->pos + 2);
3131 45
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3132
3133
        // move stream pointer to next record
3134 45
        $this->pos += 4 + $length;
3135
3136
        // offset: 0; size: 2; option flags
3137
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
3138 45
        $height = self::getUInt2d($recordData, 2);
3139 45
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
3140
    }
3141
3142
    /**
3143
     * Read SHEETPR record.
3144
     */
3145 79
    private function readSheetPr(): void
3146
    {
3147 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3148 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3149
3150
        // move stream pointer to next record
3151 79
        $this->pos += 4 + $length;
3152
3153
        // offset: 0; size: 2
3154
3155
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
3156 79
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
3157 79
        $this->phpSheet->setShowSummaryBelow($isSummaryBelow);
3158
3159
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
3160 79
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
3161 79
        $this->phpSheet->setShowSummaryRight($isSummaryRight);
3162
3163
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
3164
        // this corresponds to radio button setting in page setup dialog in Excel
3165 79
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
3166
    }
3167
3168
    /**
3169
     * Read HORIZONTALPAGEBREAKS record.
3170
     */
3171 4
    private function readHorizontalPageBreaks(): void
3172
    {
3173 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
3174 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3175
3176
        // move stream pointer to next record
3177 4
        $this->pos += 4 + $length;
3178
3179 4
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3180
            // offset: 0; size: 2; number of the following row index structures
3181 4
            $nm = self::getUInt2d($recordData, 0);
3182
3183
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3184 4
            for ($i = 0; $i < $nm; ++$i) {
3185 2
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3186 2
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3187 2
                $cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3188
3189
                // not sure why two column indexes are necessary?
3190 2
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3191
            }
3192
        }
3193
    }
3194
3195
    /**
3196
     * Read VERTICALPAGEBREAKS record.
3197
     */
3198 4
    private function readVerticalPageBreaks(): void
3199
    {
3200 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
3201 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3202
3203
        // move stream pointer to next record
3204 4
        $this->pos += 4 + $length;
3205
3206 4
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3207
            // offset: 0; size: 2; number of the following column index structures
3208 4
            $nm = self::getUInt2d($recordData, 0);
3209
3210
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3211 4
            for ($i = 0; $i < $nm; ++$i) {
3212 2
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3213 2
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3214
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3215
3216
                // not sure why two row indexes are necessary?
3217 2
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3218
            }
3219
        }
3220
    }
3221
3222
    /**
3223
     * Read HEADER record.
3224
     */
3225 77
    private function readHeader(): void
3226
    {
3227 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
3228 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3229
3230
        // move stream pointer to next record
3231 77
        $this->pos += 4 + $length;
3232
3233 77
        if (!$this->readDataOnly) {
3234
            // offset: 0; size: var
3235
            // realized that $recordData can be empty even when record exists
3236 76
            if ($recordData) {
3237 44
                if ($this->version == self::XLS_BIFF8) {
3238 43
                    $string = self::readUnicodeStringLong($recordData);
3239
                } else {
3240 1
                    $string = $this->readByteStringShort($recordData);
3241
                }
3242
3243 44
                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
3244 44
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
3245
            }
3246
        }
3247
    }
3248
3249
    /**
3250
     * Read FOOTER record.
3251
     */
3252 77
    private function readFooter(): void
3253
    {
3254 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
3255 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3256
3257
        // move stream pointer to next record
3258 77
        $this->pos += 4 + $length;
3259
3260 77
        if (!$this->readDataOnly) {
3261
            // offset: 0; size: var
3262
            // realized that $recordData can be empty even when record exists
3263 76
            if ($recordData) {
3264 46
                if ($this->version == self::XLS_BIFF8) {
3265 45
                    $string = self::readUnicodeStringLong($recordData);
3266
                } else {
3267 1
                    $string = $this->readByteStringShort($recordData);
3268
                }
3269 46
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
3270 46
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
3271
            }
3272
        }
3273
    }
3274
3275
    /**
3276
     * Read HCENTER record.
3277
     */
3278 77
    private function readHcenter(): void
3279
    {
3280 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
3281 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3282
3283
        // move stream pointer to next record
3284 77
        $this->pos += 4 + $length;
3285
3286 77
        if (!$this->readDataOnly) {
3287
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
3288 76
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
3289
3290 76
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
3291
        }
3292
    }
3293
3294
    /**
3295
     * Read VCENTER record.
3296
     */
3297 77
    private function readVcenter(): void
3298
    {
3299 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
3300 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3301
3302
        // move stream pointer to next record
3303 77
        $this->pos += 4 + $length;
3304
3305 77
        if (!$this->readDataOnly) {
3306
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
3307 76
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
3308
3309 76
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
3310
        }
3311
    }
3312
3313
    /**
3314
     * Read LEFTMARGIN record.
3315
     */
3316 62
    private function readLeftMargin(): void
3317
    {
3318 62
        $length = self::getUInt2d($this->data, $this->pos + 2);
3319 62
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3320
3321
        // move stream pointer to next record
3322 62
        $this->pos += 4 + $length;
3323
3324 62
        if (!$this->readDataOnly) {
3325
            // offset: 0; size: 8
3326 62
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
3327
        }
3328
    }
3329
3330
    /**
3331
     * Read RIGHTMARGIN record.
3332
     */
3333 62
    private function readRightMargin(): void
3334
    {
3335 62
        $length = self::getUInt2d($this->data, $this->pos + 2);
3336 62
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3337
3338
        // move stream pointer to next record
3339 62
        $this->pos += 4 + $length;
3340
3341 62
        if (!$this->readDataOnly) {
3342
            // offset: 0; size: 8
3343 62
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
3344
        }
3345
    }
3346
3347
    /**
3348
     * Read TOPMARGIN record.
3349
     */
3350 62
    private function readTopMargin(): void
3351
    {
3352 62
        $length = self::getUInt2d($this->data, $this->pos + 2);
3353 62
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3354
3355
        // move stream pointer to next record
3356 62
        $this->pos += 4 + $length;
3357
3358 62
        if (!$this->readDataOnly) {
3359
            // offset: 0; size: 8
3360 62
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
3361
        }
3362
    }
3363
3364
    /**
3365
     * Read BOTTOMMARGIN record.
3366
     */
3367 62
    private function readBottomMargin(): void
3368
    {
3369 62
        $length = self::getUInt2d($this->data, $this->pos + 2);
3370 62
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3371
3372
        // move stream pointer to next record
3373 62
        $this->pos += 4 + $length;
3374
3375 62
        if (!$this->readDataOnly) {
3376
            // offset: 0; size: 8
3377 62
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
3378
        }
3379
    }
3380
3381
    /**
3382
     * Read PAGESETUP record.
3383
     */
3384 79
    private function readPageSetup(): void
3385
    {
3386 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3387 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3388
3389
        // move stream pointer to next record
3390 79
        $this->pos += 4 + $length;
3391
3392 79
        if (!$this->readDataOnly) {
3393
            // offset: 0; size: 2; paper size
3394 78
            $paperSize = self::getUInt2d($recordData, 0);
3395
3396
            // offset: 2; size: 2; scaling factor
3397 78
            $scale = self::getUInt2d($recordData, 2);
3398
3399
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
3400 78
            $fitToWidth = self::getUInt2d($recordData, 6);
3401
3402
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
3403 78
            $fitToHeight = self::getUInt2d($recordData, 8);
3404
3405
            // offset: 10; size: 2; option flags
3406
3407
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
3408 78
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
3409
3410
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
3411 78
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
3412
3413
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
3414
            // when this bit is set, do not use flags for those properties
3415 78
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
3416
3417 78
            if (!$isNotInit) {
3418 74
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
3419 74
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
3420 74
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
3421
3422 74
                $this->phpSheet->getPageSetup()->setScale($scale, false);
3423 74
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
3424 74
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
3425 74
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
3426
            }
3427
3428
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
3429 78
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
3430 78
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
3431
3432
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
3433 78
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
3434 78
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
3435
        }
3436
    }
3437
3438
    /**
3439
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
3440
     *   if this record is omitted, then it also means no sheet protection.
3441
     */
3442 6
    private function readProtect(): void
3443
    {
3444 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
3445 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3446
3447
        // move stream pointer to next record
3448 6
        $this->pos += 4 + $length;
3449
3450 6
        if ($this->readDataOnly) {
3451
            return;
3452
        }
3453
3454
        // offset: 0; size: 2;
3455
3456
        // bit 0, mask 0x01; 1 = sheet is protected
3457 6
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3458 6
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
3459
    }
3460
3461
    /**
3462
     * SCENPROTECT.
3463
     */
3464
    private function readScenProtect(): void
3465
    {
3466
        $length = self::getUInt2d($this->data, $this->pos + 2);
3467
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3468
3469
        // move stream pointer to next record
3470
        $this->pos += 4 + $length;
3471
3472
        if ($this->readDataOnly) {
3473
            return;
3474
        }
3475
3476
        // offset: 0; size: 2;
3477
3478
        // bit: 0, mask 0x01; 1 = scenarios are protected
3479
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3480
3481
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
3482
    }
3483
3484
    /**
3485
     * OBJECTPROTECT.
3486
     */
3487 1
    private function readObjectProtect(): void
3488
    {
3489 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
3490 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3491
3492
        // move stream pointer to next record
3493 1
        $this->pos += 4 + $length;
3494
3495 1
        if ($this->readDataOnly) {
3496
            return;
3497
        }
3498
3499
        // offset: 0; size: 2;
3500
3501
        // bit: 0, mask 0x01; 1 = objects are protected
3502 1
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3503
3504 1
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
3505
    }
3506
3507
    /**
3508
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
3509
     */
3510 2
    private function readPassword(): void
3511
    {
3512 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
3513 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3514
3515
        // move stream pointer to next record
3516 2
        $this->pos += 4 + $length;
3517
3518 2
        if (!$this->readDataOnly) {
3519
            // offset: 0; size: 2; 16-bit hash value of password
3520 2
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3521 2
            $this->phpSheet->getProtection()->setPassword($password, true);
3522
        }
3523
    }
3524
3525
    /**
3526
     * Read DEFCOLWIDTH record.
3527
     */
3528 78
    private function readDefColWidth(): void
3529
    {
3530 78
        $length = self::getUInt2d($this->data, $this->pos + 2);
3531 78
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3532
3533
        // move stream pointer to next record
3534 78
        $this->pos += 4 + $length;
3535
3536
        // offset: 0; size: 2; default column width
3537 78
        $width = self::getUInt2d($recordData, 0);
3538 78
        if ($width != 8) {
3539 11
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
3540
        }
3541
    }
3542
3543
    /**
3544
     * Read COLINFO record.
3545
     */
3546 72
    private function readColInfo(): void
3547
    {
3548 72
        $length = self::getUInt2d($this->data, $this->pos + 2);
3549 72
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3550
3551
        // move stream pointer to next record
3552 72
        $this->pos += 4 + $length;
3553
3554 72
        if (!$this->readDataOnly) {
3555
            // offset: 0; size: 2; index to first column in range
3556 71
            $firstColumnIndex = self::getUInt2d($recordData, 0);
3557
3558
            // offset: 2; size: 2; index to last column in range
3559 71
            $lastColumnIndex = self::getUInt2d($recordData, 2);
3560
3561
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
3562 71
            $width = self::getUInt2d($recordData, 4);
3563
3564
            // offset: 6; size: 2; index to XF record for default column formatting
3565 71
            $xfIndex = self::getUInt2d($recordData, 6);
3566
3567
            // offset: 8; size: 2; option flags
3568
            // bit: 0; mask: 0x0001; 1= columns are hidden
3569 71
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
3570
3571
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
3572 71
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
3573
3574
            // bit: 12; mask: 0x1000; 1 = collapsed
3575 71
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
3576
3577
            // offset: 10; size: 2; not used
3578
3579 71
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
3580 71
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
3581 13
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
3582
3583 13
                    break;
3584
                }
3585 63
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
3586 63
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
3587 63
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
3588 63
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
3589 63
                if (isset($this->mapCellXfIndex[$xfIndex])) {
3590 61
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3591
                }
3592
            }
3593
        }
3594
    }
3595
3596
    /**
3597
     * ROW.
3598
     *
3599
     * This record contains the properties of a single row in a
3600
     * sheet. Rows and cells in a sheet are divided into blocks
3601
     * of 32 rows.
3602
     *
3603
     * --    "OpenOffice.org's Documentation of the Microsoft
3604
     *         Excel File Format"
3605
     */
3606 52
    private function readRow(): void
3607
    {
3608 52
        $length = self::getUInt2d($this->data, $this->pos + 2);
3609 52
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3610
3611
        // move stream pointer to next record
3612 52
        $this->pos += 4 + $length;
3613
3614 52
        if (!$this->readDataOnly) {
3615
            // offset: 0; size: 2; index of this row
3616 51
            $r = self::getUInt2d($recordData, 0);
3617
3618
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
3619
3620
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
3621
3622
            // offset: 6; size: 2;
3623
3624
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
3625 51
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
3626
3627
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
3628 51
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
3629
3630 51
            if (!$useDefaultHeight) {
3631 49
                $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
3632
            }
3633
3634
            // offset: 8; size: 2; not used
3635
3636
            // offset: 10; size: 2; not used in BIFF5-BIFF8
3637
3638
            // offset: 12; size: 4; option flags and default row formatting
3639
3640
            // bit: 2-0: mask: 0x00000007; outline level of the row
3641 51
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
3642 51
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
3643
3644
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
3645 51
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
3646 51
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
3647
3648
            // bit: 5; mask: 0x00000020; 1 = row is hidden
3649 51
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
3650 51
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
3651
3652
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
3653 51
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
3654
3655
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
3656 51
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
3657
3658 51
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
3659 6
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3660
            }
3661
        }
3662
    }
3663
3664
    /**
3665
     * Read RK record
3666
     * This record represents a cell that contains an RK value
3667
     * (encoded integer or floating-point value). If a
3668
     * floating-point value cannot be encoded to an RK value,
3669
     * a NUMBER record will be written. This record replaces the
3670
     * record INTEGER written in BIFF2.
3671
     *
3672
     * --    "OpenOffice.org's Documentation of the Microsoft
3673
     *         Excel File Format"
3674
     */
3675 26
    private function readRk(): void
3676
    {
3677 26
        $length = self::getUInt2d($this->data, $this->pos + 2);
3678 26
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3679
3680
        // move stream pointer to next record
3681 26
        $this->pos += 4 + $length;
3682
3683
        // offset: 0; size: 2; index to row
3684 26
        $row = self::getUInt2d($recordData, 0);
3685
3686
        // offset: 2; size: 2; index to column
3687 26
        $column = self::getUInt2d($recordData, 2);
3688 26
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3689
3690
        // Read cell?
3691 26
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3692
            // offset: 4; size: 2; index to XF record
3693 26
            $xfIndex = self::getUInt2d($recordData, 4);
3694
3695
            // offset: 6; size: 4; RK value
3696 26
            $rknum = self::getInt4d($recordData, 6);
3697 26
            $numValue = self::getIEEE754($rknum);
3698
3699 26
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3700 26
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3701
                // add style information
3702 23
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3703
            }
3704
3705
            // add cell
3706 26
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3707
        }
3708
    }
3709
3710
    /**
3711
     * Read LABELSST record
3712
     * This record represents a cell that contains a string. It
3713
     * replaces the LABEL record and RSTRING record used in
3714
     * BIFF2-BIFF5.
3715
     *
3716
     * --    "OpenOffice.org's Documentation of the Microsoft
3717
     *         Excel File Format"
3718
     */
3719 53
    private function readLabelSst(): void
3720
    {
3721 53
        $length = self::getUInt2d($this->data, $this->pos + 2);
3722 53
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3723
3724
        // move stream pointer to next record
3725 53
        $this->pos += 4 + $length;
3726
3727
        // offset: 0; size: 2; index to row
3728 53
        $row = self::getUInt2d($recordData, 0);
3729
3730
        // offset: 2; size: 2; index to column
3731 53
        $column = self::getUInt2d($recordData, 2);
3732 53
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3733
3734 53
        $emptyCell = true;
3735
        // Read cell?
3736 53
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3737
            // offset: 4; size: 2; index to XF record
3738 53
            $xfIndex = self::getUInt2d($recordData, 4);
3739
3740
            // offset: 6; size: 4; index to SST record
3741 53
            $index = self::getInt4d($recordData, 6);
3742
3743
            // add cell
3744 53
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
3745
                // then we should treat as rich text
3746 5
                $richText = new RichText();
3747 5
                $charPos = 0;
3748 5
                $sstCount = count($this->sst[$index]['fmtRuns']);
3749 5
                for ($i = 0; $i <= $sstCount; ++$i) {
3750 5
                    if (isset($fmtRuns[$i])) {
3751 5
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $fmtRuns[$i]['charPos'] - $charPos);
3752 5
                        $charPos = $fmtRuns[$i]['charPos'];
3753
                    } else {
3754 5
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
3755
                    }
3756
3757 5
                    if (StringHelper::countCharacters($text) > 0) {
3758 5
                        if ($i == 0) { // first text run, no style
3759 3
                            $richText->createText($text);
3760
                        } else {
3761 5
                            $textRun = $richText->createTextRun($text);
3762 5
                            if (isset($fmtRuns[$i - 1])) {
3763 5
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
3764 4
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
3765
                                } else {
3766
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
3767
                                    // check the OpenOffice documentation of the FONT record
3768 4
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1;
3769
                                }
3770 5
                                if (array_key_exists($fontIndex, $this->objFonts) === false) {
3771 1
                                    $fontIndex = count($this->objFonts) - 1;
3772
                                }
3773 5
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
3774
                            }
3775
                        }
3776
                    }
3777
                }
3778 5
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
3779 5
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3780 5
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
3781 5
                    $emptyCell = false;
3782
                }
3783
            } else {
3784 53
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
3785 53
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3786 53
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
3787 53
                    $emptyCell = false;
3788
                }
3789
            }
3790
3791 53
            if (!$this->readDataOnly && !$emptyCell && isset($this->mapCellXfIndex[$xfIndex])) {
3792
                // add style information
3793 52
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3794
            }
3795
        }
3796
    }
3797
3798
    /**
3799
     * Read MULRK record
3800
     * This record represents a cell range containing RK value
3801
     * cells. All cells are located in the same row.
3802
     *
3803
     * --    "OpenOffice.org's Documentation of the Microsoft
3804
     *         Excel File Format"
3805
     */
3806 17
    private function readMulRk(): void
3807
    {
3808 17
        $length = self::getUInt2d($this->data, $this->pos + 2);
3809 17
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3810
3811
        // move stream pointer to next record
3812 17
        $this->pos += 4 + $length;
3813
3814
        // offset: 0; size: 2; index to row
3815 17
        $row = self::getUInt2d($recordData, 0);
3816
3817
        // offset: 2; size: 2; index to first column
3818 17
        $colFirst = self::getUInt2d($recordData, 2);
3819
3820
        // offset: var; size: 2; index to last column
3821 17
        $colLast = self::getUInt2d($recordData, $length - 2);
3822 17
        $columns = $colLast - $colFirst + 1;
3823
3824
        // offset within record data
3825 17
        $offset = 4;
3826
3827 17
        for ($i = 1; $i <= $columns; ++$i) {
3828 17
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
3829
3830
            // Read cell?
3831 17
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3832
                // offset: var; size: 2; index to XF record
3833 17
                $xfIndex = self::getUInt2d($recordData, $offset);
3834
3835
                // offset: var; size: 4; RK value
3836 17
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
3837 17
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3838 17
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3839
                    // add style
3840 16
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3841
                }
3842
3843
                // add cell value
3844 17
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3845
            }
3846
3847 17
            $offset += 6;
3848
        }
3849
    }
3850
3851
    /**
3852
     * Read NUMBER record
3853
     * This record represents a cell that contains a
3854
     * floating-point value.
3855
     *
3856
     * --    "OpenOffice.org's Documentation of the Microsoft
3857
     *         Excel File Format"
3858
     */
3859 40
    private function readNumber(): void
3860
    {
3861 40
        $length = self::getUInt2d($this->data, $this->pos + 2);
3862 40
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3863
3864
        // move stream pointer to next record
3865 40
        $this->pos += 4 + $length;
3866
3867
        // offset: 0; size: 2; index to row
3868 40
        $row = self::getUInt2d($recordData, 0);
3869
3870
        // offset: 2; size 2; index to column
3871 40
        $column = self::getUInt2d($recordData, 2);
3872 40
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3873
3874
        // Read cell?
3875 40
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3876
            // offset 4; size: 2; index to XF record
3877 40
            $xfIndex = self::getUInt2d($recordData, 4);
3878
3879 40
            $numValue = self::extractNumber(substr($recordData, 6, 8));
3880
3881 40
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3882 40
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3883
                // add cell style
3884 39
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3885
            }
3886
3887
            // add cell value
3888 40
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3889
        }
3890
    }
3891
3892
    /**
3893
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
3894
     * This record contains the token array and the result of a
3895
     * formula cell.
3896
     *
3897
     * --    "OpenOffice.org's Documentation of the Microsoft
3898
     *         Excel File Format"
3899
     */
3900 22
    private function readFormula(): void
3901
    {
3902 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
3903 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3904
3905
        // move stream pointer to next record
3906 22
        $this->pos += 4 + $length;
3907
3908
        // offset: 0; size: 2; row index
3909 22
        $row = self::getUInt2d($recordData, 0);
3910
3911
        // offset: 2; size: 2; col index
3912 22
        $column = self::getUInt2d($recordData, 2);
3913 22
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3914
3915
        // offset: 20: size: variable; formula structure
3916 22
        $formulaStructure = substr($recordData, 20);
3917
3918
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
3919 22
        $options = self::getUInt2d($recordData, 14);
3920
3921
        // bit: 0; mask: 0x0001; 1 = recalculate always
3922
        // bit: 1; mask: 0x0002; 1 = calculate on open
3923
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
3924 22
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
3925
3926
        // WARNING:
3927
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
3928
        // the formula data may be ordinary formula data, therefore we need to check
3929
        // explicitly for the tExp token (0x01)
3930 22
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
3931
3932 22
        if ($isPartOfSharedFormula) {
3933
            // part of shared formula which means there will be a formula with a tExp token and nothing else
3934
            // get the base cell, grab tExp token
3935
            $baseRow = self::getUInt2d($formulaStructure, 3);
3936
            $baseCol = self::getUInt2d($formulaStructure, 5);
3937
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
3938
        }
3939
3940
        // Read cell?
3941 22
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3942 22
            if ($isPartOfSharedFormula) {
3943
                // formula is added to this cell after the sheet has been read
3944
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
3945
            }
3946
3947
            // offset: 16: size: 4; not used
3948
3949
            // offset: 4; size: 2; XF index
3950 22
            $xfIndex = self::getUInt2d($recordData, 4);
3951
3952
            // offset: 6; size: 8; result of the formula
3953 22
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
3954
                // String formula. Result follows in appended STRING record
3955 2
                $dataType = DataType::TYPE_STRING;
3956
3957
                // read possible SHAREDFMLA record
3958 2
                $code = self::getUInt2d($this->data, $this->pos);
3959 2
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
3960
                    $this->readSharedFmla();
3961
                }
3962
3963
                // read STRING record
3964 2
                $value = $this->readString();
3965
            } elseif (
3966 20
                (ord($recordData[6]) == 1)
3967 20
                && (ord($recordData[12]) == 255)
3968 20
                && (ord($recordData[13]) == 255)
3969
            ) {
3970
                // Boolean formula. Result is in +2; 0=false, 1=true
3971
                $dataType = DataType::TYPE_BOOL;
3972
                $value = (bool) ord($recordData[8]);
3973
            } elseif (
3974 20
                (ord($recordData[6]) == 2)
3975 20
                && (ord($recordData[12]) == 255)
3976 20
                && (ord($recordData[13]) == 255)
3977
            ) {
3978
                // Error formula. Error code is in +2
3979 8
                $dataType = DataType::TYPE_ERROR;
3980 8
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
3981
            } elseif (
3982 20
                (ord($recordData[6]) == 3)
3983 20
                && (ord($recordData[12]) == 255)
3984 20
                && (ord($recordData[13]) == 255)
3985
            ) {
3986
                // Formula result is a null string
3987 1
                $dataType = DataType::TYPE_NULL;
3988 1
                $value = '';
3989
            } else {
3990
                // forumla result is a number, first 14 bytes like _NUMBER record
3991 20
                $dataType = DataType::TYPE_NUMERIC;
3992 20
                $value = self::extractNumber(substr($recordData, 6, 8));
3993
            }
3994
3995 22
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3996 22
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3997
                // add cell style
3998 21
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3999
            }
4000
4001
            // store the formula
4002 22
            if (!$isPartOfSharedFormula) {
4003
                // not part of shared formula
4004
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
4005
                try {
4006 22
                    if ($this->version != self::XLS_BIFF8) {
4007 1
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
4008
                    }
4009 21
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
4010 21
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
4011 1
                } catch (PhpSpreadsheetException $e) {
4012 22
                    $cell->setValueExplicit($value, $dataType);
4013
                }
4014
            } else {
4015
                if ($this->version == self::XLS_BIFF8) {
4016
                    // do nothing at this point, formula id added later in the code
4017
                } else {
4018
                    $cell->setValueExplicit($value, $dataType);
4019
                }
4020
            }
4021
4022
            // store the cached calculated value
4023 22
            $cell->setCalculatedValue($value);
4024
        }
4025
    }
4026
4027
    /**
4028
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
4029
     * which usually contains relative references.
4030
     * These will be used to construct the formula in each shared formula part after the sheet is read.
4031
     */
4032
    private function readSharedFmla(): void
4033
    {
4034
        $length = self::getUInt2d($this->data, $this->pos + 2);
4035
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4036
4037
        // move stream pointer to next record
4038
        $this->pos += 4 + $length;
4039
4040
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
4041
        $cellRange = substr($recordData, 0, 6);
4042
        $cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
4043
4044
        // offset: 6, size: 1; not used
4045
4046
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
4047
        $no = ord($recordData[7]);
4048
4049
        // offset: 8, size: var; Binary token array of the shared formula
4050
        $formula = substr($recordData, 8);
4051
4052
        // at this point we only store the shared formula for later use
4053
        $this->sharedFormulas[$this->baseCell] = $formula;
4054
    }
4055
4056
    /**
4057
     * Read a STRING record from current stream position and advance the stream pointer to next record
4058
     * This record is used for storing result from FORMULA record when it is a string, and
4059
     * it occurs directly after the FORMULA record.
4060
     *
4061
     * @return string The string contents as UTF-8
4062
     */
4063 2
    private function readString()
4064
    {
4065 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
4066 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4067
4068
        // move stream pointer to next record
4069 2
        $this->pos += 4 + $length;
4070
4071 2
        if ($this->version == self::XLS_BIFF8) {
4072 2
            $string = self::readUnicodeStringLong($recordData);
4073 2
            $value = $string['value'];
4074
        } else {
4075
            $string = $this->readByteStringLong($recordData);
4076
            $value = $string['value'];
4077
        }
4078
4079 2
        return $value;
4080
    }
4081
4082
    /**
4083
     * Read BOOLERR record
4084
     * This record represents a Boolean value or error value
4085
     * cell.
4086
     *
4087
     * --    "OpenOffice.org's Documentation of the Microsoft
4088
     *         Excel File Format"
4089
     */
4090 9
    private function readBoolErr(): void
4091
    {
4092 9
        $length = self::getUInt2d($this->data, $this->pos + 2);
4093 9
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4094
4095
        // move stream pointer to next record
4096 9
        $this->pos += 4 + $length;
4097
4098
        // offset: 0; size: 2; row index
4099 9
        $row = self::getUInt2d($recordData, 0);
4100
4101
        // offset: 2; size: 2; column index
4102 9
        $column = self::getUInt2d($recordData, 2);
4103 9
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4104
4105
        // Read cell?
4106 9
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4107
            // offset: 4; size: 2; index to XF record
4108 9
            $xfIndex = self::getUInt2d($recordData, 4);
4109
4110
            // offset: 6; size: 1; the boolean value or error value
4111 9
            $boolErr = ord($recordData[6]);
4112
4113
            // offset: 7; size: 1; 0=boolean; 1=error
4114 9
            $isError = ord($recordData[7]);
4115
4116 9
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4117
            switch ($isError) {
4118 9
                case 0: // boolean
4119 9
                    $value = (bool) $boolErr;
4120
4121
                    // add cell value
4122 9
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
4123
4124 9
                    break;
4125
                case 1: // error type
4126
                    $value = Xls\ErrorCode::lookup($boolErr);
4127
4128
                    // add cell value
4129
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
4130
4131
                    break;
4132
            }
4133
4134 9
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4135
                // add cell style
4136 8
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4137
            }
4138
        }
4139
    }
4140
4141
    /**
4142
     * Read MULBLANK record
4143
     * This record represents a cell range of empty cells. All
4144
     * cells are located in the same row.
4145
     *
4146
     * --    "OpenOffice.org's Documentation of the Microsoft
4147
     *         Excel File Format"
4148
     */
4149 24
    private function readMulBlank(): void
4150
    {
4151 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
4152 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4153
4154
        // move stream pointer to next record
4155 24
        $this->pos += 4 + $length;
4156
4157
        // offset: 0; size: 2; index to row
4158 24
        $row = self::getUInt2d($recordData, 0);
4159
4160
        // offset: 2; size: 2; index to first column
4161 24
        $fc = self::getUInt2d($recordData, 2);
4162
4163
        // offset: 4; size: 2 x nc; list of indexes to XF records
4164
        // add style information
4165 24
        if (!$this->readDataOnly && $this->readEmptyCells) {
4166 23
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
4167 23
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
4168
4169
                // Read cell?
4170 23
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4171 23
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
4172 23
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
4173 23
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4174
                    }
4175
                }
4176
            }
4177
        }
4178
4179
        // offset: 6; size 2; index to last column (not needed)
4180
    }
4181
4182
    /**
4183
     * Read LABEL record
4184
     * This record represents a cell that contains a string. In
4185
     * BIFF8 it is usually replaced by the LABELSST record.
4186
     * Excel still uses this record, if it copies unformatted
4187
     * text cells to the clipboard.
4188
     *
4189
     * --    "OpenOffice.org's Documentation of the Microsoft
4190
     *         Excel File Format"
4191
     */
4192 2
    private function readLabel(): void
4193
    {
4194 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
4195 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4196
4197
        // move stream pointer to next record
4198 2
        $this->pos += 4 + $length;
4199
4200
        // offset: 0; size: 2; index to row
4201 2
        $row = self::getUInt2d($recordData, 0);
4202
4203
        // offset: 2; size: 2; index to column
4204 2
        $column = self::getUInt2d($recordData, 2);
4205 2
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4206
4207
        // Read cell?
4208 2
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4209
            // offset: 4; size: 2; XF index
4210 2
            $xfIndex = self::getUInt2d($recordData, 4);
4211
4212
            // add cell value
4213
            // todo: what if string is very long? continue record
4214 2
            if ($this->version == self::XLS_BIFF8) {
4215 1
                $string = self::readUnicodeStringLong(substr($recordData, 6));
4216 1
                $value = $string['value'];
4217
            } else {
4218 1
                $string = $this->readByteStringLong(substr($recordData, 6));
4219 1
                $value = $string['value'];
4220
            }
4221 2
            if ($this->readEmptyCells || trim($value) !== '') {
4222 2
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4223 2
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4224
4225 2
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4226
                    // add cell style
4227 2
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4228
                }
4229
            }
4230
        }
4231
    }
4232
4233
    /**
4234
     * Read BLANK record.
4235
     */
4236 21
    private function readBlank(): void
4237
    {
4238 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
4239 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4240
4241
        // move stream pointer to next record
4242 21
        $this->pos += 4 + $length;
4243
4244
        // offset: 0; size: 2; row index
4245 21
        $row = self::getUInt2d($recordData, 0);
4246
4247
        // offset: 2; size: 2; col index
4248 21
        $col = self::getUInt2d($recordData, 2);
4249 21
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
4250
4251
        // Read cell?
4252 21
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4253
            // offset: 4; size: 2; XF index
4254 21
            $xfIndex = self::getUInt2d($recordData, 4);
4255
4256
            // add style information
4257 21
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
4258 21
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4259
            }
4260
        }
4261
    }
4262
4263
    /**
4264
     * Read MSODRAWING record.
4265
     */
4266 16
    private function readMsoDrawing(): void
4267
    {
4268 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
4269
4270
        // get spliced record data
4271 16
        $splicedRecordData = $this->getSplicedRecordData();
4272 16
        $recordData = $splicedRecordData['recordData'];
4273
4274 16
        $this->drawingData .= $recordData;
4275
    }
4276
4277
    /**
4278
     * Read OBJ record.
4279
     */
4280 12
    private function readObj(): void
4281
    {
4282 12
        $length = self::getUInt2d($this->data, $this->pos + 2);
4283 12
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4284
4285
        // move stream pointer to next record
4286 12
        $this->pos += 4 + $length;
4287
4288 12
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
4289 1
            return;
4290
        }
4291
4292
        // recordData consists of an array of subrecords looking like this:
4293
        //    ft: 2 bytes; ftCmo type (0x15)
4294
        //    cb: 2 bytes; size in bytes of ftCmo data
4295
        //    ot: 2 bytes; Object Type
4296
        //    id: 2 bytes; Object id number
4297
        //    grbit: 2 bytes; Option Flags
4298
        //    data: var; subrecord data
4299
4300
        // for now, we are just interested in the second subrecord containing the object type
4301 11
        $ftCmoType = self::getUInt2d($recordData, 0);
4302 11
        $cbCmoSize = self::getUInt2d($recordData, 2);
4303 11
        $otObjType = self::getUInt2d($recordData, 4);
4304 11
        $idObjID = self::getUInt2d($recordData, 6);
4305 11
        $grbitOpts = self::getUInt2d($recordData, 6);
4306
4307 11
        $this->objs[] = [
4308 11
            'ftCmoType' => $ftCmoType,
4309 11
            'cbCmoSize' => $cbCmoSize,
4310 11
            'otObjType' => $otObjType,
4311 11
            'idObjID' => $idObjID,
4312 11
            'grbitOpts' => $grbitOpts,
4313 11
        ];
4314 11
        $this->textObjRef = $idObjID;
4315
    }
4316
4317
    /**
4318
     * Read WINDOW2 record.
4319
     */
4320 80
    private function readWindow2(): void
4321
    {
4322 80
        $length = self::getUInt2d($this->data, $this->pos + 2);
4323 80
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4324
4325
        // move stream pointer to next record
4326 80
        $this->pos += 4 + $length;
4327
4328
        // offset: 0; size: 2; option flags
4329 80
        $options = self::getUInt2d($recordData, 0);
4330
4331
        // offset: 2; size: 2; index to first visible row
4332 80
        $firstVisibleRow = self::getUInt2d($recordData, 2);
4333
4334
        // offset: 4; size: 2; index to first visible colum
4335 80
        $firstVisibleColumn = self::getUInt2d($recordData, 4);
4336 80
        $zoomscaleInPageBreakPreview = 0;
4337 80
        $zoomscaleInNormalView = 0;
4338 80
        if ($this->version === self::XLS_BIFF8) {
4339
            // offset:  8; size: 2; not used
4340
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
4341
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
4342
            // offset: 14; size: 4; not used
4343 79
            if (!isset($recordData[10])) {
4344
                $zoomscaleInPageBreakPreview = 0;
4345
            } else {
4346 79
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
4347
            }
4348
4349 79
            if ($zoomscaleInPageBreakPreview === 0) {
4350 76
                $zoomscaleInPageBreakPreview = 60;
4351
            }
4352
4353 79
            if (!isset($recordData[12])) {
4354
                $zoomscaleInNormalView = 0;
4355
            } else {
4356 79
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
4357
            }
4358
4359 79
            if ($zoomscaleInNormalView === 0) {
4360 34
                $zoomscaleInNormalView = 100;
4361
            }
4362
        }
4363
4364
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
4365 80
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
4366 80
        $this->phpSheet->setShowGridlines($showGridlines);
4367
4368
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
4369 80
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
4370 80
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
4371
4372
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
4373 80
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
4374
4375
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
4376 80
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
4377
4378
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
4379 80
        $isActive = (bool) ((0x0400 & $options) >> 10);
4380 80
        if ($isActive) {
4381 77
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
4382
        }
4383
4384
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
4385 80
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
4386
4387
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
4388
4389 80
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
4390
            //NOTE: this setting is inferior to page layout view(Excel2007-)
4391 80
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
4392 80
            $this->phpSheet->getSheetView()->setView($view);
4393 80
            if ($this->version === self::XLS_BIFF8) {
4394 79
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
4395 79
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
4396 79
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
4397
            }
4398
        }
4399
    }
4400
4401
    /**
4402
     * Read PLV Record(Created by Excel2007 or upper).
4403
     */
4404 53
    private function readPageLayoutView(): void
4405
    {
4406 53
        $length = self::getUInt2d($this->data, $this->pos + 2);
4407 53
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4408
4409
        // move stream pointer to next record
4410 53
        $this->pos += 4 + $length;
4411
4412
        // offset: 0; size: 2; rt
4413
        //->ignore
4414 53
        $rt = self::getUInt2d($recordData, 0);
4415
        // offset: 2; size: 2; grbitfr
4416
        //->ignore
4417 53
        $grbitFrt = self::getUInt2d($recordData, 2);
4418
        // offset: 4; size: 8; reserved
4419
        //->ignore
4420
4421
        // offset: 12; size 2; zoom scale
4422 53
        $wScalePLV = self::getUInt2d($recordData, 12);
4423
        // offset: 14; size 2; grbit
4424 53
        $grbit = self::getUInt2d($recordData, 14);
4425
4426
        // decomprise grbit
4427 53
        $fPageLayoutView = $grbit & 0x01;
4428 53
        $fRulerVisible = ($grbit >> 1) & 0x01; //no support
4429 53
        $fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
4430
4431 53
        if ($fPageLayoutView === 1) {
4432
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
4433
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
4434
        }
4435
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
4436
    }
4437
4438
    /**
4439
     * Read SCL record.
4440
     */
4441 5
    private function readScl(): void
4442
    {
4443 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4444 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4445
4446
        // move stream pointer to next record
4447 5
        $this->pos += 4 + $length;
4448
4449
        // offset: 0; size: 2; numerator of the view magnification
4450 5
        $numerator = self::getUInt2d($recordData, 0);
4451
4452
        // offset: 2; size: 2; numerator of the view magnification
4453 5
        $denumerator = self::getUInt2d($recordData, 2);
4454
4455
        // set the zoom scale (in percent)
4456 5
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
4457
    }
4458
4459
    /**
4460
     * Read PANE record.
4461
     */
4462 8
    private function readPane(): void
4463
    {
4464 8
        $length = self::getUInt2d($this->data, $this->pos + 2);
4465 8
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4466
4467
        // move stream pointer to next record
4468 8
        $this->pos += 4 + $length;
4469
4470 8
        if (!$this->readDataOnly) {
4471
            // offset: 0; size: 2; position of vertical split
4472 8
            $px = self::getUInt2d($recordData, 0);
4473
4474
            // offset: 2; size: 2; position of horizontal split
4475 8
            $py = self::getUInt2d($recordData, 2);
4476
4477
            // offset: 4; size: 2; top most visible row in the bottom pane
4478 8
            $rwTop = self::getUInt2d($recordData, 4);
4479
4480
            // offset: 6; size: 2; first visible left column in the right pane
4481 8
            $colLeft = self::getUInt2d($recordData, 6);
4482
4483 8
            if ($this->frozen) {
4484
                // frozen panes
4485 8
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
4486 8
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
4487 8
                $this->phpSheet->freezePane($cell, $topLeftCell);
4488
            }
4489
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
4490
        }
4491
    }
4492
4493
    /**
4494
     * Read SELECTION record. There is one such record for each pane in the sheet.
4495
     */
4496 77
    private function readSelection(): void
4497
    {
4498 77
        $length = self::getUInt2d($this->data, $this->pos + 2);
4499 77
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4500
4501
        // move stream pointer to next record
4502 77
        $this->pos += 4 + $length;
4503
4504 77
        if (!$this->readDataOnly) {
4505
            // offset: 0; size: 1; pane identifier
4506 76
            $paneId = ord($recordData[0]);
4507
4508
            // offset: 1; size: 2; index to row of the active cell
4509 76
            $r = self::getUInt2d($recordData, 1);
4510
4511
            // offset: 3; size: 2; index to column of the active cell
4512 76
            $c = self::getUInt2d($recordData, 3);
4513
4514
            // offset: 5; size: 2; index into the following cell range list to the
4515
            //  entry that contains the active cell
4516 76
            $index = self::getUInt2d($recordData, 5);
4517
4518
            // offset: 7; size: var; cell range address list containing all selected cell ranges
4519 76
            $data = substr($recordData, 7);
4520 76
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
4521
4522 76
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
4523
4524
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
4525 76
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
4526
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
4527
            }
4528
4529
            // first row '1' + last row '65536' indicates that full column is selected
4530 76
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
4531
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
4532
            }
4533
4534
            // first column 'A' + last column 'IV' indicates that full row is selected
4535 76
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
4536 2
                $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
4537
            }
4538
4539 76
            $this->phpSheet->setSelectedCells($selectedCells);
4540
        }
4541
    }
4542
4543 17
    private function includeCellRangeFiltered($cellRangeAddress)
4544
    {
4545 17
        $includeCellRange = true;
4546 17
        if ($this->getReadFilter() !== null) {
4547 17
            $includeCellRange = false;
4548 17
            $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
4549 17
            ++$rangeBoundaries[1][0];
4550 17
            for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
4551 17
                for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; ++$column) {
4552 17
                    if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
4553 17
                        $includeCellRange = true;
4554
4555 17
                        break 2;
4556
                    }
4557
                }
4558
            }
4559
        }
4560
4561 17
        return $includeCellRange;
4562
    }
4563
4564
    /**
4565
     * MERGEDCELLS.
4566
     *
4567
     * This record contains the addresses of merged cell ranges
4568
     * in the current sheet.
4569
     *
4570
     * --    "OpenOffice.org's Documentation of the Microsoft
4571
     *         Excel File Format"
4572
     */
4573 18
    private function readMergedCells(): void
4574
    {
4575 18
        $length = self::getUInt2d($this->data, $this->pos + 2);
4576 18
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4577
4578
        // move stream pointer to next record
4579 18
        $this->pos += 4 + $length;
4580
4581 18
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
4582 17
            $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData);
4583 17
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
4584
                if (
4585 17
                    (strpos($cellRangeAddress, ':') !== false) &&
4586 17
                    ($this->includeCellRangeFiltered($cellRangeAddress))
4587
                ) {
4588 17
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
4589
                }
4590
            }
4591
        }
4592
    }
4593
4594
    /**
4595
     * Read HYPERLINK record.
4596
     */
4597 4
    private function readHyperLink(): void
4598
    {
4599 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
4600 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4601
4602
        // move stream pointer forward to next record
4603 4
        $this->pos += 4 + $length;
4604
4605 4
        if (!$this->readDataOnly) {
4606
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
4607
            try {
4608 4
                $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData);
4609
            } catch (PhpSpreadsheetException $e) {
4610
                return;
4611
            }
4612
4613
            // offset: 8, size: 16; GUID of StdLink
4614
4615
            // offset: 24, size: 4; unknown value
4616
4617
            // offset: 28, size: 4; option flags
4618
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
4619 4
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
4620
4621
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
4622 4
            $isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
4623
4624
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
4625 4
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
4626
4627
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
4628 4
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
4629
4630
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
4631 4
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
4632
4633
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
4634 4
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
4635
4636
            // offset within record data
4637 4
            $offset = 32;
4638
4639 4
            if ($hasDesc) {
4640
                // offset: 32; size: var; character count of description text
4641 3
                $dl = self::getInt4d($recordData, 32);
4642
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
4643 3
                $desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
4644 3
                $offset += 4 + 2 * $dl;
4645
            }
4646 4
            if ($hasFrame) {
4647
                $fl = self::getInt4d($recordData, $offset);
4648
                $offset += 4 + 2 * $fl;
4649
            }
4650
4651
            // detect type of hyperlink (there are 4 types)
4652 4
            $hyperlinkType = null;
4653
4654 4
            if ($isUNC) {
4655
                $hyperlinkType = 'UNC';
4656 4
            } elseif (!$isFileLinkOrUrl) {
4657 3
                $hyperlinkType = 'workbook';
4658 4
            } elseif (ord($recordData[$offset]) == 0x03) {
4659
                $hyperlinkType = 'local';
4660 4
            } elseif (ord($recordData[$offset]) == 0xE0) {
4661 4
                $hyperlinkType = 'URL';
4662
            }
4663
4664
            switch ($hyperlinkType) {
4665 4
                case 'URL':
4666
                    // section 5.58.2: Hyperlink containing a URL
4667
                    // e.g. http://example.org/index.php
4668
4669
                    // offset: var; size: 16; GUID of URL Moniker
4670 4
                    $offset += 16;
4671
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
4672 4
                    $us = self::getInt4d($recordData, $offset);
4673 4
                    $offset += 4;
4674
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
4675 4
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
4676 4
                    $nullOffset = strpos($url, chr(0x00));
4677 4
                    if ($nullOffset) {
4678 3
                        $url = substr($url, 0, $nullOffset);
4679
                    }
4680 4
                    $url .= $hasText ? '#' : '';
4681 4
                    $offset += $us;
4682
4683 4
                    break;
4684 3
                case 'local':
4685
                    // section 5.58.3: Hyperlink to local file
4686
                    // examples:
4687
                    //   mydoc.txt
4688
                    //   ../../somedoc.xls#Sheet!A1
4689
4690
                    // offset: var; size: 16; GUI of File Moniker
4691
                    $offset += 16;
4692
4693
                    // offset: var; size: 2; directory up-level count.
4694
                    $upLevelCount = self::getUInt2d($recordData, $offset);
4695
                    $offset += 2;
4696
4697
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
4698
                    $sl = self::getInt4d($recordData, $offset);
4699
                    $offset += 4;
4700
4701
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
4702
                    $shortenedFilePath = substr($recordData, $offset, $sl);
4703
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
4704
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
4705
4706
                    $offset += $sl;
4707
4708
                    // offset: var; size: 24; unknown sequence
4709
                    $offset += 24;
4710
4711
                    // extended file path
4712
                    // offset: var; size: 4; size of the following file link field including string lenth mark
4713
                    $sz = self::getInt4d($recordData, $offset);
4714
                    $offset += 4;
4715
4716
                    // only present if $sz > 0
4717
                    if ($sz > 0) {
4718
                        // offset: var; size: 4; size of the character array of the extended file path and name
4719
                        $xl = self::getInt4d($recordData, $offset);
4720
                        $offset += 4;
4721
4722
                        // offset: var; size 2; unknown
4723
                        $offset += 2;
4724
4725
                        // offset: var; size $xl; character array of the extended file path and name.
4726
                        $extendedFilePath = substr($recordData, $offset, $xl);
4727
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
4728
                        $offset += $xl;
4729
                    }
4730
4731
                    // construct the path
4732
                    $url = str_repeat('..\\', $upLevelCount);
4733
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
4734
                    $url .= $hasText ? '#' : '';
4735
4736
                    break;
4737 3
                case 'UNC':
4738
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
4739
                    // todo: implement
4740
                    return;
4741 3
                case 'workbook':
4742
                    // section 5.58.5: Hyperlink to the Current Workbook
4743
                    // e.g. Sheet2!B1:C2, stored in text mark field
4744 3
                    $url = 'sheet://';
4745
4746 3
                    break;
4747
                default:
4748
                    return;
4749
            }
4750
4751 4
            if ($hasText) {
4752
                // offset: var; size: 4; character count of text mark including trailing zero word
4753 3
                $tl = self::getInt4d($recordData, $offset);
4754 3
                $offset += 4;
4755
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
4756 3
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4757 3
                $url .= $text;
4758
            }
4759
4760
            // apply the hyperlink to all the relevant cells
4761 4
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
4762 4
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
4763
            }
4764
        }
4765
    }
4766
4767
    /**
4768
     * Read DATAVALIDATIONS record.
4769
     */
4770 2
    private function readDataValidations(): void
4771
    {
4772 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
4773 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4774
4775
        // move stream pointer forward to next record
4776 2
        $this->pos += 4 + $length;
4777
    }
4778
4779
    /**
4780
     * Read DATAVALIDATION record.
4781
     */
4782 2
    private function readDataValidation(): void
4783
    {
4784 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
4785 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4786
4787
        // move stream pointer forward to next record
4788 2
        $this->pos += 4 + $length;
4789
4790 2
        if ($this->readDataOnly) {
4791
            return;
4792
        }
4793
4794
        // offset: 0; size: 4; Options
4795 2
        $options = self::getInt4d($recordData, 0);
4796
4797
        // bit: 0-3; mask: 0x0000000F; type
4798 2
        $type = (0x0000000F & $options) >> 0;
4799 2
        $type = Xls\DataValidationHelper::type($type);
4800
4801
        // bit: 4-6; mask: 0x00000070; error type
4802 2
        $errorStyle = (0x00000070 & $options) >> 4;
4803 2
        $errorStyle = Xls\DataValidationHelper::errorStyle($errorStyle);
4804
4805
        // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
4806
        // I have only seen cases where this is 1
4807 2
        $explicitFormula = (0x00000080 & $options) >> 7;
4808
4809
        // bit: 8; mask: 0x00000100; 1= empty cells allowed
4810 2
        $allowBlank = (0x00000100 & $options) >> 8;
4811
4812
        // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
4813 2
        $suppressDropDown = (0x00000200 & $options) >> 9;
4814
4815
        // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
4816 2
        $showInputMessage = (0x00040000 & $options) >> 18;
4817
4818
        // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
4819 2
        $showErrorMessage = (0x00080000 & $options) >> 19;
4820
4821
        // bit: 20-23; mask: 0x00F00000; condition operator
4822 2
        $operator = (0x00F00000 & $options) >> 20;
4823 2
        $operator = Xls\DataValidationHelper::operator($operator);
4824
4825 2
        if ($type === null || $errorStyle === null || $operator === null) {
4826
            return;
4827
        }
4828
4829
        // offset: 4; size: var; title of the prompt box
4830 2
        $offset = 4;
4831 2
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4832 2
        $promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4833 2
        $offset += $string['size'];
4834
4835
        // offset: var; size: var; title of the error box
4836 2
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4837 2
        $errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4838 2
        $offset += $string['size'];
4839
4840
        // offset: var; size: var; text of the prompt box
4841 2
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4842 2
        $prompt = $string['value'] !== chr(0) ? $string['value'] : '';
4843 2
        $offset += $string['size'];
4844
4845
        // offset: var; size: var; text of the error box
4846 2
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4847 2
        $error = $string['value'] !== chr(0) ? $string['value'] : '';
4848 2
        $offset += $string['size'];
4849
4850
        // offset: var; size: 2; size of the formula data for the first condition
4851 2
        $sz1 = self::getUInt2d($recordData, $offset);
4852 2
        $offset += 2;
4853
4854
        // offset: var; size: 2; not used
4855 2
        $offset += 2;
4856
4857
        // offset: var; size: $sz1; formula data for first condition (without size field)
4858 2
        $formula1 = substr($recordData, $offset, $sz1);
4859 2
        $formula1 = pack('v', $sz1) . $formula1; // prepend the length
4860
4861
        try {
4862 2
            $formula1 = $this->getFormulaFromStructure($formula1);
4863
4864
            // in list type validity, null characters are used as item separators
4865 2
            if ($type == DataValidation::TYPE_LIST) {
4866 2
                $formula1 = str_replace(chr(0), ',', $formula1);
4867
            }
4868
        } catch (PhpSpreadsheetException $e) {
4869
            return;
4870
        }
4871 2
        $offset += $sz1;
4872
4873
        // offset: var; size: 2; size of the formula data for the first condition
4874 2
        $sz2 = self::getUInt2d($recordData, $offset);
4875 2
        $offset += 2;
4876
4877
        // offset: var; size: 2; not used
4878 2
        $offset += 2;
4879
4880
        // offset: var; size: $sz2; formula data for second condition (without size field)
4881 2
        $formula2 = substr($recordData, $offset, $sz2);
4882 2
        $formula2 = pack('v', $sz2) . $formula2; // prepend the length
4883
4884
        try {
4885 2
            $formula2 = $this->getFormulaFromStructure($formula2);
4886
        } catch (PhpSpreadsheetException $e) {
4887
            return;
4888
        }
4889 2
        $offset += $sz2;
4890
4891
        // offset: var; size: var; cell range address list with
4892 2
        $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset));
4893 2
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
4894
4895 2
        foreach ($cellRangeAddresses as $cellRange) {
4896 2
            $stRange = $this->phpSheet->shrinkRangeToFit($cellRange);
4897 2
            foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
4898 2
                $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation();
4899 2
                $objValidation->setType($type);
4900 2
                $objValidation->setErrorStyle($errorStyle);
4901 2
                $objValidation->setAllowBlank((bool) $allowBlank);
4902 2
                $objValidation->setShowInputMessage((bool) $showInputMessage);
4903 2
                $objValidation->setShowErrorMessage((bool) $showErrorMessage);
4904 2
                $objValidation->setShowDropDown(!$suppressDropDown);
4905 2
                $objValidation->setOperator($operator);
4906 2
                $objValidation->setErrorTitle($errorTitle);
4907 2
                $objValidation->setError($error);
4908 2
                $objValidation->setPromptTitle($promptTitle);
4909 2
                $objValidation->setPrompt($prompt);
4910 2
                $objValidation->setFormula1($formula1);
4911 2
                $objValidation->setFormula2($formula2);
4912
            }
4913
        }
4914
    }
4915
4916
    /**
4917
     * Read SHEETLAYOUT record. Stores sheet tab color information.
4918
     */
4919 5
    private function readSheetLayout(): void
4920
    {
4921 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4922 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4923
4924
        // move stream pointer to next record
4925 5
        $this->pos += 4 + $length;
4926
4927
        // local pointer in record data
4928 5
        $offset = 0;
4929
4930 5
        if (!$this->readDataOnly) {
4931
            // offset: 0; size: 2; repeated record identifier 0x0862
4932
4933
            // offset: 2; size: 10; not used
4934
4935
            // offset: 12; size: 4; size of record data
4936
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
4937 5
            $sz = self::getInt4d($recordData, 12);
4938
4939
            switch ($sz) {
4940 5
                case 0x14:
4941
                    // offset: 16; size: 2; color index for sheet tab
4942 1
                    $colorIndex = self::getUInt2d($recordData, 16);
4943 1
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
4944 1
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
4945
4946 1
                    break;
4947 4
                case 0x28:
4948
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
4949 4
                    return;
4950
            }
4951
        }
4952
    }
4953
4954
    /**
4955
     * Read SHEETPROTECTION record (FEATHEADR).
4956
     */
4957 63
    private function readSheetProtection(): void
4958
    {
4959 63
        $length = self::getUInt2d($this->data, $this->pos + 2);
4960 63
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4961
4962
        // move stream pointer to next record
4963 63
        $this->pos += 4 + $length;
4964
4965 63
        if ($this->readDataOnly) {
4966
            return;
4967
        }
4968
4969
        // offset: 0; size: 2; repeated record header
4970
4971
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
4972
4973
        // offset: 4; size: 8; Currently not used and set to 0
4974
4975
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
4976 63
        $isf = self::getUInt2d($recordData, 12);
4977 63
        if ($isf != 2) {
4978
            return;
4979
        }
4980
4981
        // offset: 14; size: 1; =1 since this is a feat header
4982
4983
        // offset: 15; size: 4; size of rgbHdrSData
4984
4985
        // rgbHdrSData, assume "Enhanced Protection"
4986
        // offset: 19; size: 2; option flags
4987 63
        $options = self::getUInt2d($recordData, 19);
4988
4989
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
4990
        // Note - do not negate $bool
4991 63
        $bool = (0x0001 & $options) >> 0;
4992 63
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
4993
4994
        // bit: 1; mask 0x0002; edit scenarios
4995
        // Note - do not negate $bool
4996 63
        $bool = (0x0002 & $options) >> 1;
4997 63
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
4998
4999
        // bit: 2; mask 0x0004; format cells
5000 63
        $bool = (0x0004 & $options) >> 2;
5001 63
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
5002
5003
        // bit: 3; mask 0x0008; format columns
5004 63
        $bool = (0x0008 & $options) >> 3;
5005 63
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
5006
5007
        // bit: 4; mask 0x0010; format rows
5008 63
        $bool = (0x0010 & $options) >> 4;
5009 63
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
5010
5011
        // bit: 5; mask 0x0020; insert columns
5012 63
        $bool = (0x0020 & $options) >> 5;
5013 63
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
5014
5015
        // bit: 6; mask 0x0040; insert rows
5016 63
        $bool = (0x0040 & $options) >> 6;
5017 63
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
5018
5019
        // bit: 7; mask 0x0080; insert hyperlinks
5020 63
        $bool = (0x0080 & $options) >> 7;
5021 63
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
5022
5023
        // bit: 8; mask 0x0100; delete columns
5024 63
        $bool = (0x0100 & $options) >> 8;
5025 63
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
5026
5027
        // bit: 9; mask 0x0200; delete rows
5028 63
        $bool = (0x0200 & $options) >> 9;
5029 63
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
5030
5031
        // bit: 10; mask 0x0400; select locked cells
5032
        // Note that this is opposite of most of above.
5033 63
        $bool = (0x0400 & $options) >> 10;
5034 63
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
5035
5036
        // bit: 11; mask 0x0800; sort cell range
5037 63
        $bool = (0x0800 & $options) >> 11;
5038 63
        $this->phpSheet->getProtection()->setSort(!$bool);
5039
5040
        // bit: 12; mask 0x1000; auto filter
5041 63
        $bool = (0x1000 & $options) >> 12;
5042 63
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
5043
5044
        // bit: 13; mask 0x2000; pivot tables
5045 63
        $bool = (0x2000 & $options) >> 13;
5046 63
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
5047
5048
        // bit: 14; mask 0x4000; select unlocked cells
5049
        // Note that this is opposite of most of above.
5050 63
        $bool = (0x4000 & $options) >> 14;
5051 63
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
5052
5053
        // offset: 21; size: 2; not used
5054
    }
5055
5056
    /**
5057
     * Read RANGEPROTECTION record
5058
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
5059
     * where it is referred to as FEAT record.
5060
     */
5061 1
    private function readRangeProtection(): void
5062
    {
5063 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
5064 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5065
5066
        // move stream pointer to next record
5067 1
        $this->pos += 4 + $length;
5068
5069
        // local pointer in record data
5070 1
        $offset = 0;
5071
5072 1
        if (!$this->readDataOnly) {
5073 1
            $offset += 12;
5074
5075
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
5076 1
            $isf = self::getUInt2d($recordData, 12);
5077 1
            if ($isf != 2) {
5078
                // we only read FEAT records of type 2
5079
                return;
5080
            }
5081 1
            $offset += 2;
5082
5083 1
            $offset += 5;
5084
5085
            // offset: 19; size: 2; count of ref ranges this feature is on
5086 1
            $cref = self::getUInt2d($recordData, 19);
5087 1
            $offset += 2;
5088
5089 1
            $offset += 6;
5090
5091
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
5092 1
            $cellRanges = [];
5093 1
            for ($i = 0; $i < $cref; ++$i) {
5094
                try {
5095 1
                    $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
5096
                } catch (PhpSpreadsheetException $e) {
5097
                    return;
5098
                }
5099 1
                $cellRanges[] = $cellRange;
5100 1
                $offset += 8;
5101
            }
5102
5103
            // offset: var; size: var; variable length of feature specific data
5104 1
            $rgbFeat = substr($recordData, $offset);
5105 1
            $offset += 4;
5106
5107
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
5108 1
            $wPassword = self::getInt4d($recordData, $offset);
5109 1
            $offset += 4;
5110
5111
            // Apply range protection to sheet
5112 1
            if ($cellRanges) {
5113 1
                $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
5114
            }
5115
        }
5116
    }
5117
5118
    /**
5119
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
5120
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
5121
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
5122
     */
5123 1
    private function readContinue(): void
5124
    {
5125 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
5126 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5127
5128
        // check if we are reading drawing data
5129
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
5130 1
        if ($this->drawingData == '') {
5131
            // move stream pointer to next record
5132 1
            $this->pos += 4 + $length;
5133
5134 1
            return;
5135
        }
5136
5137
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
5138
        if ($length < 4) {
5139
            // move stream pointer to next record
5140
            $this->pos += 4 + $length;
5141
5142
            return;
5143
        }
5144
5145
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
5146
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
5147
        // we know that Escher stream may be split at least at
5148
        //        0xF003 MsofbtSpgrContainer
5149
        //        0xF004 MsofbtSpContainer
5150
        //        0xF00D MsofbtClientTextbox
5151
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
5152
5153
        $splitPoint = self::getUInt2d($recordData, 2);
5154
        if (in_array($splitPoint, $validSplitPoints)) {
5155
            // get spliced record data (and move pointer to next record)
5156
            $splicedRecordData = $this->getSplicedRecordData();
5157
            $this->drawingData .= $splicedRecordData['recordData'];
5158
5159
            return;
5160
        }
5161
5162
        // move stream pointer to next record
5163
        $this->pos += 4 + $length;
5164
    }
5165
5166
    /**
5167
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
5168
     * records are found. Splices the record data pieces and returns the combined string as if record data
5169
     * is in one piece.
5170
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
5171
     *
5172
     * @return array
5173
     */
5174 78
    private function getSplicedRecordData()
5175
    {
5176 78
        $data = '';
5177 78
        $spliceOffsets = [];
5178
5179 78
        $i = 0;
5180 78
        $spliceOffsets[0] = 0;
5181
5182
        do {
5183 78
            ++$i;
5184
5185
            // offset: 0; size: 2; identifier
5186 78
            $identifier = self::getUInt2d($this->data, $this->pos);
5187
            // offset: 2; size: 2; length
5188 78
            $length = self::getUInt2d($this->data, $this->pos + 2);
5189 78
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
5190
5191 78
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
5192
5193 78
            $this->pos += 4 + $length;
5194 78
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
5195 78
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
5196
5197 78
        return [
5198 78
            'recordData' => $data,
5199 78
            'spliceOffsets' => $spliceOffsets,
5200 78
        ];
5201
    }
5202
5203
    /**
5204
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
5205
     *
5206
     * @param string $formulaStructure The complete binary data for the formula
5207
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5208
     *
5209
     * @return string Human readable formula
5210
     */
5211 38
    private function getFormulaFromStructure($formulaStructure, $baseCell = 'A1')
5212
    {
5213
        // offset: 0; size: 2; size of the following formula data
5214 38
        $sz = self::getUInt2d($formulaStructure, 0);
5215
5216
        // offset: 2; size: sz
5217 38
        $formulaData = substr($formulaStructure, 2, $sz);
5218
5219
        // offset: 2 + sz; size: variable (optional)
5220 38
        if (strlen($formulaStructure) > 2 + $sz) {
5221
            $additionalData = substr($formulaStructure, 2 + $sz);
5222
        } else {
5223 38
            $additionalData = '';
5224
        }
5225
5226 38
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
5227
    }
5228
5229
    /**
5230
     * Take formula data and additional data for formula and return human readable formula.
5231
     *
5232
     * @param string $formulaData The binary data for the formula itself
5233
     * @param string $additionalData Additional binary data going with the formula
5234
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5235
     *
5236
     * @return string Human readable formula
5237
     */
5238 38
    private function getFormulaFromData($formulaData, $additionalData = '', $baseCell = 'A1')
5239
    {
5240
        // start parsing the formula data
5241 38
        $tokens = [];
5242
5243 38
        while (strlen($formulaData) > 0 && $token = $this->getNextToken($formulaData, $baseCell)) {
5244 38
            $tokens[] = $token;
5245 38
            $formulaData = substr($formulaData, $token['size']);
5246
        }
5247
5248 38
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
5249
5250 38
        return $formulaString;
5251
    }
5252
5253
    /**
5254
     * Take array of tokens together with additional data for formula and return human readable formula.
5255
     *
5256
     * @param array $tokens
5257
     * @param string $additionalData Additional binary data going with the formula
5258
     *
5259
     * @return string Human readable formula
5260
     */
5261 38
    private function createFormulaFromTokens($tokens, $additionalData)
5262
    {
5263
        // empty formula?
5264 38
        if (empty($tokens)) {
5265 2
            return '';
5266
        }
5267
5268 38
        $formulaStrings = [];
5269 38
        foreach ($tokens as $token) {
5270
            // initialize spaces
5271 38
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
5272 38
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
5273 38
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
5274 38
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
5275 38
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
5276 38
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
5277
5278 38
            switch ($token['name']) {
5279 38
                case 'tAdd': // addition
5280 38
                case 'tConcat': // addition
5281 38
                case 'tDiv': // division
5282 38
                case 'tEQ': // equality
5283 38
                case 'tGE': // greater than or equal
5284 38
                case 'tGT': // greater than
5285 38
                case 'tIsect': // intersection
5286 38
                case 'tLE': // less than or equal
5287 38
                case 'tList': // less than or equal
5288 38
                case 'tLT': // less than
5289 38
                case 'tMul': // multiplication
5290 38
                case 'tNE': // multiplication
5291 38
                case 'tPower': // power
5292 38
                case 'tRange': // range
5293 38
                case 'tSub': // subtraction
5294 24
                    $op2 = array_pop($formulaStrings);
5295 24
                    $op1 = array_pop($formulaStrings);
5296 24
                    $formulaStrings[] = "$op1$space1$space0{$token['data']}$op2";
5297 24
                    unset($space0, $space1);
5298
5299 24
                    break;
5300 38
                case 'tUplus': // unary plus
5301 38
                case 'tUminus': // unary minus
5302
                    $op = array_pop($formulaStrings);
5303
                    $formulaStrings[] = "$space1$space0{$token['data']}$op";
5304
                    unset($space0, $space1);
5305
5306
                    break;
5307 38
                case 'tPercent': // percent sign
5308
                    $op = array_pop($formulaStrings);
5309
                    $formulaStrings[] = "$op$space1$space0{$token['data']}";
5310
                    unset($space0, $space1);
5311
5312
                    break;
5313 38
                case 'tAttrVolatile': // indicates volatile function
5314 38
                case 'tAttrIf':
5315 38
                case 'tAttrSkip':
5316 38
                case 'tAttrChoose':
5317
                    // token is only important for Excel formula evaluator
5318
                    // do nothing
5319
                    break;
5320 38
                case 'tAttrSpace': // space / carriage return
5321
                    // space will be used when next token arrives, do not alter formulaString stack
5322
                    switch ($token['data']['spacetype']) {
5323
                        case 'type0':
5324
                            $space0 = str_repeat(' ', $token['data']['spacecount']);
5325
5326
                            break;
5327
                        case 'type1':
5328
                            $space1 = str_repeat("\n", $token['data']['spacecount']);
5329
5330
                            break;
5331
                        case 'type2':
5332
                            $space2 = str_repeat(' ', $token['data']['spacecount']);
5333
5334
                            break;
5335
                        case 'type3':
5336
                            $space3 = str_repeat("\n", $token['data']['spacecount']);
5337
5338
                            break;
5339
                        case 'type4':
5340
                            $space4 = str_repeat(' ', $token['data']['spacecount']);
5341
5342
                            break;
5343
                        case 'type5':
5344
                            $space5 = str_repeat("\n", $token['data']['spacecount']);
5345
5346
                            break;
5347
                    }
5348
5349
                    break;
5350 38
                case 'tAttrSum': // SUM function with one parameter
5351 10
                    $op = array_pop($formulaStrings);
5352 10
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
5353 10
                    unset($space0, $space1);
5354
5355 10
                    break;
5356 38
                case 'tFunc': // function with fixed number of arguments
5357 38
                case 'tFuncV': // function with variable number of arguments
5358 23
                    if ($token['data']['function'] != '') {
5359
                        // normal function
5360 23
                        $ops = []; // array of operators
5361 23
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
5362 15
                            $ops[] = array_pop($formulaStrings);
5363
                        }
5364 23
                        $ops = array_reverse($ops);
5365 23
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
5366 23
                        unset($space0, $space1);
5367
                    } else {
5368
                        // add-in function
5369
                        $ops = []; // array of operators
5370
                        for ($i = 0; $i < $token['data']['args'] - 1; ++$i) {
5371
                            $ops[] = array_pop($formulaStrings);
5372
                        }
5373
                        $ops = array_reverse($ops);
5374
                        $function = array_pop($formulaStrings);
5375
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
5376
                        unset($space0, $space1);
5377
                    }
5378
5379 23
                    break;
5380 38
                case 'tParen': // parenthesis
5381
                    $expression = array_pop($formulaStrings);
5382
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
5383
                    unset($space2, $space3, $space4, $space5);
5384
5385
                    break;
5386 38
                case 'tArray': // array constant
5387
                    $constantArray = self::readBIFF8ConstantArray($additionalData);
5388
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
5389
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
5390
                    unset($space0, $space1);
5391
5392
                    break;
5393 38
                case 'tMemArea':
5394
                    // bite off chunk of additional data
5395
                    $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData);
5396
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
5397
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5398
                    unset($space0, $space1);
5399
5400
                    break;
5401 38
                case 'tArea': // cell range address
5402 36
                case 'tBool': // boolean
5403 36
                case 'tErr': // error code
5404 35
                case 'tInt': // integer
5405 26
                case 'tMemErr':
5406 26
                case 'tMemFunc':
5407 26
                case 'tMissArg':
5408 26
                case 'tName':
5409 26
                case 'tNameX':
5410 26
                case 'tNum': // number
5411 26
                case 'tRef': // single cell reference
5412 22
                case 'tRef3d': // 3d cell reference
5413 20
                case 'tArea3d': // 3d cell range reference
5414 12
                case 'tRefN':
5415 12
                case 'tAreaN':
5416 12
                case 'tStr': // string
5417 38
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5418 38
                    unset($space0, $space1);
5419
5420 38
                    break;
5421
            }
5422
        }
5423 38
        $formulaString = $formulaStrings[0];
5424
5425 38
        return $formulaString;
5426
    }
5427
5428
    /**
5429
     * Fetch next token from binary formula data.
5430
     *
5431
     * @param string $formulaData Formula data
5432
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5433
     *
5434
     * @return array
5435
     */
5436 38
    private function getNextToken($formulaData, $baseCell = 'A1')
5437
    {
5438
        // offset: 0; size: 1; token id
5439 38
        $id = ord($formulaData[0]); // token id
5440 38
        $name = false; // initialize token name
5441
5442
        switch ($id) {
5443 38
            case 0x03:
5444 6
                $name = 'tAdd';
5445 6
                $size = 1;
5446 6
                $data = '+';
5447
5448 6
                break;
5449 38
            case 0x04:
5450 7
                $name = 'tSub';
5451 7
                $size = 1;
5452 7
                $data = '-';
5453
5454 7
                break;
5455 38
            case 0x05:
5456 3
                $name = 'tMul';
5457 3
                $size = 1;
5458 3
                $data = '*';
5459
5460 3
                break;
5461 38
            case 0x06:
5462 11
                $name = 'tDiv';
5463 11
                $size = 1;
5464 11
                $data = '/';
5465
5466 11
                break;
5467 38
            case 0x07:
5468
                $name = 'tPower';
5469
                $size = 1;
5470
                $data = '^';
5471
5472
                break;
5473 38
            case 0x08:
5474 2
                $name = 'tConcat';
5475 2
                $size = 1;
5476 2
                $data = '&';
5477
5478 2
                break;
5479 38
            case 0x09:
5480
                $name = 'tLT';
5481
                $size = 1;
5482
                $data = '<';
5483
5484
                break;
5485 38
            case 0x0A:
5486
                $name = 'tLE';
5487
                $size = 1;
5488
                $data = '<=';
5489
5490
                break;
5491 38
            case 0x0B:
5492 2
                $name = 'tEQ';
5493 2
                $size = 1;
5494 2
                $data = '=';
5495
5496 2
                break;
5497 38
            case 0x0C:
5498
                $name = 'tGE';
5499
                $size = 1;
5500
                $data = '>=';
5501
5502
                break;
5503 38
            case 0x0D:
5504
                $name = 'tGT';
5505
                $size = 1;
5506
                $data = '>';
5507
5508
                break;
5509 38
            case 0x0E:
5510 1
                $name = 'tNE';
5511 1
                $size = 1;
5512 1
                $data = '<>';
5513
5514 1
                break;
5515 38
            case 0x0F:
5516
                $name = 'tIsect';
5517
                $size = 1;
5518
                $data = ' ';
5519
5520
                break;
5521 38
            case 0x10:
5522 1
                $name = 'tList';
5523 1
                $size = 1;
5524 1
                $data = ',';
5525
5526 1
                break;
5527 38
            case 0x11:
5528
                $name = 'tRange';
5529
                $size = 1;
5530
                $data = ':';
5531
5532
                break;
5533 38
            case 0x12:
5534
                $name = 'tUplus';
5535
                $size = 1;
5536
                $data = '+';
5537
5538
                break;
5539 38
            case 0x13:
5540
                $name = 'tUminus';
5541
                $size = 1;
5542
                $data = '-';
5543
5544
                break;
5545 38
            case 0x14:
5546
                $name = 'tPercent';
5547
                $size = 1;
5548
                $data = '%';
5549
5550
                break;
5551 38
            case 0x15:    //    parenthesis
5552
                $name = 'tParen';
5553
                $size = 1;
5554
                $data = null;
5555
5556
                break;
5557 38
            case 0x16:    //    missing argument
5558
                $name = 'tMissArg';
5559
                $size = 1;
5560
                $data = '';
5561
5562
                break;
5563 38
            case 0x17:    //    string
5564 12
                $name = 'tStr';
5565
                // offset: 1; size: var; Unicode string, 8-bit string length
5566 12
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
5567 12
                $size = 1 + $string['size'];
5568 12
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
5569
5570 12
                break;
5571 38
            case 0x19:    //    Special attribute
5572
                // offset: 1; size: 1; attribute type flags:
5573 10
                switch (ord($formulaData[1])) {
5574 10
                    case 0x01:
5575
                        $name = 'tAttrVolatile';
5576
                        $size = 4;
5577
                        $data = null;
5578
5579
                        break;
5580 10
                    case 0x02:
5581
                        $name = 'tAttrIf';
5582
                        $size = 4;
5583
                        $data = null;
5584
5585
                        break;
5586 10
                    case 0x04:
5587
                        $name = 'tAttrChoose';
5588
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
5589
                        $nc = self::getUInt2d($formulaData, 2);
5590
                        // offset: 4; size: 2 * $nc
5591
                        // offset: 4 + 2 * $nc; size: 2
5592
                        $size = 2 * $nc + 6;
5593
                        $data = null;
5594
5595
                        break;
5596 10
                    case 0x08:
5597
                        $name = 'tAttrSkip';
5598
                        $size = 4;
5599
                        $data = null;
5600
5601
                        break;
5602 10
                    case 0x10:
5603 10
                        $name = 'tAttrSum';
5604 10
                        $size = 4;
5605 10
                        $data = null;
5606
5607 10
                        break;
5608
                    case 0x40:
5609
                    case 0x41:
5610
                        $name = 'tAttrSpace';
5611
                        $size = 4;
5612
                        // offset: 2; size: 2; space type and position
5613
                        switch (ord($formulaData[2])) {
5614
                            case 0x00:
5615
                                $spacetype = 'type0';
5616
5617
                                break;
5618
                            case 0x01:
5619
                                $spacetype = 'type1';
5620
5621
                                break;
5622
                            case 0x02:
5623
                                $spacetype = 'type2';
5624
5625
                                break;
5626
                            case 0x03:
5627
                                $spacetype = 'type3';
5628
5629
                                break;
5630
                            case 0x04:
5631
                                $spacetype = 'type4';
5632
5633
                                break;
5634
                            case 0x05:
5635
                                $spacetype = 'type5';
5636
5637
                                break;
5638
                            default:
5639
                                throw new Exception('Unrecognized space type in tAttrSpace token');
5640
5641
                                break;
5642
                        }
5643
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
5644
                        $spacecount = ord($formulaData[3]);
5645
5646
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
5647
5648
                        break;
5649
                    default:
5650
                        throw new Exception('Unrecognized attribute flag in tAttr token');
5651
5652
                        break;
5653
                }
5654
5655 10
                break;
5656 38
            case 0x1C:    //    error code
5657
                // offset: 1; size: 1; error code
5658 1
                $name = 'tErr';
5659 1
                $size = 2;
5660 1
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5661
5662 1
                break;
5663 37
            case 0x1D:    //    boolean
5664
                // offset: 1; size: 1; 0 = false, 1 = true;
5665
                $name = 'tBool';
5666
                $size = 2;
5667
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
5668
5669
                break;
5670 37
            case 0x1E:    //    integer
5671
                // offset: 1; size: 2; unsigned 16-bit integer
5672 20
                $name = 'tInt';
5673 20
                $size = 3;
5674 20
                $data = self::getUInt2d($formulaData, 1);
5675
5676 20
                break;
5677 35
            case 0x1F:    //    number
5678
                // offset: 1; size: 8;
5679 4
                $name = 'tNum';
5680 4
                $size = 9;
5681 4
                $data = self::extractNumber(substr($formulaData, 1));
5682 4
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
5683
5684 4
                break;
5685 35
            case 0x20:    //    array constant
5686 35
            case 0x40:
5687 35
            case 0x60:
5688
                // offset: 1; size: 7; not used
5689
                $name = 'tArray';
5690
                $size = 8;
5691
                $data = null;
5692
5693
                break;
5694 35
            case 0x21:    //    function with fixed number of arguments
5695 35
            case 0x41:
5696 34
            case 0x61:
5697 11
                $name = 'tFunc';
5698 11
                $size = 3;
5699
                // offset: 1; size: 2; index to built-in sheet function
5700 11
                switch (self::getUInt2d($formulaData, 1)) {
5701 11
                    case 2:
5702
                        $function = 'ISNA';
5703
                        $args = 1;
5704
5705
                        break;
5706 11
                    case 3:
5707
                        $function = 'ISERROR';
5708
                        $args = 1;
5709
5710
                        break;
5711 11
                    case 10:
5712 8
                        $function = 'NA';
5713 8
                        $args = 0;
5714
5715 8
                        break;
5716 3
                    case 15:
5717 1
                        $function = 'SIN';
5718 1
                        $args = 1;
5719
5720 1
                        break;
5721 2
                    case 16:
5722
                        $function = 'COS';
5723
                        $args = 1;
5724
5725
                        break;
5726 2
                    case 17:
5727
                        $function = 'TAN';
5728
                        $args = 1;
5729
5730
                        break;
5731 2
                    case 18:
5732
                        $function = 'ATAN';
5733
                        $args = 1;
5734
5735
                        break;
5736 2
                    case 19:
5737
                        $function = 'PI';
5738
                        $args = 0;
5739
5740
                        break;
5741 2
                    case 20:
5742
                        $function = 'SQRT';
5743
                        $args = 1;
5744
5745
                        break;
5746 2
                    case 21:
5747
                        $function = 'EXP';
5748
                        $args = 1;
5749
5750
                        break;
5751 2
                    case 22:
5752
                        $function = 'LN';
5753
                        $args = 1;
5754
5755
                        break;
5756 2
                    case 23:
5757
                        $function = 'LOG10';
5758
                        $args = 1;
5759
5760
                        break;
5761 2
                    case 24:
5762
                        $function = 'ABS';
5763
                        $args = 1;
5764
5765
                        break;
5766 2
                    case 25:
5767
                        $function = 'INT';
5768
                        $args = 1;
5769
5770
                        break;
5771 2
                    case 26:
5772
                        $function = 'SIGN';
5773
                        $args = 1;
5774
5775
                        break;
5776 2
                    case 27:
5777
                        $function = 'ROUND';
5778
                        $args = 2;
5779
5780
                        break;
5781 2
                    case 30:
5782 1
                        $function = 'REPT';
5783 1
                        $args = 2;
5784
5785 1
                        break;
5786 2
                    case 31:
5787
                        $function = 'MID';
5788
                        $args = 3;
5789
5790
                        break;
5791 2
                    case 32:
5792
                        $function = 'LEN';
5793
                        $args = 1;
5794
5795
                        break;
5796 2
                    case 33:
5797
                        $function = 'VALUE';
5798
                        $args = 1;
5799
5800
                        break;
5801 2
                    case 34:
5802
                        $function = 'TRUE';
5803
                        $args = 0;
5804
5805
                        break;
5806 2
                    case 35:
5807
                        $function = 'FALSE';
5808
                        $args = 0;
5809
5810
                        break;
5811 2
                    case 38:
5812
                        $function = 'NOT';
5813
                        $args = 1;
5814
5815
                        break;
5816 2
                    case 39:
5817
                        $function = 'MOD';
5818
                        $args = 2;
5819
5820
                        break;
5821 2
                    case 40:
5822
                        $function = 'DCOUNT';
5823
                        $args = 3;
5824
5825
                        break;
5826 2
                    case 41:
5827
                        $function = 'DSUM';
5828
                        $args = 3;
5829
5830
                        break;
5831 2
                    case 42:
5832
                        $function = 'DAVERAGE';
5833
                        $args = 3;
5834
5835
                        break;
5836 2
                    case 43:
5837
                        $function = 'DMIN';
5838
                        $args = 3;
5839
5840
                        break;
5841 2
                    case 44:
5842
                        $function = 'DMAX';
5843
                        $args = 3;
5844
5845
                        break;
5846 2
                    case 45:
5847
                        $function = 'DSTDEV';
5848
                        $args = 3;
5849
5850
                        break;
5851 2
                    case 48:
5852
                        $function = 'TEXT';
5853
                        $args = 2;
5854
5855
                        break;
5856 2
                    case 61:
5857
                        $function = 'MIRR';
5858
                        $args = 3;
5859
5860
                        break;
5861 2
                    case 63:
5862
                        $function = 'RAND';
5863
                        $args = 0;
5864
5865
                        break;
5866 2
                    case 65:
5867
                        $function = 'DATE';
5868
                        $args = 3;
5869
5870
                        break;
5871 2
                    case 66:
5872
                        $function = 'TIME';
5873
                        $args = 3;
5874
5875
                        break;
5876 2
                    case 67:
5877
                        $function = 'DAY';
5878
                        $args = 1;
5879
5880
                        break;
5881 2
                    case 68:
5882
                        $function = 'MONTH';
5883
                        $args = 1;
5884
5885
                        break;
5886 2
                    case 69:
5887
                        $function = 'YEAR';
5888
                        $args = 1;
5889
5890
                        break;
5891 2
                    case 71:
5892
                        $function = 'HOUR';
5893
                        $args = 1;
5894
5895
                        break;
5896 2
                    case 72:
5897
                        $function = 'MINUTE';
5898
                        $args = 1;
5899
5900
                        break;
5901 2
                    case 73:
5902
                        $function = 'SECOND';
5903
                        $args = 1;
5904
5905
                        break;
5906 2
                    case 74:
5907
                        $function = 'NOW';
5908
                        $args = 0;
5909
5910
                        break;
5911 2
                    case 75:
5912
                        $function = 'AREAS';
5913
                        $args = 1;
5914
5915
                        break;
5916 2
                    case 76:
5917
                        $function = 'ROWS';
5918
                        $args = 1;
5919
5920
                        break;
5921 2
                    case 77:
5922
                        $function = 'COLUMNS';
5923
                        $args = 1;
5924
5925
                        break;
5926 2
                    case 83:
5927
                        $function = 'TRANSPOSE';
5928
                        $args = 1;
5929
5930
                        break;
5931 2
                    case 86:
5932
                        $function = 'TYPE';
5933
                        $args = 1;
5934
5935
                        break;
5936 2
                    case 97:
5937
                        $function = 'ATAN2';
5938
                        $args = 2;
5939
5940
                        break;
5941 2
                    case 98:
5942
                        $function = 'ASIN';
5943
                        $args = 1;
5944
5945
                        break;
5946 2
                    case 99:
5947
                        $function = 'ACOS';
5948
                        $args = 1;
5949
5950
                        break;
5951 2
                    case 105:
5952
                        $function = 'ISREF';
5953
                        $args = 1;
5954
5955
                        break;
5956 2
                    case 111:
5957 1
                        $function = 'CHAR';
5958 1
                        $args = 1;
5959
5960 1
                        break;
5961 1
                    case 112:
5962
                        $function = 'LOWER';
5963
                        $args = 1;
5964
5965
                        break;
5966 1
                    case 113:
5967
                        $function = 'UPPER';
5968
                        $args = 1;
5969
5970
                        break;
5971 1
                    case 114:
5972
                        $function = 'PROPER';
5973
                        $args = 1;
5974
5975
                        break;
5976 1
                    case 117:
5977
                        $function = 'EXACT';
5978
                        $args = 2;
5979
5980
                        break;
5981 1
                    case 118:
5982
                        $function = 'TRIM';
5983
                        $args = 1;
5984
5985
                        break;
5986 1
                    case 119:
5987
                        $function = 'REPLACE';
5988
                        $args = 4;
5989
5990
                        break;
5991 1
                    case 121:
5992
                        $function = 'CODE';
5993
                        $args = 1;
5994
5995
                        break;
5996 1
                    case 126:
5997
                        $function = 'ISERR';
5998
                        $args = 1;
5999
6000
                        break;
6001 1
                    case 127:
6002
                        $function = 'ISTEXT';
6003
                        $args = 1;
6004
6005
                        break;
6006 1
                    case 128:
6007
                        $function = 'ISNUMBER';
6008
                        $args = 1;
6009
6010
                        break;
6011 1
                    case 129:
6012
                        $function = 'ISBLANK';
6013
                        $args = 1;
6014
6015
                        break;
6016 1
                    case 130:
6017
                        $function = 'T';
6018
                        $args = 1;
6019
6020
                        break;
6021 1
                    case 131:
6022
                        $function = 'N';
6023
                        $args = 1;
6024
6025
                        break;
6026 1
                    case 140:
6027
                        $function = 'DATEVALUE';
6028
                        $args = 1;
6029
6030
                        break;
6031 1
                    case 141:
6032
                        $function = 'TIMEVALUE';
6033
                        $args = 1;
6034
6035
                        break;
6036 1
                    case 142:
6037
                        $function = 'SLN';
6038
                        $args = 3;
6039
6040
                        break;
6041 1
                    case 143:
6042
                        $function = 'SYD';
6043
                        $args = 4;
6044
6045
                        break;
6046 1
                    case 162:
6047
                        $function = 'CLEAN';
6048
                        $args = 1;
6049
6050
                        break;
6051 1
                    case 163:
6052
                        $function = 'MDETERM';
6053
                        $args = 1;
6054
6055
                        break;
6056 1
                    case 164:
6057
                        $function = 'MINVERSE';
6058
                        $args = 1;
6059
6060
                        break;
6061 1
                    case 165:
6062
                        $function = 'MMULT';
6063
                        $args = 2;
6064
6065
                        break;
6066 1
                    case 184:
6067
                        $function = 'FACT';
6068
                        $args = 1;
6069
6070
                        break;
6071 1
                    case 189:
6072
                        $function = 'DPRODUCT';
6073
                        $args = 3;
6074
6075
                        break;
6076 1
                    case 190:
6077
                        $function = 'ISNONTEXT';
6078
                        $args = 1;
6079
6080
                        break;
6081 1
                    case 195:
6082
                        $function = 'DSTDEVP';
6083
                        $args = 3;
6084
6085
                        break;
6086 1
                    case 196:
6087
                        $function = 'DVARP';
6088
                        $args = 3;
6089
6090
                        break;
6091 1
                    case 198:
6092
                        $function = 'ISLOGICAL';
6093
                        $args = 1;
6094
6095
                        break;
6096 1
                    case 199:
6097
                        $function = 'DCOUNTA';
6098
                        $args = 3;
6099
6100
                        break;
6101 1
                    case 207:
6102
                        $function = 'REPLACEB';
6103
                        $args = 4;
6104
6105
                        break;
6106 1
                    case 210:
6107
                        $function = 'MIDB';
6108
                        $args = 3;
6109
6110
                        break;
6111 1
                    case 211:
6112
                        $function = 'LENB';
6113
                        $args = 1;
6114
6115
                        break;
6116 1
                    case 212:
6117
                        $function = 'ROUNDUP';
6118
                        $args = 2;
6119
6120
                        break;
6121 1
                    case 213:
6122
                        $function = 'ROUNDDOWN';
6123
                        $args = 2;
6124
6125
                        break;
6126 1
                    case 214:
6127
                        $function = 'ASC';
6128
                        $args = 1;
6129
6130
                        break;
6131 1
                    case 215:
6132
                        $function = 'DBCS';
6133
                        $args = 1;
6134
6135
                        break;
6136 1
                    case 221:
6137
                        $function = 'TODAY';
6138
                        $args = 0;
6139
6140
                        break;
6141 1
                    case 229:
6142
                        $function = 'SINH';
6143
                        $args = 1;
6144
6145
                        break;
6146 1
                    case 230:
6147
                        $function = 'COSH';
6148
                        $args = 1;
6149
6150
                        break;
6151 1
                    case 231:
6152
                        $function = 'TANH';
6153
                        $args = 1;
6154
6155
                        break;
6156 1
                    case 232:
6157
                        $function = 'ASINH';
6158
                        $args = 1;
6159
6160
                        break;
6161 1
                    case 233:
6162
                        $function = 'ACOSH';
6163
                        $args = 1;
6164
6165
                        break;
6166 1
                    case 234:
6167
                        $function = 'ATANH';
6168
                        $args = 1;
6169
6170
                        break;
6171 1
                    case 235:
6172
                        $function = 'DGET';
6173
                        $args = 3;
6174
6175
                        break;
6176 1
                    case 244:
6177
                        $function = 'INFO';
6178
                        $args = 1;
6179
6180
                        break;
6181 1
                    case 252:
6182
                        $function = 'FREQUENCY';
6183
                        $args = 2;
6184
6185
                        break;
6186 1
                    case 261:
6187
                        $function = 'ERROR.TYPE';
6188
                        $args = 1;
6189
6190
                        break;
6191 1
                    case 271:
6192
                        $function = 'GAMMALN';
6193
                        $args = 1;
6194
6195
                        break;
6196 1
                    case 273:
6197
                        $function = 'BINOMDIST';
6198
                        $args = 4;
6199
6200
                        break;
6201 1
                    case 274:
6202
                        $function = 'CHIDIST';
6203
                        $args = 2;
6204
6205
                        break;
6206 1
                    case 275:
6207
                        $function = 'CHIINV';
6208
                        $args = 2;
6209
6210
                        break;
6211 1
                    case 276:
6212
                        $function = 'COMBIN';
6213
                        $args = 2;
6214
6215
                        break;
6216 1
                    case 277:
6217
                        $function = 'CONFIDENCE';
6218
                        $args = 3;
6219
6220
                        break;
6221 1
                    case 278:
6222
                        $function = 'CRITBINOM';
6223
                        $args = 3;
6224
6225
                        break;
6226 1
                    case 279:
6227
                        $function = 'EVEN';
6228
                        $args = 1;
6229
6230
                        break;
6231 1
                    case 280:
6232
                        $function = 'EXPONDIST';
6233
                        $args = 3;
6234
6235
                        break;
6236 1
                    case 281:
6237
                        $function = 'FDIST';
6238
                        $args = 3;
6239
6240
                        break;
6241 1
                    case 282:
6242
                        $function = 'FINV';
6243
                        $args = 3;
6244
6245
                        break;
6246 1
                    case 283:
6247
                        $function = 'FISHER';
6248
                        $args = 1;
6249
6250
                        break;
6251 1
                    case 284:
6252
                        $function = 'FISHERINV';
6253
                        $args = 1;
6254
6255
                        break;
6256 1
                    case 285:
6257
                        $function = 'FLOOR';
6258
                        $args = 2;
6259
6260
                        break;
6261 1
                    case 286:
6262
                        $function = 'GAMMADIST';
6263
                        $args = 4;
6264
6265
                        break;
6266 1
                    case 287:
6267
                        $function = 'GAMMAINV';
6268
                        $args = 3;
6269
6270
                        break;
6271 1
                    case 288:
6272
                        $function = 'CEILING';
6273
                        $args = 2;
6274
6275
                        break;
6276 1
                    case 289:
6277
                        $function = 'HYPGEOMDIST';
6278
                        $args = 4;
6279
6280
                        break;
6281 1
                    case 290:
6282
                        $function = 'LOGNORMDIST';
6283
                        $args = 3;
6284
6285
                        break;
6286 1
                    case 291:
6287
                        $function = 'LOGINV';
6288
                        $args = 3;
6289
6290
                        break;
6291 1
                    case 292:
6292
                        $function = 'NEGBINOMDIST';
6293
                        $args = 3;
6294
6295
                        break;
6296 1
                    case 293:
6297
                        $function = 'NORMDIST';
6298
                        $args = 4;
6299
6300
                        break;
6301 1
                    case 294:
6302
                        $function = 'NORMSDIST';
6303
                        $args = 1;
6304
6305
                        break;
6306 1
                    case 295:
6307
                        $function = 'NORMINV';
6308
                        $args = 3;
6309
6310
                        break;
6311 1
                    case 296:
6312
                        $function = 'NORMSINV';
6313
                        $args = 1;
6314
6315
                        break;
6316 1
                    case 297:
6317
                        $function = 'STANDARDIZE';
6318
                        $args = 3;
6319
6320
                        break;
6321 1
                    case 298:
6322
                        $function = 'ODD';
6323
                        $args = 1;
6324
6325
                        break;
6326 1
                    case 299:
6327
                        $function = 'PERMUT';
6328
                        $args = 2;
6329
6330
                        break;
6331 1
                    case 300:
6332
                        $function = 'POISSON';
6333
                        $args = 3;
6334
6335
                        break;
6336 1
                    case 301:
6337
                        $function = 'TDIST';
6338
                        $args = 3;
6339
6340
                        break;
6341 1
                    case 302:
6342
                        $function = 'WEIBULL';
6343
                        $args = 4;
6344
6345
                        break;
6346 1
                    case 303:
6347
                        $function = 'SUMXMY2';
6348
                        $args = 2;
6349
6350
                        break;
6351 1
                    case 304:
6352
                        $function = 'SUMX2MY2';
6353
                        $args = 2;
6354
6355
                        break;
6356 1
                    case 305:
6357
                        $function = 'SUMX2PY2';
6358
                        $args = 2;
6359
6360
                        break;
6361 1
                    case 306:
6362
                        $function = 'CHITEST';
6363
                        $args = 2;
6364
6365
                        break;
6366 1
                    case 307:
6367
                        $function = 'CORREL';
6368
                        $args = 2;
6369
6370
                        break;
6371 1
                    case 308:
6372
                        $function = 'COVAR';
6373
                        $args = 2;
6374
6375
                        break;
6376 1
                    case 309:
6377
                        $function = 'FORECAST';
6378
                        $args = 3;
6379
6380
                        break;
6381 1
                    case 310:
6382
                        $function = 'FTEST';
6383
                        $args = 2;
6384
6385
                        break;
6386 1
                    case 311:
6387
                        $function = 'INTERCEPT';
6388
                        $args = 2;
6389
6390
                        break;
6391 1
                    case 312:
6392
                        $function = 'PEARSON';
6393
                        $args = 2;
6394
6395
                        break;
6396 1
                    case 313:
6397
                        $function = 'RSQ';
6398
                        $args = 2;
6399
6400
                        break;
6401 1
                    case 314:
6402
                        $function = 'STEYX';
6403
                        $args = 2;
6404
6405
                        break;
6406 1
                    case 315:
6407
                        $function = 'SLOPE';
6408
                        $args = 2;
6409
6410
                        break;
6411 1
                    case 316:
6412
                        $function = 'TTEST';
6413
                        $args = 4;
6414
6415
                        break;
6416 1
                    case 325:
6417
                        $function = 'LARGE';
6418
                        $args = 2;
6419
6420
                        break;
6421 1
                    case 326:
6422
                        $function = 'SMALL';
6423
                        $args = 2;
6424
6425
                        break;
6426 1
                    case 327:
6427
                        $function = 'QUARTILE';
6428
                        $args = 2;
6429
6430
                        break;
6431 1
                    case 328:
6432
                        $function = 'PERCENTILE';
6433
                        $args = 2;
6434
6435
                        break;
6436 1
                    case 331:
6437
                        $function = 'TRIMMEAN';
6438
                        $args = 2;
6439
6440
                        break;
6441 1
                    case 332:
6442
                        $function = 'TINV';
6443
                        $args = 2;
6444
6445
                        break;
6446 1
                    case 337:
6447
                        $function = 'POWER';
6448
                        $args = 2;
6449
6450
                        break;
6451 1
                    case 342:
6452
                        $function = 'RADIANS';
6453
                        $args = 1;
6454
6455
                        break;
6456 1
                    case 343:
6457
                        $function = 'DEGREES';
6458
                        $args = 1;
6459
6460
                        break;
6461 1
                    case 346:
6462
                        $function = 'COUNTIF';
6463
                        $args = 2;
6464
6465
                        break;
6466 1
                    case 347:
6467
                        $function = 'COUNTBLANK';
6468
                        $args = 1;
6469
6470
                        break;
6471 1
                    case 350:
6472
                        $function = 'ISPMT';
6473
                        $args = 4;
6474
6475
                        break;
6476 1
                    case 351:
6477
                        $function = 'DATEDIF';
6478
                        $args = 3;
6479
6480
                        break;
6481 1
                    case 352:
6482
                        $function = 'DATESTRING';
6483
                        $args = 1;
6484
6485
                        break;
6486 1
                    case 353:
6487
                        $function = 'NUMBERSTRING';
6488
                        $args = 2;
6489
6490
                        break;
6491 1
                    case 360:
6492
                        $function = 'PHONETIC';
6493
                        $args = 1;
6494
6495
                        break;
6496 1
                    case 368:
6497
                        $function = 'BAHTTEXT';
6498
                        $args = 1;
6499
6500
                        break;
6501
                    default:
6502 1
                        throw new Exception('Unrecognized function in formula');
6503
6504
                        break;
6505
                }
6506 11
                $data = ['function' => $function, 'args' => $args];
6507
6508 11
                break;
6509 34
            case 0x22:    //    function with variable number of arguments
6510 34
            case 0x42:
6511 34
            case 0x62:
6512 12
                $name = 'tFuncV';
6513 12
                $size = 4;
6514
                // offset: 1; size: 1; number of arguments
6515 12
                $args = ord($formulaData[1]);
6516
                // offset: 2: size: 2; index to built-in sheet function
6517 12
                $index = self::getUInt2d($formulaData, 2);
6518
                switch ($index) {
6519 12
                    case 0:
6520 2
                        $function = 'COUNT';
6521
6522 2
                        break;
6523 12
                    case 1:
6524 1
                        $function = 'IF';
6525
6526 1
                        break;
6527 12
                    case 4:
6528 5
                        $function = 'SUM';
6529
6530 5
                        break;
6531 7
                    case 5:
6532 5
                        $function = 'AVERAGE';
6533
6534 5
                        break;
6535 7
                    case 6:
6536
                        $function = 'MIN';
6537
6538
                        break;
6539 7
                    case 7:
6540
                        $function = 'MAX';
6541
6542
                        break;
6543 7
                    case 8:
6544
                        $function = 'ROW';
6545
6546
                        break;
6547 7
                    case 9:
6548
                        $function = 'COLUMN';
6549
6550
                        break;
6551 7
                    case 11:
6552
                        $function = 'NPV';
6553
6554
                        break;
6555 7
                    case 12:
6556 5
                        $function = 'STDEV';
6557
6558 5
                        break;
6559 2
                    case 13:
6560
                        $function = 'DOLLAR';
6561
6562
                        break;
6563 2
                    case 14:
6564
                        $function = 'FIXED';
6565
6566
                        break;
6567 2
                    case 28:
6568
                        $function = 'LOOKUP';
6569
6570
                        break;
6571 2
                    case 29:
6572
                        $function = 'INDEX';
6573
6574
                        break;
6575 2
                    case 36:
6576 2
                        $function = 'AND';
6577
6578 2
                        break;
6579
                    case 37:
6580
                        $function = 'OR';
6581
6582
                        break;
6583
                    case 46:
6584
                        $function = 'VAR';
6585
6586
                        break;
6587
                    case 49:
6588
                        $function = 'LINEST';
6589
6590
                        break;
6591
                    case 50:
6592
                        $function = 'TREND';
6593
6594
                        break;
6595
                    case 51:
6596
                        $function = 'LOGEST';
6597
6598
                        break;
6599
                    case 52:
6600
                        $function = 'GROWTH';
6601
6602
                        break;
6603
                    case 56:
6604
                        $function = 'PV';
6605
6606
                        break;
6607
                    case 57:
6608
                        $function = 'FV';
6609
6610
                        break;
6611
                    case 58:
6612
                        $function = 'NPER';
6613
6614
                        break;
6615
                    case 59:
6616
                        $function = 'PMT';
6617
6618
                        break;
6619
                    case 60:
6620
                        $function = 'RATE';
6621
6622
                        break;
6623
                    case 62:
6624
                        $function = 'IRR';
6625
6626
                        break;
6627
                    case 64:
6628
                        $function = 'MATCH';
6629
6630
                        break;
6631
                    case 70:
6632
                        $function = 'WEEKDAY';
6633
6634
                        break;
6635
                    case 78:
6636
                        $function = 'OFFSET';
6637
6638
                        break;
6639
                    case 82:
6640
                        $function = 'SEARCH';
6641
6642
                        break;
6643
                    case 100:
6644
                        $function = 'CHOOSE';
6645
6646
                        break;
6647
                    case 101:
6648
                        $function = 'HLOOKUP';
6649
6650
                        break;
6651
                    case 102:
6652
                        $function = 'VLOOKUP';
6653
6654
                        break;
6655
                    case 109:
6656
                        $function = 'LOG';
6657
6658
                        break;
6659
                    case 115:
6660
                        $function = 'LEFT';
6661
6662
                        break;
6663
                    case 116:
6664
                        $function = 'RIGHT';
6665
6666
                        break;
6667
                    case 120:
6668
                        $function = 'SUBSTITUTE';
6669
6670
                        break;
6671
                    case 124:
6672
                        $function = 'FIND';
6673
6674
                        break;
6675
                    case 125:
6676
                        $function = 'CELL';
6677
6678
                        break;
6679
                    case 144:
6680
                        $function = 'DDB';
6681
6682
                        break;
6683
                    case 148:
6684
                        $function = 'INDIRECT';
6685
6686
                        break;
6687
                    case 167:
6688
                        $function = 'IPMT';
6689
6690
                        break;
6691
                    case 168:
6692
                        $function = 'PPMT';
6693
6694
                        break;
6695
                    case 169:
6696
                        $function = 'COUNTA';
6697
6698
                        break;
6699
                    case 183:
6700
                        $function = 'PRODUCT';
6701
6702
                        break;
6703
                    case 193:
6704
                        $function = 'STDEVP';
6705
6706
                        break;
6707
                    case 194:
6708
                        $function = 'VARP';
6709
6710
                        break;
6711
                    case 197:
6712
                        $function = 'TRUNC';
6713
6714
                        break;
6715
                    case 204:
6716
                        $function = 'USDOLLAR';
6717
6718
                        break;
6719
                    case 205:
6720
                        $function = 'FINDB';
6721
6722
                        break;
6723
                    case 206:
6724
                        $function = 'SEARCHB';
6725
6726
                        break;
6727
                    case 208:
6728
                        $function = 'LEFTB';
6729
6730
                        break;
6731
                    case 209:
6732
                        $function = 'RIGHTB';
6733
6734
                        break;
6735
                    case 216:
6736
                        $function = 'RANK';
6737
6738
                        break;
6739
                    case 219:
6740
                        $function = 'ADDRESS';
6741
6742
                        break;
6743
                    case 220:
6744
                        $function = 'DAYS360';
6745
6746
                        break;
6747
                    case 222:
6748
                        $function = 'VDB';
6749
6750
                        break;
6751
                    case 227:
6752
                        $function = 'MEDIAN';
6753
6754
                        break;
6755
                    case 228:
6756
                        $function = 'SUMPRODUCT';
6757
6758
                        break;
6759
                    case 247:
6760
                        $function = 'DB';
6761
6762
                        break;
6763
                    case 255:
6764
                        $function = '';
6765
6766
                        break;
6767
                    case 269:
6768
                        $function = 'AVEDEV';
6769
6770
                        break;
6771
                    case 270:
6772
                        $function = 'BETADIST';
6773
6774
                        break;
6775
                    case 272:
6776
                        $function = 'BETAINV';
6777
6778
                        break;
6779
                    case 317:
6780
                        $function = 'PROB';
6781
6782
                        break;
6783
                    case 318:
6784
                        $function = 'DEVSQ';
6785
6786
                        break;
6787
                    case 319:
6788
                        $function = 'GEOMEAN';
6789
6790
                        break;
6791
                    case 320:
6792
                        $function = 'HARMEAN';
6793
6794
                        break;
6795
                    case 321:
6796
                        $function = 'SUMSQ';
6797
6798
                        break;
6799
                    case 322:
6800
                        $function = 'KURT';
6801
6802
                        break;
6803
                    case 323:
6804
                        $function = 'SKEW';
6805
6806
                        break;
6807
                    case 324:
6808
                        $function = 'ZTEST';
6809
6810
                        break;
6811
                    case 329:
6812
                        $function = 'PERCENTRANK';
6813
6814
                        break;
6815
                    case 330:
6816
                        $function = 'MODE';
6817
6818
                        break;
6819
                    case 336:
6820
                        $function = 'CONCATENATE';
6821
6822
                        break;
6823
                    case 344:
6824
                        $function = 'SUBTOTAL';
6825
6826
                        break;
6827
                    case 345:
6828
                        $function = 'SUMIF';
6829
6830
                        break;
6831
                    case 354:
6832
                        $function = 'ROMAN';
6833
6834
                        break;
6835
                    case 358:
6836
                        $function = 'GETPIVOTDATA';
6837
6838
                        break;
6839
                    case 359:
6840
                        $function = 'HYPERLINK';
6841
6842
                        break;
6843
                    case 361:
6844
                        $function = 'AVERAGEA';
6845
6846
                        break;
6847
                    case 362:
6848
                        $function = 'MAXA';
6849
6850
                        break;
6851
                    case 363:
6852
                        $function = 'MINA';
6853
6854
                        break;
6855
                    case 364:
6856
                        $function = 'STDEVPA';
6857
6858
                        break;
6859
                    case 365:
6860
                        $function = 'VARPA';
6861
6862
                        break;
6863
                    case 366:
6864
                        $function = 'STDEVA';
6865
6866
                        break;
6867
                    case 367:
6868
                        $function = 'VARA';
6869
6870
                        break;
6871
                    default:
6872
                        throw new Exception('Unrecognized function in formula');
6873
6874
                        break;
6875
                }
6876 12
                $data = ['function' => $function, 'args' => $args];
6877
6878 12
                break;
6879 34
            case 0x23:    //    index to defined name
6880 34
            case 0x43:
6881 34
            case 0x63:
6882 1
                $name = 'tName';
6883 1
                $size = 5;
6884
                // offset: 1; size: 2; one-based index to definedname record
6885 1
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
6886
                // offset: 2; size: 2; not used
6887 1
                $data = $this->definedname[$definedNameIndex]['name'] ?? '';
6888
6889 1
                break;
6890 33
            case 0x24:    //    single cell reference e.g. A5
6891 33
            case 0x44:
6892 30
            case 0x64:
6893 13
                $name = 'tRef';
6894 13
                $size = 5;
6895 13
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));
6896
6897 13
                break;
6898 28
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
6899 12
            case 0x45:
6900 12
            case 0x65:
6901 20
                $name = 'tArea';
6902 20
                $size = 9;
6903 20
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
6904
6905 20
                break;
6906 12
            case 0x26:    //    Constant reference sub-expression
6907 12
            case 0x46:
6908 12
            case 0x66:
6909
                $name = 'tMemArea';
6910
                // offset: 1; size: 4; not used
6911
                // offset: 5; size: 2; size of the following subexpression
6912
                $subSize = self::getUInt2d($formulaData, 5);
6913
                $size = 7 + $subSize;
6914
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
6915
6916
                break;
6917 12
            case 0x27:    //    Deleted constant reference sub-expression
6918 12
            case 0x47:
6919 12
            case 0x67:
6920
                $name = 'tMemErr';
6921
                // offset: 1; size: 4; not used
6922
                // offset: 5; size: 2; size of the following subexpression
6923
                $subSize = self::getUInt2d($formulaData, 5);
6924
                $size = 7 + $subSize;
6925
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
6926
6927
                break;
6928 12
            case 0x29:    //    Variable reference sub-expression
6929 12
            case 0x49:
6930 12
            case 0x69:
6931
                $name = 'tMemFunc';
6932
                // offset: 1; size: 2; size of the following sub-expression
6933
                $subSize = self::getUInt2d($formulaData, 1);
6934
                $size = 3 + $subSize;
6935
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
6936
6937
                break;
6938 12
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
6939 12
            case 0x4C:
6940 12
            case 0x6C:
6941 2
                $name = 'tRefN';
6942 2
                $size = 5;
6943 2
                $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
6944
6945 2
                break;
6946 10
            case 0x2D:    //    Relative 2d range reference
6947 10
            case 0x4D:
6948 10
            case 0x6D:
6949
                $name = 'tAreaN';
6950
                $size = 9;
6951
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
6952
6953
                break;
6954 10
            case 0x39:    //    External name
6955 10
            case 0x59:
6956 10
            case 0x79:
6957
                $name = 'tNameX';
6958
                $size = 7;
6959
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
6960
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
6961
                $index = self::getUInt2d($formulaData, 3);
6962
                // assume index is to EXTERNNAME record
6963
                $data = $this->externalNames[$index - 1]['name'] ?? '';
6964
                // offset: 5; size: 2; not used
6965
                break;
6966 10
            case 0x3A:    //    3d reference to cell
6967 8
            case 0x5A:
6968 8
            case 0x7A:
6969 2
                $name = 'tRef3d';
6970 2
                $size = 7;
6971
6972
                try {
6973
                    // offset: 1; size: 2; index to REF entry
6974 2
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
6975
                    // offset: 3; size: 4; cell address
6976 2
                    $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4));
6977
6978 2
                    $data = "$sheetRange!$cellAddress";
6979
                } catch (PhpSpreadsheetException $e) {
6980
                    // deleted sheet reference
6981
                    $data = '#REF!';
6982
                }
6983
6984 2
                break;
6985 8
            case 0x3B:    //    3d reference to cell range
6986
            case 0x5B:
6987
            case 0x7B:
6988 8
                $name = 'tArea3d';
6989 8
                $size = 11;
6990
6991
                try {
6992
                    // offset: 1; size: 2; index to REF entry
6993 8
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
6994
                    // offset: 3; size: 8; cell address
6995 8
                    $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
6996
6997 8
                    $data = "$sheetRange!$cellRangeAddress";
6998
                } catch (PhpSpreadsheetException $e) {
6999
                    // deleted sheet reference
7000
                    $data = '#REF!';
7001
                }
7002
7003 8
                break;
7004
                // Unknown cases    // don't know how to deal with
7005
            default:
7006
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
7007
7008
                break;
7009
        }
7010
7011 38
        return [
7012 38
            'id' => $id,
7013 38
            'name' => $name,
7014 38
            'size' => $size,
7015 38
            'data' => $data,
7016 38
        ];
7017
    }
7018
7019
    /**
7020
     * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2'
7021
     * section 3.3.4.
7022
     *
7023
     * @param string $cellAddressStructure
7024
     *
7025
     * @return string
7026
     */
7027 16
    private function readBIFF8CellAddress($cellAddressStructure)
7028
    {
7029
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7030 16
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
7031
7032
        // offset: 2; size: 2; index to column or column offset + relative flags
7033
        // bit: 7-0; mask 0x00FF; column index
7034 16
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);
7035
7036
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7037 16
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
7038 10
            $column = '$' . $column;
7039
        }
7040
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7041 16
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7042 10
            $row = '$' . $row;
7043
        }
7044
7045 16
        return $column . $row;
7046
    }
7047
7048
    /**
7049
     * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column
7050
     * to indicate offsets from a base cell
7051
     * section 3.3.4.
7052
     *
7053
     * @param string $cellAddressStructure
7054
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
7055
     *
7056
     * @return string
7057
     */
7058 2
    private function readBIFF8CellAddressB($cellAddressStructure, $baseCell = 'A1')
7059
    {
7060 2
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
7061 2
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
7062 2
        $baseRow = (int) $baseRow;
7063
7064
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7065 2
        $rowIndex = self::getUInt2d($cellAddressStructure, 0);
7066 2
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
7067
7068
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7069 2
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
7070
            // offset: 2; size: 2; index to column or column offset + relative flags
7071
            // bit: 7-0; mask 0x00FF; column index
7072 2
            $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2);
7073
7074 2
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
7075 2
            $column = '$' . $column;
7076
        } else {
7077
            // offset: 2; size: 2; index to column or column offset + relative flags
7078
            // bit: 7-0; mask 0x00FF; column index
7079
            $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2);
7080
            $colIndex = $baseCol + $relativeColIndex;
7081
            $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256;
7082
            $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256;
7083
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
7084
        }
7085
7086
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7087 2
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7088
            $row = '$' . $row;
7089
        } else {
7090 2
            $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536;
7091 2
            $row = $baseRow + $rowIndex;
7092
        }
7093
7094 2
        return $column . $row;
7095
    }
7096
7097
    /**
7098
     * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1'
7099
     * always fixed range
7100
     * section 2.5.14.
7101
     *
7102
     * @param string $subData
7103
     *
7104
     * @return string
7105
     */
7106 76
    private function readBIFF5CellRangeAddressFixed($subData)
7107
    {
7108
        // offset: 0; size: 2; index to first row
7109 76
        $fr = self::getUInt2d($subData, 0) + 1;
7110
7111
        // offset: 2; size: 2; index to last row
7112 76
        $lr = self::getUInt2d($subData, 2) + 1;
7113
7114
        // offset: 4; size: 1; index to first column
7115 76
        $fc = ord($subData[4]);
7116
7117
        // offset: 5; size: 1; index to last column
7118 76
        $lc = ord($subData[5]);
7119
7120
        // check values
7121 76
        if ($fr > $lr || $fc > $lc) {
7122
            throw new Exception('Not a cell range address');
7123
        }
7124
7125
        // column index to letter
7126 76
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7127 76
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7128
7129 76
        if ($fr == $lr && $fc == $lc) {
7130 65
            return "$fc$fr";
7131
        }
7132
7133 25
        return "$fc$fr:$lc$lr";
7134
    }
7135
7136
    /**
7137
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1'
7138
     * always fixed range
7139
     * section 2.5.14.
7140
     *
7141
     * @param string $subData
7142
     *
7143
     * @return string
7144
     */
7145 26
    private function readBIFF8CellRangeAddressFixed($subData)
7146
    {
7147
        // offset: 0; size: 2; index to first row
7148 26
        $fr = self::getUInt2d($subData, 0) + 1;
7149
7150
        // offset: 2; size: 2; index to last row
7151 26
        $lr = self::getUInt2d($subData, 2) + 1;
7152
7153
        // offset: 4; size: 2; index to first column
7154 26
        $fc = self::getUInt2d($subData, 4);
7155
7156
        // offset: 6; size: 2; index to last column
7157 26
        $lc = self::getUInt2d($subData, 6);
7158
7159
        // check values
7160 26
        if ($fr > $lr || $fc > $lc) {
7161
            throw new Exception('Not a cell range address');
7162
        }
7163
7164
        // column index to letter
7165 26
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7166 26
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7167
7168 26
        if ($fr == $lr && $fc == $lc) {
7169 6
            return "$fc$fr";
7170
        }
7171
7172 24
        return "$fc$fr:$lc$lr";
7173
    }
7174
7175
    /**
7176
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6'
7177
     * there are flags indicating whether column/row index is relative
7178
     * section 3.3.4.
7179
     *
7180
     * @param string $subData
7181
     *
7182
     * @return string
7183
     */
7184 24
    private function readBIFF8CellRangeAddress($subData)
7185
    {
7186
        // todo: if cell range is just a single cell, should this funciton
7187
        // not just return e.g. 'A1' and not 'A1:A1' ?
7188
7189
        // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
7190 24
        $fr = self::getUInt2d($subData, 0) + 1;
7191
7192
        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
7193 24
        $lr = self::getUInt2d($subData, 2) + 1;
7194
7195
        // offset: 4; size: 2; index to first column or column offset + relative flags
7196
7197
        // bit: 7-0; mask 0x00FF; column index
7198 24
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);
7199
7200
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7201 24
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
7202 13
            $fc = '$' . $fc;
7203
        }
7204
7205
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7206 24
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
7207 13
            $fr = '$' . $fr;
7208
        }
7209
7210
        // offset: 6; size: 2; index to last column or column offset + relative flags
7211
7212
        // bit: 7-0; mask 0x00FF; column index
7213 24
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);
7214
7215
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7216 24
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
7217 13
            $lc = '$' . $lc;
7218
        }
7219
7220
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7221 24
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7222 13
            $lr = '$' . $lr;
7223
        }
7224
7225 24
        return "$fc$fr:$lc$lr";
7226
    }
7227
7228
    /**
7229
     * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column
7230
     * to indicate offsets from a base cell
7231
     * section 3.3.4.
7232
     *
7233
     * @param string $subData
7234
     * @param string $baseCell Base cell
7235
     *
7236
     * @return string Cell range address
7237
     */
7238
    private function readBIFF8CellRangeAddressB($subData, $baseCell = 'A1')
7239
    {
7240
        [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell);
7241
        $baseCol = $baseCol - 1;
7242
7243
        // TODO: if cell range is just a single cell, should this funciton
7244
        // not just return e.g. 'A1' and not 'A1:A1' ?
7245
7246
        // offset: 0; size: 2; first row
7247
        $frIndex = self::getUInt2d($subData, 0); // adjust below
7248
7249
        // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767)
7250
        $lrIndex = self::getUInt2d($subData, 2); // adjust below
7251
7252
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7253
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
7254
            // absolute column index
7255
            // offset: 4; size: 2; first column with relative/absolute flags
7256
            // bit: 7-0; mask 0x00FF; column index
7257
            $fcIndex = 0x00FF & self::getUInt2d($subData, 4);
7258
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
7259
            $fc = '$' . $fc;
7260
        } else {
7261
            // column offset
7262
            // offset: 4; size: 2; first column with relative/absolute flags
7263
            // bit: 7-0; mask 0x00FF; column index
7264
            $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4);
7265
            $fcIndex = $baseCol + $relativeFcIndex;
7266
            $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256;
7267
            $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256;
7268
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
7269
        }
7270
7271
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7272
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
7273
            // absolute row index
7274
            $fr = $frIndex + 1;
7275
            $fr = '$' . $fr;
7276
        } else {
7277
            // row offset
7278
            $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536;
7279
            $fr = $baseRow + $frIndex;
7280
        }
7281
7282
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7283
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
7284
            // absolute column index
7285
            // offset: 6; size: 2; last column with relative/absolute flags
7286
            // bit: 7-0; mask 0x00FF; column index
7287
            $lcIndex = 0x00FF & self::getUInt2d($subData, 6);
7288
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
7289
            $lc = '$' . $lc;
7290
        } else {
7291
            // column offset
7292
            // offset: 4; size: 2; first column with relative/absolute flags
7293
            // bit: 7-0; mask 0x00FF; column index
7294
            $relativeLcIndex = 0x00FF & self::getInt2d($subData, 4);
7295
            $lcIndex = $baseCol + $relativeLcIndex;
7296
            $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256;
7297
            $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256;
7298
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
7299
        }
7300
7301
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7302
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7303
            // absolute row index
7304
            $lr = $lrIndex + 1;
7305
            $lr = '$' . $lr;
7306
        } else {
7307
            // row offset
7308
            $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536;
7309
            $lr = $baseRow + $lrIndex;
7310
        }
7311
7312
        return "$fc$fr:$lc$lr";
7313
    }
7314
7315
    /**
7316
     * Read BIFF8 cell range address list
7317
     * section 2.5.15.
7318
     *
7319
     * @param string $subData
7320
     *
7321
     * @return array
7322
     */
7323 26
    private function readBIFF8CellRangeAddressList($subData)
7324
    {
7325 26
        $cellRangeAddresses = [];
7326
7327
        // offset: 0; size: 2; number of the following cell range addresses
7328 26
        $nm = self::getUInt2d($subData, 0);
7329
7330 26
        $offset = 2;
7331
        // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses
7332 26
        for ($i = 0; $i < $nm; ++$i) {
7333 26
            $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8));
7334 26
            $offset += 8;
7335
        }
7336
7337 26
        return [
7338 26
            'size' => 2 + 8 * $nm,
7339 26
            'cellRangeAddresses' => $cellRangeAddresses,
7340 26
        ];
7341
    }
7342
7343
    /**
7344
     * Read BIFF5 cell range address list
7345
     * section 2.5.15.
7346
     *
7347
     * @param string $subData
7348
     *
7349
     * @return array
7350
     */
7351 76
    private function readBIFF5CellRangeAddressList($subData)
7352
    {
7353 76
        $cellRangeAddresses = [];
7354
7355
        // offset: 0; size: 2; number of the following cell range addresses
7356 76
        $nm = self::getUInt2d($subData, 0);
7357
7358 76
        $offset = 2;
7359
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
7360 76
        for ($i = 0; $i < $nm; ++$i) {
7361 76
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
7362 76
            $offset += 6;
7363
        }
7364
7365 76
        return [
7366 76
            'size' => 2 + 6 * $nm,
7367 76
            'cellRangeAddresses' => $cellRangeAddresses,
7368 76
        ];
7369
    }
7370
7371
    /**
7372
     * Get a sheet range like Sheet1:Sheet3 from REF index
7373
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
7374
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
7375
     * in which case an Exception is thrown.
7376
     *
7377
     * @param int $index
7378
     *
7379
     * @return false|string
7380
     */
7381 10
    private function readSheetRangeByRefIndex($index)
7382
    {
7383 10
        if (isset($this->ref[$index])) {
7384 10
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
7385
7386
            switch ($type) {
7387 10
                case 'internal':
7388
                    // check if we have a deleted 3d reference
7389 10
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
7390
                        throw new Exception('Deleted sheet reference');
7391
                    }
7392
7393
                    // we have normal sheet range (collapsed or uncollapsed)
7394 10
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
7395 10
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
7396
7397 10
                    if ($firstSheetName == $lastSheetName) {
7398
                        // collapsed sheet range
7399 10
                        $sheetRange = $firstSheetName;
7400
                    } else {
7401
                        $sheetRange = "$firstSheetName:$lastSheetName";
7402
                    }
7403
7404
                    // escape the single-quotes
7405 10
                    $sheetRange = str_replace("'", "''", $sheetRange);
7406
7407
                    // if there are special characters, we need to enclose the range in single-quotes
7408
                    // todo: check if we have identified the whole set of special characters
7409
                    // it seems that the following characters are not accepted for sheet names
7410
                    // and we may assume that they are not present: []*/:\?
7411 10
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
7412 3
                        $sheetRange = "'$sheetRange'";
7413
                    }
7414
7415 10
                    return $sheetRange;
7416
7417
                    break;
7418
                default:
7419
                    // TODO: external sheet support
7420
                    throw new Exception('Xls reader only supports internal sheets in formulas');
7421
7422
                    break;
7423
            }
7424
        }
7425
7426
        return false;
7427
    }
7428
7429
    /**
7430
     * read BIFF8 constant value array from array data
7431
     * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40]
7432
     * section 2.5.8.
7433
     *
7434
     * @param string $arrayData
7435
     *
7436
     * @return array
7437
     */
7438
    private static function readBIFF8ConstantArray($arrayData)
7439
    {
7440
        // offset: 0; size: 1; number of columns decreased by 1
7441
        $nc = ord($arrayData[0]);
7442
7443
        // offset: 1; size: 2; number of rows decreased by 1
7444
        $nr = self::getUInt2d($arrayData, 1);
7445
        $size = 3; // initialize
7446
        $arrayData = substr($arrayData, 3);
7447
7448
        // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values
7449
        $matrixChunks = [];
7450
        for ($r = 1; $r <= $nr + 1; ++$r) {
7451
            $items = [];
7452
            for ($c = 1; $c <= $nc + 1; ++$c) {
7453
                $constant = self::readBIFF8Constant($arrayData);
7454
                $items[] = $constant['value'];
7455
                $arrayData = substr($arrayData, $constant['size']);
7456
                $size += $constant['size'];
7457
            }
7458
            $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"'
7459
        }
7460
        $matrix = '{' . implode(';', $matrixChunks) . '}';
7461
7462
        return [
7463
            'value' => $matrix,
7464
            'size' => $size,
7465
        ];
7466
    }
7467
7468
    /**
7469
     * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value'
7470
     * section 2.5.7
7471
     * returns e.g. ['value' => '5', 'size' => 9].
7472
     *
7473
     * @param string $valueData
7474
     *
7475
     * @return array
7476
     */
7477
    private static function readBIFF8Constant($valueData)
7478
    {
7479
        // offset: 0; size: 1; identifier for type of constant
7480
        $identifier = ord($valueData[0]);
7481
7482
        switch ($identifier) {
7483
            case 0x00: // empty constant (what is this?)
7484
                $value = '';
7485
                $size = 9;
7486
7487
                break;
7488
            case 0x01: // number
7489
                // offset: 1; size: 8; IEEE 754 floating-point value
7490
                $value = self::extractNumber(substr($valueData, 1, 8));
7491
                $size = 9;
7492
7493
                break;
7494
            case 0x02: // string value
7495
                // offset: 1; size: var; Unicode string, 16-bit string length
7496
                $string = self::readUnicodeStringLong(substr($valueData, 1));
7497
                $value = '"' . $string['value'] . '"';
7498
                $size = 1 + $string['size'];
7499
7500
                break;
7501
            case 0x04: // boolean
7502
                // offset: 1; size: 1; 0 = FALSE, 1 = TRUE
7503
                if (ord($valueData[1])) {
7504
                    $value = 'TRUE';
7505
                } else {
7506
                    $value = 'FALSE';
7507
                }
7508
                $size = 9;
7509
7510
                break;
7511
            case 0x10: // error code
7512
                // offset: 1; size: 1; error code
7513
                $value = Xls\ErrorCode::lookup(ord($valueData[1]));
7514
                $size = 9;
7515
7516
                break;
7517
            default:
7518
                throw new PhpSpreadsheetException('Unsupported BIFF8 constant');
7519
        }
7520
7521
        return [
7522
            'value' => $value,
7523
            'size' => $size,
7524
        ];
7525
    }
7526
7527
    /**
7528
     * Extract RGB color
7529
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4.
7530
     *
7531
     * @param string $rgb Encoded RGB value (4 bytes)
7532
     *
7533
     * @return array
7534
     */
7535 53
    private static function readRGB($rgb)
7536
    {
7537
        // offset: 0; size 1; Red component
7538 53
        $r = ord($rgb[0]);
7539
7540
        // offset: 1; size: 1; Green component
7541 53
        $g = ord($rgb[1]);
7542
7543
        // offset: 2; size: 1; Blue component
7544 53
        $b = ord($rgb[2]);
7545
7546
        // HEX notation, e.g. 'FF00FC'
7547 53
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);
7548
7549 53
        return ['rgb' => $rgb];
7550
    }
7551
7552
    /**
7553
     * Read byte string (8-bit string length)
7554
     * OpenOffice documentation: 2.5.2.
7555
     *
7556
     * @param string $subData
7557
     *
7558
     * @return array
7559
     */
7560 1
    private function readByteStringShort($subData)
7561
    {
7562
        // offset: 0; size: 1; length of the string (character count)
7563 1
        $ln = ord($subData[0]);
7564
7565
        // offset: 1: size: var; character array (8-bit characters)
7566 1
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7567
7568 1
        return [
7569 1
            'value' => $value,
7570 1
            'size' => 1 + $ln, // size in bytes of data structure
7571 1
        ];
7572
    }
7573
7574
    /**
7575
     * Read byte string (16-bit string length)
7576
     * OpenOffice documentation: 2.5.2.
7577
     *
7578
     * @param string $subData
7579
     *
7580
     * @return array
7581
     */
7582 1
    private function readByteStringLong($subData)
7583
    {
7584
        // offset: 0; size: 2; length of the string (character count)
7585 1
        $ln = self::getUInt2d($subData, 0);
7586
7587
        // offset: 2: size: var; character array (8-bit characters)
7588 1
        $value = $this->decodeCodepage(substr($subData, 2));
7589
7590
        //return $string;
7591 1
        return [
7592 1
            'value' => $value,
7593 1
            'size' => 2 + $ln, // size in bytes of data structure
7594 1
        ];
7595
    }
7596
7597
    /**
7598
     * Extracts an Excel Unicode short string (8-bit string length)
7599
     * OpenOffice documentation: 2.5.3
7600
     * function will automatically find out where the Unicode string ends.
7601
     *
7602
     * @param string $subData
7603
     *
7604
     * @return array
7605
     */
7606 82
    private static function readUnicodeStringShort($subData)
7607
    {
7608 82
        $value = '';
7609
7610
        // offset: 0: size: 1; length of the string (character count)
7611 82
        $characterCount = ord($subData[0]);
7612
7613 82
        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
7614
7615
        // add 1 for the string length
7616 82
        ++$string['size'];
7617
7618 82
        return $string;
7619
    }
7620
7621
    /**
7622
     * Extracts an Excel Unicode long string (16-bit string length)
7623
     * OpenOffice documentation: 2.5.3
7624
     * this function is under construction, needs to support rich text, and Asian phonetic settings.
7625
     *
7626
     * @param string $subData
7627
     *
7628
     * @return array
7629
     */
7630 78
    private static function readUnicodeStringLong($subData)
7631
    {
7632 78
        $value = '';
7633
7634
        // offset: 0: size: 2; length of the string (character count)
7635 78
        $characterCount = self::getUInt2d($subData, 0);
7636
7637 78
        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
7638
7639
        // add 2 for the string length
7640 78
        $string['size'] += 2;
7641
7642 78
        return $string;
7643
    }
7644
7645
    /**
7646
     * Read Unicode string with no string length field, but with known character count
7647
     * this function is under construction, needs to support rich text, and Asian phonetic settings
7648
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3.
7649
     *
7650
     * @param string $subData
7651
     * @param int $characterCount
7652
     *
7653
     * @return array
7654
     */
7655 82
    private static function readUnicodeString($subData, $characterCount)
7656
    {
7657 82
        $value = '';
7658
7659
        // offset: 0: size: 1; option flags
7660
        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
7661 82
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
7662
7663
        // bit: 2; mask: 0x04; Asian phonetic settings
7664 82
        $hasAsian = (0x04) & ord($subData[0]) >> 2;
7665
7666
        // bit: 3; mask: 0x08; Rich-Text settings
7667 82
        $hasRichText = (0x08) & ord($subData[0]) >> 3;
7668
7669
        // offset: 1: size: var; character array
7670
        // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
7671
        // needs to be fixed
7672 82
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
7673
7674 82
        return [
7675 82
            'value' => $value,
7676 82
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
7677 82
        ];
7678
    }
7679
7680
    /**
7681
     * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas.
7682
     * Example:  hello"world  -->  "hello""world".
7683
     *
7684
     * @param string $value UTF-8 encoded string
7685
     *
7686
     * @return string
7687
     */
7688 12
    private static function UTF8toExcelDoubleQuoted($value)
7689
    {
7690 12
        return '"' . str_replace('"', '""', $value) . '"';
7691
    }
7692
7693
    /**
7694
     * Reads first 8 bytes of a string and return IEEE 754 float.
7695
     *
7696
     * @param string $data Binary string that is at least 8 bytes long
7697
     *
7698
     * @return float
7699
     */
7700 80
    private static function extractNumber($data)
7701
    {
7702 80
        $rknumhigh = self::getInt4d($data, 4);
7703 80
        $rknumlow = self::getInt4d($data, 0);
7704 80
        $sign = ($rknumhigh & (int) 0x80000000) >> 31;
7705 80
        $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
7706 80
        $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
7707 80
        $mantissalow1 = ($rknumlow & (int) 0x80000000) >> 31;
7708 80
        $mantissalow2 = ($rknumlow & 0x7fffffff);
7709 80
        $value = $mantissa / 2 ** (20 - $exp);
7710
7711 80
        if ($mantissalow1 != 0) {
7712 22
            $value += 1 / 2 ** (21 - $exp);
7713
        }
7714
7715 80
        $value += $mantissalow2 / 2 ** (52 - $exp);
7716 80
        if ($sign) {
7717 16
            $value *= -1;
7718
        }
7719
7720 80
        return $value;
7721
    }
7722
7723
    /**
7724
     * @param int $rknum
7725
     *
7726
     * @return float
7727
     */
7728 29
    private static function getIEEE754($rknum)
7729
    {
7730 29
        if (($rknum & 0x02) != 0) {
7731 7
            $value = $rknum >> 2;
7732
        } else {
7733
            // changes by mmp, info on IEEE754 encoding from
7734
            // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
7735
            // The RK format calls for using only the most significant 30 bits
7736
            // of the 64 bit floating point value. The other 34 bits are assumed
7737
            // to be 0 so we use the upper 30 bits of $rknum as follows...
7738 24
            $sign = ($rknum & (int) 0x80000000) >> 31;
7739 24
            $exp = ($rknum & 0x7ff00000) >> 20;
7740 24
            $mantissa = (0x100000 | ($rknum & 0x000ffffc));
7741 24
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
7742 24
            if ($sign) {
7743 10
                $value = -1 * $value;
7744
            }
7745
            //end of changes by mmp
7746
        }
7747 29
        if (($rknum & 0x01) != 0) {
7748 12
            $value /= 100;
7749
        }
7750
7751 29
        return $value;
7752
    }
7753
7754
    /**
7755
     * Get UTF-8 string from (compressed or uncompressed) UTF-16 string.
7756
     *
7757
     * @param string $string
7758
     * @param bool $compressed
7759
     *
7760
     * @return string
7761
     */
7762 82
    private static function encodeUTF16($string, $compressed = false)
7763
    {
7764 82
        if ($compressed) {
7765 47
            $string = self::uncompressByteString($string);
7766
        }
7767
7768 82
        return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE');
7769
    }
7770
7771
    /**
7772
     * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
7773
     *
7774
     * @param string $string
7775
     *
7776
     * @return string
7777
     */
7778 47
    private static function uncompressByteString($string)
7779
    {
7780 47
        $uncompressedString = '';
7781 47
        $strLen = strlen($string);
7782 47
        for ($i = 0; $i < $strLen; ++$i) {
7783 46
            $uncompressedString .= $string[$i] . "\0";
7784
        }
7785
7786 47
        return $uncompressedString;
7787
    }
7788
7789
    /**
7790
     * Convert string to UTF-8. Only used for BIFF5.
7791
     *
7792
     * @param string $string
7793
     *
7794
     * @return string
7795
     */
7796 1
    private function decodeCodepage($string)
7797
    {
7798 1
        return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage);
7799
    }
7800
7801
    /**
7802
     * Read 16-bit unsigned integer.
7803
     *
7804
     * @param string $data
7805
     * @param int $pos
7806
     *
7807
     * @return int
7808
     */
7809 83
    public static function getUInt2d($data, $pos)
7810
    {
7811 83
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
7812
    }
7813
7814
    /**
7815
     * Read 16-bit signed integer.
7816
     *
7817
     * @param string $data
7818
     * @param int $pos
7819
     *
7820
     * @return int
7821
     */
7822
    public static function getInt2d($data, $pos)
7823
    {
7824
        return unpack('s', $data[$pos] . $data[$pos + 1])[1];
7825
    }
7826
7827
    /**
7828
     * Read 32-bit signed integer.
7829
     *
7830
     * @param string $data
7831
     * @param int $pos
7832
     *
7833
     * @return int
7834
     */
7835 83
    public static function getInt4d($data, $pos)
7836
    {
7837
        // FIX: represent numbers correctly on 64-bit system
7838
        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
7839
        // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
7840 83
        $_or_24 = ord($data[$pos + 3]);
7841 83
        if ($_or_24 >= 128) {
7842
            // negative number
7843 30
            $_ord_24 = -abs((256 - $_or_24) << 24);
7844
        } else {
7845 83
            $_ord_24 = ($_or_24 & 127) << 24;
7846
        }
7847
7848 83
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
7849
    }
7850
7851 3
    private function parseRichText($is)
7852
    {
7853 3
        $value = new RichText();
7854 3
        $value->createText($is);
7855
7856 3
        return $value;
7857
    }
7858
7859
    /**
7860
     * Phpstan 1.4.4 complains that this property is never read.
7861
     * So, we might be able to get rid of it altogether.
7862
     * For now, however, this function makes it readable,
7863
     * which satisfies Phpstan.
7864
     *
7865
     * @codeCoverageIgnore
7866
     */
7867
    public function getMapCellStyleXfIndex(): array
7868
    {
7869
        return $this->mapCellStyleXfIndex;
7870
    }
7871
7872 15
    private function readCFHeader(): array
7873
    {
7874 15
        $length = self::getUInt2d($this->data, $this->pos + 2);
7875 15
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7876
7877
        // move stream pointer forward to next record
7878 15
        $this->pos += 4 + $length;
7879
7880 15
        if ($this->readDataOnly) {
7881 1
            return [];
7882
        }
7883
7884
        // offset: 0; size: 2; Rule Count
7885
//        $ruleCount = self::getUInt2d($recordData, 0);
7886
7887
        // offset: var; size: var; cell range address list with
7888 14
        $cellRangeAddressList = ($this->version == self::XLS_BIFF8)
7889 14
            ? $this->readBIFF8CellRangeAddressList(substr($recordData, 12))
7890 14
            : $this->readBIFF5CellRangeAddressList(substr($recordData, 12));
7891 14
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
7892
7893 14
        return $cellRangeAddresses;
7894
    }
7895
7896 15
    private function readCFRule(array $cellRangeAddresses): void
7897
    {
7898 15
        $length = self::getUInt2d($this->data, $this->pos + 2);
7899 15
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7900
7901
        // move stream pointer forward to next record
7902 15
        $this->pos += 4 + $length;
7903
7904 15
        if ($this->readDataOnly) {
7905 1
            return;
7906
        }
7907
7908
        // offset: 0; size: 2; Options
7909 14
        $cfRule = self::getUInt2d($recordData, 0);
7910
7911
        // bit: 8-15; mask: 0x00FF; type
7912 14
        $type = (0x00FF & $cfRule) >> 0;
7913 14
        $type = ConditionalFormatting::type($type);
7914
7915
        // bit: 0-7; mask: 0xFF00; type
7916 14
        $operator = (0xFF00 & $cfRule) >> 8;
7917 14
        $operator = ConditionalFormatting::operator($operator);
7918
7919 14
        if ($type === null || $operator === null) {
7920
            return;
7921
        }
7922
7923
        // offset: 2; size: 2; Size1
7924 14
        $size1 = self::getUInt2d($recordData, 2);
7925
7926
        // offset: 4; size: 2; Size2
7927 14
        $size2 = self::getUInt2d($recordData, 4);
7928
7929
        // offset: 6; size: 4; Options
7930 14
        $options = self::getInt4d($recordData, 6);
7931
7932 14
        $style = new Style();
7933 14
        $this->getCFStyleOptions($options, $style);
7934
7935 14
        $hasFontRecord = (bool) ((0x04000000 & $options) >> 26);
7936 14
        $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27);
7937 14
        $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28);
7938 14
        $hasFillRecord = (bool) ((0x20000000 & $options) >> 29);
7939 14
        $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30);
7940
7941 14
        $offset = 12;
7942
7943 14
        if ($hasFontRecord === true) {
7944 14
            $fontStyle = substr($recordData, $offset, 118);
7945 14
            $this->getCFFontStyle($fontStyle, $style);
7946 14
            $offset += 118;
7947
        }
7948
7949 14
        if ($hasAlignmentRecord === true) {
7950
            $alignmentStyle = substr($recordData, $offset, 8);
7951
            $this->getCFAlignmentStyle($alignmentStyle, $style);
7952
            $offset += 8;
7953
        }
7954
7955 14
        if ($hasBorderRecord === true) {
7956
            $borderStyle = substr($recordData, $offset, 8);
7957
            $this->getCFBorderStyle($borderStyle, $style);
7958
            $offset += 8;
7959
        }
7960
7961 14
        if ($hasFillRecord === true) {
7962 2
            $fillStyle = substr($recordData, $offset, 4);
7963 2
            $this->getCFFillStyle($fillStyle, $style);
7964 2
            $offset += 4;
7965
        }
7966
7967 14
        if ($hasProtectionRecord === true) {
7968
            $protectionStyle = substr($recordData, $offset, 4);
7969
            $this->getCFProtectionStyle($protectionStyle, $style);
7970
            $offset += 2;
7971
        }
7972
7973 14
        $formula1 = $formula2 = null;
7974 14
        if ($size1 > 0) {
7975 14
            $formula1 = $this->readCFFormula($recordData, $offset, $size1);
7976 14
            if ($formula1 === null) {
7977
                return;
7978
            }
7979
7980 14
            $offset += $size1;
7981
        }
7982
7983 14
        if ($size2 > 0) {
7984 5
            $formula2 = $this->readCFFormula($recordData, $offset, $size2);
7985 5
            if ($formula2 === null) {
7986
                return;
7987
            }
7988
7989 5
            $offset += $size2;
7990
        }
7991
7992 14
        $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style);
7993
    }
7994
7995 14
    private function getCFStyleOptions(int $options, Style $style): void
7996
    {
7997 14
    }
7998
7999 14
    private function getCFFontStyle(string $options, Style $style): void
8000
    {
8001 14
        $fontSize = self::getInt4d($options, 64);
8002 14
        if ($fontSize !== -1) {
8003 7
            $style->getFont()->setSize($fontSize / 20); // Convert twips to points
8004
        }
8005
8006 14
        $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold
8007 14
        $style->getFont()->setBold($bold);
8008
8009 14
        $color = self::getInt4d($options, 80);
8010
8011 14
        if ($color !== -1) {
8012 14
            $style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']);
8013
        }
8014
    }
8015
8016
    private function getCFAlignmentStyle(string $options, Style $style): void
8017
    {
8018
    }
8019
8020
    private function getCFBorderStyle(string $options, Style $style): void
8021
    {
8022
    }
8023
8024 2
    private function getCFFillStyle(string $options, Style $style): void
8025
    {
8026 2
        $fillPattern = self::getUInt2d($options, 0);
8027
        // bit: 10-15; mask: 0xFC00; type
8028 2
        $fillPattern = (0xFC00 & $fillPattern) >> 10;
8029 2
        $fillPattern = FillPattern::lookup($fillPattern);
8030 2
        $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern;
8031
8032 2
        if ($fillPattern !== Fill::FILL_NONE) {
8033 2
            $style->getFill()->setFillType($fillPattern);
8034
8035 2
            $fillColors = self::getUInt2d($options, 2);
8036
8037
            // bit: 0-6; mask: 0x007F; type
8038 2
            $color1 = (0x007F & $fillColors) >> 0;
8039 2
            $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color1, $this->palette, $this->version)['rgb']);
8040
8041
            // bit: 7-13; mask: 0x3F80; type
8042 2
            $color2 = (0x3F80 & $fillColors) >> 7;
8043 2
            $style->getFill()->getEndColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']);
8044
        }
8045
    }
8046
8047
    private function getCFProtectionStyle(string $options, Style $style): void
8048
    {
8049
    }
8050
8051
    /**
8052
     * @return null|float|int|string
8053
     */
8054 14
    private function readCFFormula(string $recordData, int $offset, int $size)
8055
    {
8056
        try {
8057 14
            $formula = substr($recordData, $offset, $size);
8058 14
            $formula = pack('v', $size) . $formula; // prepend the length
8059
8060 14
            $formula = $this->getFormulaFromStructure($formula);
8061 14
            if (is_numeric($formula)) {
8062 12
                return (strpos($formula, '.') !== false) ? (float) $formula : (int) $formula;
8063
            }
8064
8065 7
            return $formula;
8066
        } catch (PhpSpreadsheetException $e) {
8067
        }
8068
8069
        return null;
8070
    }
8071
8072
    /**
8073
     * @param null|float|int|string $formula1
8074
     * @param null|float|int|string $formula2
8075
     */
8076 14
    private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2, Style $style): void
8077
    {
8078 14
        foreach ($cellRanges as $cellRange) {
8079 14
            $conditional = new Conditional();
8080 14
            $conditional->setConditionType($type);
8081 14
            $conditional->setOperatorType($operator);
8082 14
            if ($formula1 !== null) {
8083 14
                $conditional->addCondition($formula1);
8084
            }
8085 14
            if ($formula2 !== null) {
8086 5
                $conditional->addCondition($formula2);
8087
            }
8088 14
            $conditional->setStyle($style);
8089
8090 14
            $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles();
8091 14
            $conditionalStyles[] = $conditional;
8092
8093 14
            $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles);
8094
        }
8095
    }
8096
}
8097