Xls::readPageSetup()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 51
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 23
nc 3
nop 0
dl 0
loc 51
ccs 24
cts 24
cp 1
crap 5
rs 9.2408
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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