Completed
Push — master ( c2a964...f9b1b7 )
by
unknown
36s queued 26s
created

Xls::setEncryptionPassword()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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