Passed
Push — master ( 93fbf8...6caa0c )
by Adrien
08:47
created

Xls::readFont()   D

Complexity

Conditions 13
Paths 289

Size

Total Lines 91
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 20.1716

Importance

Changes 0
Metric Value
cc 13
eloc 50
c 0
b 0
f 0
nc 289
nop 0
dl 0
loc 91
ccs 28
cts 43
cp 0.6512
crap 20.1716
rs 4.6708

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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