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

Xls::readPalette()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 16
ccs 9
cts 9
cp 1
crap 3
rs 10
c 0
b 0
f 0
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