Test Failed
Push — main ( c8394f...8477f1 )
by Rafael
66:21
created

Xls   F

Complexity

Total Complexity 1121

Size/Duplication

Total Lines 7895
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 3987
dl 0
loc 7895
rs 0.8
c 0
b 0
f 0
wmc 1121

How to fix   Complexity   

Complex Class

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

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

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

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