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

Xls::readHyperLink()   F

Complexity

Conditions 20
Paths 1082

Size

Total Lines 166
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 52.7438

Importance

Changes 0
Metric Value
cc 20
eloc 81
c 0
b 0
f 0
nc 1082
nop 0
dl 0
loc 166
ccs 43
cts 76
cp 0.5658
crap 52.7438
rs 0

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