• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 18013950625

25 Sep 2025 04:20PM UTC coverage: 95.867% (+0.3%) from 95.602%
18013950625

push

github

web-flow
Merge pull request #4663 from oleibman/tweakcoveralls

Tweak Coveralls

45116 of 47061 relevant lines covered (95.87%)

373.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.39
/src/PhpSpreadsheet/Reader/Xls.php
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 array<int, array{'externalBookIndex': int, 'firstSheetIndex': int, 'lastSheetIndex': int}>
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
    public function listWorksheetNames(string $filename): array
7✔
283
    {
284
        return (new Xls\ListFunctions())->listWorksheetNames2($filename, $this);
7✔
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
    public function listWorksheetInfo(string $filename): array
5✔
293
    {
294
        return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this);
5✔
295
    }
296

297
    /**
298
     * Loads PhpSpreadsheet from file.
299
     */
300
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
136✔
301
    {
302
        return (new Xls\LoadSpreadsheet())->loadSpreadsheetFromFile2($filename, $this);
136✔
303
    }
304

305
    /**
306
     * Read record data from stream, decrypting as required.
307
     *
308
     * @param string $data Data stream to read from
309
     * @param int $pos Position to start reading from
310
     * @param int $len Record data length
311
     *
312
     * @return string Record data
313
     */
314
    protected function readRecordData(string $data, int $pos, int $len): string
147✔
315
    {
316
        $data = substr($data, $pos, $len);
147✔
317

318
        // File not encrypted, or record before encryption start point
319
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
147✔
320
            return $data;
147✔
321
        }
322

323
        $recordData = '';
2✔
324
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
2✔
325
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
2✔
326
            $block = (int) floor($pos / self::REKEY_BLOCK);
2✔
327
            $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK);
2✔
328

329
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
330
            // at a point earlier in the current block, re-use it as we can save some time.
331
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
2✔
332
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
2✔
333
                $step = $pos % self::REKEY_BLOCK;
2✔
334
            } else {
335
                $step = $pos - $this->rc4Pos;
2✔
336
            }
337
            $this->rc4Key->RC4(str_repeat("\0", $step));
2✔
338

339
            // Decrypt record data (re-keying at the end of every block)
340
            while ($block != $endBlock) {
2✔
341
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1✔
342
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1✔
343
                $data = substr($data, $step);
1✔
344
                $pos += $step;
1✔
345
                $len -= $step;
1✔
346
                ++$block;
1✔
347
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1✔
348
            }
349
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
2✔
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
            $this->rc4Pos = $pos + $len;
2✔
354
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
×
355
            throw new Exception('XOr encryption not supported');
×
356
        }
357

358
        return $recordData;
2✔
359
    }
360

361
    /**
362
     * Use OLE reader to extract the relevant data streams from the OLE file.
363
     */
364
    protected function loadOLE(string $filename): void
147✔
365
    {
366
        // OLE reader
367
        $ole = new OLERead();
147✔
368
        // get excel data,
369
        $ole->read($filename);
147✔
370
        // Get workbook data: workbook stream + sheet streams
371
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
147✔
372
        // Get summary information data
373
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
147✔
374
        // Get additional document summary information data
375
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
147✔
376
    }
377

378
    /**
379
     * Read summary information.
380
     */
381
    protected function readSummaryInformation(): void
136✔
382
    {
383
        if (!isset($this->summaryInformation)) {
136✔
384
            return;
3✔
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
        $secOffset = self::getInt4d($this->summaryInformation, 44);
133✔
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
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
133✔
405

406
        // initialize code page (used to resolve string values)
407
        $codePage = 'CP1252';
133✔
408

409
        // offset: ($secOffset+8); size: var
410
        // loop through property decarations and properties
411
        for ($i = 0; $i < $countProperties; ++$i) {
133✔
412
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
413
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
133✔
414

415
            // Use value of property id as appropriate
416
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
417
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
133✔
418

419
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
133✔
420

421
            // initialize property value
422
            $value = null;
133✔
423

424
            // extract property value based on property type
425
            switch ($type) {
426
                case 0x02: // 2 byte signed integer
133✔
427
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
133✔
428

429
                    break;
133✔
430
                case 0x03: // 4 byte signed integer
133✔
431
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
129✔
432

433
                    break;
129✔
434
                case 0x13: // 4 byte unsigned integer
133✔
435
                    // not needed yet, fix later if necessary
436
                    break;
1✔
437
                case 0x1E: // null-terminated string prepended by dword string length
133✔
438
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
131✔
439
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
131✔
440
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
131✔
441
                    $value = rtrim($value);
131✔
442

443
                    break;
131✔
444
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
133✔
445
                    // PHP-time
446
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
133✔
447

448
                    break;
133✔
449
                case 0x47: // Clipboard format
2✔
450
                    // not needed yet, fix later if necessary
451
                    break;
×
452
            }
453

454
            switch ($id) {
455
                case 0x01:    //    Code Page
133✔
456
                    $codePage = CodePage::numberToName((int) $value);
133✔
457

458
                    break;
133✔
459
                case 0x02:    //    Title
133✔
460
                    $this->spreadsheet->getProperties()->setTitle("$value");
78✔
461

462
                    break;
78✔
463
                case 0x03:    //    Subject
133✔
464
                    $this->spreadsheet->getProperties()->setSubject("$value");
17✔
465

466
                    break;
17✔
467
                case 0x04:    //    Author (Creator)
133✔
468
                    $this->spreadsheet->getProperties()->setCreator("$value");
119✔
469

470
                    break;
119✔
471
                case 0x05:    //    Keywords
133✔
472
                    $this->spreadsheet->getProperties()->setKeywords("$value");
17✔
473

474
                    break;
17✔
475
                case 0x06:    //    Comments (Description)
133✔
476
                    $this->spreadsheet->getProperties()->setDescription("$value");
17✔
477

478
                    break;
17✔
479
                case 0x07:    //    Template
133✔
480
                    //    Not supported by PhpSpreadsheet
481
                    break;
×
482
                case 0x08:    //    Last Saved By (LastModifiedBy)
133✔
483
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
131✔
484

485
                    break;
131✔
486
                case 0x09:    //    Revision
133✔
487
                    //    Not supported by PhpSpreadsheet
488
                    break;
3✔
489
                case 0x0A:    //    Total Editing Time
133✔
490
                    //    Not supported by PhpSpreadsheet
491
                    break;
3✔
492
                case 0x0B:    //    Last Printed
133✔
493
                    //    Not supported by PhpSpreadsheet
494
                    break;
7✔
495
                case 0x0C:    //    Created Date/Time
133✔
496
                    $this->spreadsheet->getProperties()->setCreated($value);
126✔
497

498
                    break;
126✔
499
                case 0x0D:    //    Modified Date/Time
133✔
500
                    $this->spreadsheet->getProperties()->setModified($value);
132✔
501

502
                    break;
132✔
503
                case 0x0E:    //    Number of Pages
130✔
504
                    //    Not supported by PhpSpreadsheet
505
                    break;
×
506
                case 0x0F:    //    Number of Words
130✔
507
                    //    Not supported by PhpSpreadsheet
508
                    break;
×
509
                case 0x10:    //    Number of Characters
130✔
510
                    //    Not supported by PhpSpreadsheet
511
                    break;
×
512
                case 0x11:    //    Thumbnail
130✔
513
                    //    Not supported by PhpSpreadsheet
514
                    break;
×
515
                case 0x12:    //    Name of creating application
130✔
516
                    //    Not supported by PhpSpreadsheet
517
                    break;
49✔
518
                case 0x13:    //    Security
130✔
519
                    //    Not supported by PhpSpreadsheet
520
                    break;
129✔
521
            }
522
        }
523
    }
524

525
    /**
526
     * Read additional document summary information.
527
     */
528
    protected function readDocumentSummaryInformation(): void
136✔
529
    {
530
        if (!isset($this->documentSummaryInformation)) {
136✔
531
            return;
4✔
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
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
132✔
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
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
132✔
552

553
        // initialize code page (used to resolve string values)
554
        $codePage = 'CP1252';
132✔
555

556
        //    offset: ($secOffset+8);    size: var
557
        //    loop through property decarations and properties
558
        for ($i = 0; $i < $countProperties; ++$i) {
132✔
559
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
560
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
132✔
561

562
            // Use value of property id as appropriate
563
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
564
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
132✔
565

566
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
132✔
567

568
            // initialize property value
569
            $value = null;
132✔
570

571
            // extract property value based on property type
572
            switch ($type) {
573
                case 0x02:    //    2 byte signed integer
132✔
574
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
132✔
575

576
                    break;
132✔
577
                case 0x03:    //    4 byte signed integer
129✔
578
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
126✔
579

580
                    break;
126✔
581
                case 0x0B:  // Boolean
129✔
582
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
129✔
583
                    $value = ($value == 0 ? false : true);
129✔
584

585
                    break;
129✔
586
                case 0x13:    //    4 byte unsigned integer
128✔
587
                    // not needed yet, fix later if necessary
588
                    break;
1✔
589
                case 0x1E:    //    null-terminated string prepended by dword string length
127✔
590
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
51✔
591
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
51✔
592
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
51✔
593
                    $value = rtrim($value);
51✔
594

595
                    break;
51✔
596
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
127✔
597
                    // PHP-Time
598
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
×
599

600
                    break;
×
601
                case 0x47:    //    Clipboard format
127✔
602
                    // not needed yet, fix later if necessary
603
                    break;
×
604
            }
605

606
            switch ($id) {
607
                case 0x01:    //    Code Page
132✔
608
                    $codePage = CodePage::numberToName((int) $value);
132✔
609

610
                    break;
132✔
611
                case 0x02:    //    Category
129✔
612
                    $this->spreadsheet->getProperties()->setCategory("$value");
17✔
613

614
                    break;
17✔
615
                case 0x03:    //    Presentation Target
129✔
616
                    //    Not supported by PhpSpreadsheet
617
                    break;
×
618
                case 0x04:    //    Bytes
129✔
619
                    //    Not supported by PhpSpreadsheet
620
                    break;
×
621
                case 0x05:    //    Lines
129✔
622
                    //    Not supported by PhpSpreadsheet
623
                    break;
×
624
                case 0x06:    //    Paragraphs
129✔
625
                    //    Not supported by PhpSpreadsheet
626
                    break;
×
627
                case 0x07:    //    Slides
129✔
628
                    //    Not supported by PhpSpreadsheet
629
                    break;
×
630
                case 0x08:    //    Notes
129✔
631
                    //    Not supported by PhpSpreadsheet
632
                    break;
×
633
                case 0x09:    //    Hidden Slides
129✔
634
                    //    Not supported by PhpSpreadsheet
635
                    break;
×
636
                case 0x0A:    //    MM Clips
129✔
637
                    //    Not supported by PhpSpreadsheet
638
                    break;
×
639
                case 0x0B:    //    Scale Crop
129✔
640
                    //    Not supported by PhpSpreadsheet
641
                    break;
129✔
642
                case 0x0C:    //    Heading Pairs
129✔
643
                    //    Not supported by PhpSpreadsheet
644
                    break;
127✔
645
                case 0x0D:    //    Titles of Parts
129✔
646
                    //    Not supported by PhpSpreadsheet
647
                    break;
127✔
648
                case 0x0E:    //    Manager
129✔
649
                    $this->spreadsheet->getProperties()->setManager("$value");
2✔
650

651
                    break;
2✔
652
                case 0x0F:    //    Company
129✔
653
                    $this->spreadsheet->getProperties()->setCompany("$value");
41✔
654

655
                    break;
41✔
656
                case 0x10:    //    Links up-to-date
129✔
657
                    //    Not supported by PhpSpreadsheet
658
                    break;
129✔
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
    protected function readDefault(): void
145✔
667
    {
668
        $length = self::getUInt2d($this->data, $this->pos + 2);
145✔
669

670
        // move stream pointer to next record
671
        $this->pos += 4 + $length;
145✔
672
    }
673

674
    /**
675
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
676
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
677
     */
678
    protected function readNote(): void
3✔
679
    {
680
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
681
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
682

683
        // move stream pointer to next record
684
        $this->pos += 4 + $length;
3✔
685

686
        if ($this->readDataOnly) {
3✔
687
            return;
×
688
        }
689

690
        $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($recordData, 0, 4));
3✔
691
        if ($this->version == self::XLS_BIFF8) {
3✔
692
            $noteObjID = self::getUInt2d($recordData, 6);
2✔
693
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
2✔
694
            $noteAuthor = $noteAuthor['value'];
2✔
695
            $this->cellNotes[$noteObjID] = [
2✔
696
                'cellRef' => $cellAddress,
2✔
697
                'objectID' => $noteObjID,
2✔
698
                'author' => $noteAuthor,
2✔
699
            ];
2✔
700
        } else {
701
            $extension = false;
1✔
702
            if ($cellAddress == '$B$65536') {
1✔
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
            $cellAddress = str_replace('$', '', (string) $cellAddress);
1✔
713
            //$noteLength = self::getUInt2d($recordData, 4);
714
            $noteText = trim(substr($recordData, 6));
1✔
715

716
            if ($extension) {
1✔
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
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1✔
724
//                                                    ->setAuthor($author)
725
            }
726
        }
727
    }
728

729
    /**
730
     * The TEXT Object record contains the text associated with a cell annotation.
731
     */
732
    protected function readTextObject(): void
2✔
733
    {
734
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
735
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
736

737
        // move stream pointer to next record
738
        $this->pos += 4 + $length;
2✔
739

740
        if ($this->readDataOnly) {
2✔
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
        $grbitOpts = self::getUInt2d($recordData, 0);
2✔
751
        $rot = self::getUInt2d($recordData, 2);
2✔
752
        //$cchText = self::getUInt2d($recordData, 10);
753
        $cbRuns = self::getUInt2d($recordData, 12);
2✔
754
        $text = $this->getSplicedRecordData();
2✔
755

756
        /** @var int[] */
757
        $tempSplice = $text['spliceOffsets'];
2✔
758
        /** @var int */
759
        $temp = $tempSplice[0];
2✔
760
        /** @var int */
761
        $temp1 = $tempSplice[1];
2✔
762
        $textByte = $temp1 - $temp - 1;
2✔
763
        /** @var string */
764
        $textRecordData = $text['recordData'];
2✔
765
        $textStr = substr($textRecordData, $temp + 1, $textByte);
2✔
766
        // get 1 byte
767
        $is16Bit = ord($textRecordData[0]);
2✔
768
        // it is possible to use a compressed format,
769
        // which omits the high bytes of all characters, if they are all zero
770
        if (($is16Bit & 0x01) === 0) {
2✔
771
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
2✔
772
        } else {
773
            $textStr = $this->decodeCodepage($textStr);
×
774
        }
775

776
        $this->textObjects[$this->textObjRef] = [
2✔
777
            'text' => $textStr,
2✔
778
            'format' => substr($textRecordData, $tempSplice[1], $cbRuns),
2✔
779
            'alignment' => $grbitOpts,
2✔
780
            'rotation' => $rot,
2✔
781
        ];
2✔
782
    }
783

784
    /**
785
     * Read BOF.
786
     */
787
    protected function readBof(): void
147✔
788
    {
789
        $length = self::getUInt2d($this->data, $this->pos + 2);
147✔
790
        $recordData = substr($this->data, $this->pos + 4, $length);
147✔
791

792
        // move stream pointer to next record
793
        $this->pos += 4 + $length;
147✔
794

795
        // offset: 2; size: 2; type of the following data
796
        $substreamType = self::getUInt2d($recordData, 2);
147✔
797

798
        switch ($substreamType) {
799
            case self::XLS_WORKBOOKGLOBALS:
147✔
800
                $version = self::getUInt2d($recordData, 0);
147✔
801
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
147✔
802
                    throw new Exception('Cannot read this Excel file. Version is too old.');
×
803
                }
804
                $this->version = $version;
147✔
805

806
                break;
147✔
807
            case self::XLS_WORKSHEET:
136✔
808
                // do not use this version information for anything
809
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
810
                break;
136✔
811
            default:
812
                // substream, e.g. chart
813
                // just skip the entire substream
814
                do {
815
                    $code = self::getUInt2d($this->data, $this->pos);
×
816
                    $this->readDefault();
×
817
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
×
818

819
                break;
×
820
        }
821
    }
822

823
    public function setEncryptionPassword(string $encryptionPassword): self
1✔
824
    {
825
        $this->encryptionPassword = $encryptionPassword;
1✔
826

827
        return $this;
1✔
828
    }
829

830
    /**
831
     * FILEPASS.
832
     *
833
     * This record is part of the File Protection Block. It
834
     * contains information about the read/write password of the
835
     * file. All record contents following this record will be
836
     * encrypted.
837
     *
838
     * --    "OpenOffice.org's Documentation of the Microsoft
839
     *         Excel File Format"
840
     *
841
     * The decryption functions and objects used from here on in
842
     * are based on the source of Spreadsheet-ParseExcel:
843
     * https://metacpan.org/release/Spreadsheet-ParseExcel
844
     */
845
    protected function readFilepass(): void
4✔
846
    {
847
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
848

849
        if ($length < 54) {
4✔
850
            throw new Exception('Unexpected file pass record length');
×
851
        }
852

853
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
854

855
        // move stream pointer to next record
856
        $this->pos += 4 + $length;
4✔
857

858
        if (substr($recordData, 0, 2) !== "\x01\x00" || substr($recordData, 4, 2) !== "\x01\x00") {
4✔
859
            throw new Exception('Unsupported encryption algorithm');
1✔
860
        }
861
        if (!$this->verifyPassword($this->encryptionPassword, substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
3✔
862
            throw new Exception('Decryption password incorrect');
1✔
863
        }
864

865
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
2✔
866

867
        // Decryption required from the record after next onwards
868
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
2✔
869
    }
870

871
    /**
872
     * Make an RC4 decryptor for the given block.
873
     *
874
     * @param int $block Block for which to create decrypto
875
     * @param string $valContext MD5 context state
876
     */
877
    private function makeKey(int $block, string $valContext): Xls\RC4
3✔
878
    {
879
        $pwarray = str_repeat("\0", 64);
3✔
880

881
        for ($i = 0; $i < 5; ++$i) {
3✔
882
            $pwarray[$i] = $valContext[$i];
3✔
883
        }
884

885
        $pwarray[5] = chr($block & 0xFF);
3✔
886
        $pwarray[6] = chr(($block >> 8) & 0xFF);
3✔
887
        $pwarray[7] = chr(($block >> 16) & 0xFF);
3✔
888
        $pwarray[8] = chr(($block >> 24) & 0xFF);
3✔
889

890
        $pwarray[9] = "\x80";
3✔
891
        $pwarray[56] = "\x48";
3✔
892

893
        $md5 = new Xls\MD5();
3✔
894
        $md5->add($pwarray);
3✔
895

896
        $s = $md5->getContext();
3✔
897

898
        return new Xls\RC4($s);
3✔
899
    }
900

901
    /**
902
     * Verify RC4 file password.
903
     *
904
     * @param string $password Password to check
905
     * @param string $docid Document id
906
     * @param string $salt_data Salt data
907
     * @param string $hashedsalt_data Hashed salt data
908
     * @param string $valContext Set to the MD5 context of the value
909
     *
910
     * @return bool Success
911
     */
912
    private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, string &$valContext): bool
3✔
913
    {
914
        $pwarray = str_repeat("\0", 64);
3✔
915

916
        $iMax = strlen($password);
3✔
917
        for ($i = 0; $i < $iMax; ++$i) {
3✔
918
            $o = ord(substr($password, $i, 1));
3✔
919
            $pwarray[2 * $i] = chr($o & 0xFF);
3✔
920
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xFF);
3✔
921
        }
922
        $pwarray[2 * $i] = chr(0x80);
3✔
923
        $pwarray[56] = chr(($i << 4) & 0xFF);
3✔
924

925
        $md5 = new Xls\MD5();
3✔
926
        $md5->add($pwarray);
3✔
927

928
        $mdContext1 = $md5->getContext();
3✔
929

930
        $offset = 0;
3✔
931
        $keyoffset = 0;
3✔
932
        $tocopy = 5;
3✔
933

934
        $md5->reset();
3✔
935

936
        while ($offset != 16) {
3✔
937
            if ((64 - $offset) < 5) {
3✔
938
                $tocopy = 64 - $offset;
3✔
939
            }
940
            for ($i = 0; $i <= $tocopy; ++$i) {
3✔
941
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
3✔
942
            }
943
            $offset += $tocopy;
3✔
944

945
            if ($offset == 64) {
3✔
946
                $md5->add($pwarray);
3✔
947
                $keyoffset = $tocopy;
3✔
948
                $tocopy = 5 - $tocopy;
3✔
949
                $offset = 0;
3✔
950

951
                continue;
3✔
952
            }
953

954
            $keyoffset = 0;
3✔
955
            $tocopy = 5;
3✔
956
            for ($i = 0; $i < 16; ++$i) {
3✔
957
                $pwarray[$offset + $i] = $docid[$i];
3✔
958
            }
959
            $offset += 16;
3✔
960
        }
961

962
        $pwarray[16] = "\x80";
3✔
963
        for ($i = 0; $i < 47; ++$i) {
3✔
964
            $pwarray[17 + $i] = "\0";
3✔
965
        }
966
        $pwarray[56] = "\x80";
3✔
967
        $pwarray[57] = "\x0a";
3✔
968

969
        $md5->add($pwarray);
3✔
970
        $valContext = $md5->getContext();
3✔
971

972
        $key = $this->makeKey(0, $valContext);
3✔
973

974
        $salt = $key->RC4($salt_data);
3✔
975
        $hashedsalt = $key->RC4($hashedsalt_data);
3✔
976

977
        $salt .= "\x80" . str_repeat("\0", 47);
3✔
978
        $salt[56] = "\x80";
3✔
979

980
        $md5->reset();
3✔
981
        $md5->add($salt);
3✔
982
        $mdContext2 = $md5->getContext();
3✔
983

984
        return $mdContext2 == $hashedsalt;
3✔
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
    protected function readCodepage(): void
141✔
997
    {
998
        $length = self::getUInt2d($this->data, $this->pos + 2);
141✔
999
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
141✔
1000

1001
        // move stream pointer to next record
1002
        $this->pos += 4 + $length;
141✔
1003

1004
        // offset: 0; size: 2; code page identifier
1005
        $codepage = self::getUInt2d($recordData, 0);
141✔
1006

1007
        $this->codepage = CodePage::numberToName($codepage);
141✔
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
    protected function readDateMode(): void
133✔
1023
    {
1024
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
1025
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
1026

1027
        // move stream pointer to next record
1028
        $this->pos += 4 + $length;
133✔
1029

1030
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
1031
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
133✔
1032
        $this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
133✔
1033
        if (ord($recordData[0]) == 1) {
133✔
1034
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1035
            $this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1036
        }
1037
    }
1038

1039
    /**
1040
     * Read a FONT record.
1041
     */
1042
    protected function readFont(): void
133✔
1043
    {
1044
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
1045
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
1046

1047
        // move stream pointer to next record
1048
        $this->pos += 4 + $length;
133✔
1049

1050
        if (!$this->readDataOnly) {
133✔
1051
            $objFont = new Font();
131✔
1052

1053
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
1054
            $size = self::getUInt2d($recordData, 0);
131✔
1055
            $objFont->setSize($size / 20);
131✔
1056

1057
            // offset: 2; size: 2; option flags
1058
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
1059
            // bit: 1; mask 0x0002; italic
1060
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
131✔
1061
            if ($isItalic) {
131✔
1062
                $objFont->setItalic(true);
57✔
1063
            }
1064

1065
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
1066
            // bit: 3; mask 0x0008; strikethrough
1067
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
131✔
1068
            if ($isStrike) {
131✔
1069
                $objFont->setStrikethrough(true);
×
1070
            }
1071

1072
            // offset: 4; size: 2; colour index
1073
            $colorIndex = self::getUInt2d($recordData, 4);
131✔
1074
            $objFont->colorIndex = $colorIndex;
131✔
1075

1076
            // offset: 6; size: 2; font weight
1077
            $weight = self::getUInt2d($recordData, 6); // regular=400 bold=700
131✔
1078
            if ($weight >= 550) {
131✔
1079
                $objFont->setBold(true);
68✔
1080
            }
1081

1082
            // offset: 8; size: 2; escapement type
1083
            $escapement = self::getUInt2d($recordData, 8);
131✔
1084
            CellFont::escapement($objFont, $escapement);
131✔
1085

1086
            // offset: 10; size: 1; underline type
1087
            $underlineType = ord($recordData[10]);
131✔
1088
            CellFont::underline($objFont, $underlineType);
131✔
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
            if ($this->version == self::XLS_BIFF8) {
131✔
1095
                $string = self::readUnicodeStringShort(substr($recordData, 14));
129✔
1096
            } else {
1097
                $string = $this->readByteStringShort(substr($recordData, 14));
2✔
1098
            }
1099
            /** @var string[] $string */
1100
            $objFont->setName($string['value']);
131✔
1101

1102
            $this->objFonts[] = $objFont;
131✔
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
    protected function readFormat(): void
70✔
1121
    {
1122
        $length = self::getUInt2d($this->data, $this->pos + 2);
70✔
1123
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
70✔
1124

1125
        // move stream pointer to next record
1126
        $this->pos += 4 + $length;
70✔
1127

1128
        if (!$this->readDataOnly) {
70✔
1129
            $indexCode = self::getUInt2d($recordData, 0);
68✔
1130

1131
            if ($this->version == self::XLS_BIFF8) {
68✔
1132
                $string = self::readUnicodeStringLong(substr($recordData, 2));
66✔
1133
            } else {
1134
                // BIFF7
1135
                $string = $this->readByteStringShort(substr($recordData, 2));
2✔
1136
            }
1137

1138
            $formatString = $string['value'];
68✔
1139
            // Apache Open Office sets wrong case writing to xls - issue 2239
1140
            if ($formatString === 'GENERAL') {
68✔
1141
                $formatString = NumberFormat::FORMAT_GENERAL;
1✔
1142
            }
1143
            $this->formats[$indexCode] = $formatString;
68✔
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
    protected function readXf(): void
134✔
1162
    {
1163
        $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
1164
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
1165

1166
        // move stream pointer to next record
1167
        $this->pos += 4 + $length;
134✔
1168

1169
        $objStyle = new Style();
134✔
1170

1171
        if (!$this->readDataOnly) {
134✔
1172
            // offset:  0; size: 2; Index to FONT record
1173
            if (self::getUInt2d($recordData, 0) < 4) {
132✔
1174
                $fontIndex = self::getUInt2d($recordData, 0);
132✔
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
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
62✔
1179
            }
1180
            if (isset($this->objFonts[$fontIndex])) {
132✔
1181
                $objStyle->setFont($this->objFonts[$fontIndex]);
131✔
1182
            }
1183

1184
            // offset:  2; size: 2; Index to FORMAT record
1185
            $numberFormatIndex = self::getUInt2d($recordData, 2);
132✔
1186
            if (isset($this->formats[$numberFormatIndex])) {
132✔
1187
                // then we have user-defined format code
1188
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
62✔
1189
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
132✔
1190
                // then we have built-in format code
1191
                $numberFormat = ['formatCode' => $code];
132✔
1192
            } else {
1193
                // we set the general format code
1194
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
4✔
1195
            }
1196
            /** @var string[] $numberFormat */
1197
            $objStyle->getNumberFormat()
132✔
1198
                ->setFormatCode($numberFormat['formatCode']);
132✔
1199

1200
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
1201
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
1202
            $xfTypeProt = self::getUInt2d($recordData, 4);
132✔
1203
            // bit 0; mask 0x01; 1 = cell is locked
1204
            $isLocked = (0x01 & $xfTypeProt) >> 0;
132✔
1205
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
132✔
1206

1207
            // bit 1; mask 0x02; 1 = Formula is hidden
1208
            $isHidden = (0x02 & $xfTypeProt) >> 1;
132✔
1209
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
132✔
1210

1211
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
1212
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
132✔
1213

1214
            // offset:  6; size: 1; Alignment and text break
1215
            // bit 2-0, mask 0x07; horizontal alignment
1216
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
132✔
1217
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
132✔
1218

1219
            // bit 3, mask 0x08; wrap text
1220
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
132✔
1221
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
132✔
1222

1223
            // bit 6-4, mask 0x70; vertical alignment
1224
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
132✔
1225
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
132✔
1226

1227
            if ($this->version == self::XLS_BIFF8) {
132✔
1228
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
1229
                $angle = ord($recordData[7]);
130✔
1230
                $rotation = 0;
130✔
1231
                if ($angle <= 90) {
130✔
1232
                    $rotation = $angle;
130✔
1233
                } elseif ($angle <= 180) {
4✔
1234
                    $rotation = 90 - $angle;
×
1235
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
4✔
1236
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
4✔
1237
                }
1238
                $objStyle->getAlignment()->setTextRotation($rotation);
130✔
1239

1240
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
1241
                // bit: 3-0; mask: 0x0F; indent level
1242
                $indent = (0x0F & ord($recordData[8])) >> 0;
130✔
1243
                $objStyle->getAlignment()->setIndent($indent);
130✔
1244

1245
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
1246
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
130✔
1247
                switch ($shrinkToFit) {
1248
                    case 0:
130✔
1249
                        $objStyle->getAlignment()->setShrinkToFit(false);
130✔
1250

1251
                        break;
130✔
1252
                    case 1:
1✔
1253
                        $objStyle->getAlignment()->setShrinkToFit(true);
1✔
1254

1255
                        break;
1✔
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
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
130✔
1263
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
130✔
1264
                }
1265
                // bit: 7-4; mask: 0x000000F0; right style
1266
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
130✔
1267
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
130✔
1268
                }
1269
                // bit: 11-8; mask: 0x00000F00; top style
1270
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
130✔
1271
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
130✔
1272
                }
1273
                // bit: 15-12; mask: 0x0000F000; bottom style
1274
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
130✔
1275
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
130✔
1276
                }
1277
                // bit: 22-16; mask: 0x007F0000; left color
1278
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
130✔
1279

1280
                // bit: 29-23; mask: 0x3F800000; right color
1281
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
130✔
1282

1283
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
1284
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
130✔
1285

1286
                // bit: 31; mask: 0x800000; 1 = diagonal line from bottom left to top right
1287
                $diagonalUp = (self::HIGH_ORDER_BIT & self::getInt4d($recordData, 10)) >> 31 ? true : false;
130✔
1288

1289
                if ($diagonalUp === false) {
130✔
1290
                    if ($diagonalDown === false) {
130✔
1291
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
130✔
1292
                    } else {
1293
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
1✔
1294
                    }
1295
                } elseif ($diagonalDown === false) {
1✔
1296
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
1✔
1297
                } else {
1298
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
1✔
1299
                }
1300

1301
                // offset: 14; size: 4;
1302
                // bit: 6-0; mask: 0x0000007F; top color
1303
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
130✔
1304

1305
                // bit: 13-7; mask: 0x00003F80; bottom color
1306
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
130✔
1307

1308
                // bit: 20-14; mask: 0x001FC000; diagonal color
1309
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
130✔
1310

1311
                // bit: 24-21; mask: 0x01E00000; diagonal style
1312
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
130✔
1313
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
130✔
1314
                }
1315

1316
                // bit: 31-26; mask: 0xFC000000 fill pattern
1317
                if ($fillType = FillPattern::lookup((self::FC000000 & self::getInt4d($recordData, 14)) >> 26)) {
130✔
1318
                    $objStyle->getFill()->setFillType($fillType);
130✔
1319
                }
1320
                // offset: 18; size: 2; pattern and background colour
1321
                // bit: 6-0; mask: 0x007F; color index for pattern color
1322
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
130✔
1323

1324
                // bit: 13-7; mask: 0x3F80; color index for pattern background
1325
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
130✔
1326
            } else {
1327
                // BIFF5
1328

1329
                // offset: 7; size: 1; Text orientation and flags
1330
                $orientationAndFlags = ord($recordData[7]);
2✔
1331

1332
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
1333
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2✔
1334
                switch ($xfOrientation) {
1335
                    case 0:
2✔
1336
                        $objStyle->getAlignment()->setTextRotation(0);
2✔
1337

1338
                        break;
2✔
1339
                    case 1:
1✔
1340
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
1✔
1341

1342
                        break;
1✔
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
                $borderAndBackground = self::getInt4d($recordData, 8);
2✔
1355

1356
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
1357
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2✔
1358

1359
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
1360
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2✔
1361

1362
                // bit: 21-16; mask: 0x003F0000; fill pattern
1363
                $objStyle->getFill()->setFillType(FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2✔
1364

1365
                // bit: 24-22; mask: 0x01C00000; bottom line style
1366
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2✔
1367

1368
                // bit: 31-25; mask: 0xFE000000; bottom line color
1369
                $objStyle->getBorders()->getBottom()->colorIndex = (self::FE000000 & $borderAndBackground) >> 25;
2✔
1370

1371
                // offset: 12; size: 4; cell border lines
1372
                $borderLines = self::getInt4d($recordData, 12);
2✔
1373

1374
                // bit: 2-0; mask: 0x00000007; top line style
1375
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2✔
1376

1377
                // bit: 5-3; mask: 0x00000038; left line style
1378
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2✔
1379

1380
                // bit: 8-6; mask: 0x000001C0; right line style
1381
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2✔
1382

1383
                // bit: 15-9; mask: 0x0000FE00; top line color index
1384
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2✔
1385

1386
                // bit: 22-16; mask: 0x007F0000; left line color index
1387
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2✔
1388

1389
                // bit: 29-23; mask: 0x3F800000; right line color index
1390
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2✔
1391
            }
1392

1393
            // add cellStyleXf or cellXf and update mapping
1394
            if ($isCellStyleXf) {
132✔
1395
                // we only read one style XF record which is always the first
1396
                if ($this->xfIndex == 0) {
132✔
1397
                    $this->spreadsheet->addCellStyleXf($objStyle);
132✔
1398
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
132✔
1399
                }
1400
            } else {
1401
                // we read all cell XF records
1402
                $this->spreadsheet->addCellXf($objStyle);
132✔
1403
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
132✔
1404
            }
1405

1406
            // update XF index for when we read next record
1407
            ++$this->xfIndex;
132✔
1408
        }
1409
    }
1410

1411
    protected function readXfExt(): void
53✔
1412
    {
1413
        $length = self::getUInt2d($this->data, $this->pos + 2);
53✔
1414
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
53✔
1415

1416
        // move stream pointer to next record
1417
        $this->pos += 4 + $length;
53✔
1418

1419
        if (!$this->readDataOnly) {
53✔
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
            $ixfe = self::getUInt2d($recordData, 14);
51✔
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
            $offset = 20;
51✔
1438
            while ($offset < $length) {
51✔
1439
                // extension type
1440
                $extType = self::getUInt2d($recordData, $offset);
51✔
1441

1442
                // extension length
1443
                $cb = self::getUInt2d($recordData, $offset + 2);
51✔
1444

1445
                // extension data
1446
                $extData = substr($recordData, $offset + 4, $cb);
51✔
1447

1448
                switch ($extType) {
1449
                    case 4:        // fill start color
51✔
1450
                        $xclfType = self::getUInt2d($extData, 0); // color type
51✔
1451
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
51✔
1452

1453
                        if ($xclfType == 2) {
51✔
1454
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
49✔
1455

1456
                            // modify the relevant style property
1457
                            if (isset($this->mapCellXfIndex[$ixfe])) {
49✔
1458
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
7✔
1459
                                $fill->getStartColor()->setRGB($rgb);
7✔
1460
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
7✔
1461
                            }
1462
                        }
1463

1464
                        break;
51✔
1465
                    case 5:        // fill end color
49✔
1466
                        $xclfType = self::getUInt2d($extData, 0); // color type
5✔
1467
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
5✔
1468

1469
                        if ($xclfType == 2) {
5✔
1470
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
5✔
1471

1472
                            // modify the relevant style property
1473
                            if (isset($this->mapCellXfIndex[$ixfe])) {
5✔
1474
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
5✔
1475
                                $fill->getEndColor()->setRGB($rgb);
5✔
1476
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
5✔
1477
                            }
1478
                        }
1479

1480
                        break;
5✔
1481
                    case 7:        // border color top
49✔
1482
                        $xclfType = self::getUInt2d($extData, 0); // color type
49✔
1483
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
49✔
1484

1485
                        if ($xclfType == 2) {
49✔
1486
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
49✔
1487

1488
                            // modify the relevant style property
1489
                            if (isset($this->mapCellXfIndex[$ixfe])) {
49✔
1490
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
4✔
1491
                                $top->getColor()->setRGB($rgb);
4✔
1492
                                $top->colorIndex = null; // normal color index does not apply, discard
4✔
1493
                            }
1494
                        }
1495

1496
                        break;
49✔
1497
                    case 8:        // border color bottom
49✔
1498
                        $xclfType = self::getUInt2d($extData, 0); // color type
49✔
1499
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
49✔
1500

1501
                        if ($xclfType == 2) {
49✔
1502
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
49✔
1503

1504
                            // modify the relevant style property
1505
                            if (isset($this->mapCellXfIndex[$ixfe])) {
49✔
1506
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
5✔
1507
                                $bottom->getColor()->setRGB($rgb);
5✔
1508
                                $bottom->colorIndex = null; // normal color index does not apply, discard
5✔
1509
                            }
1510
                        }
1511

1512
                        break;
49✔
1513
                    case 9:        // border color left
49✔
1514
                        $xclfType = self::getUInt2d($extData, 0); // color type
49✔
1515
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
49✔
1516

1517
                        if ($xclfType == 2) {
49✔
1518
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
49✔
1519

1520
                            // modify the relevant style property
1521
                            if (isset($this->mapCellXfIndex[$ixfe])) {
49✔
1522
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
4✔
1523
                                $left->getColor()->setRGB($rgb);
4✔
1524
                                $left->colorIndex = null; // normal color index does not apply, discard
4✔
1525
                            }
1526
                        }
1527

1528
                        break;
49✔
1529
                    case 10:        // border color right
49✔
1530
                        $xclfType = self::getUInt2d($extData, 0); // color type
49✔
1531
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
49✔
1532

1533
                        if ($xclfType == 2) {
49✔
1534
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
49✔
1535

1536
                            // modify the relevant style property
1537
                            if (isset($this->mapCellXfIndex[$ixfe])) {
49✔
1538
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
4✔
1539
                                $right->getColor()->setRGB($rgb);
4✔
1540
                                $right->colorIndex = null; // normal color index does not apply, discard
4✔
1541
                            }
1542
                        }
1543

1544
                        break;
49✔
1545
                    case 11:        // border color diagonal
49✔
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
                    case 13:    // font color
49✔
1562
                        $xclfType = self::getUInt2d($extData, 0); // color type
49✔
1563
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
49✔
1564

1565
                        if ($xclfType == 2) {
49✔
1566
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
49✔
1567

1568
                            // modify the relevant style property
1569
                            if (isset($this->mapCellXfIndex[$ixfe])) {
49✔
1570
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
9✔
1571
                                $font->getColor()->setRGB($rgb);
9✔
1572
                                $font->colorIndex = null; // normal color index does not apply, discard
9✔
1573
                            }
1574
                        }
1575

1576
                        break;
49✔
1577
                }
1578

1579
                $offset += $cb;
51✔
1580
            }
1581
        }
1582
    }
1583

1584
    /**
1585
     * Read STYLE record.
1586
     */
1587
    protected function readStyle(): void
134✔
1588
    {
1589
        $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
1590
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
1591

1592
        // move stream pointer to next record
1593
        $this->pos += 4 + $length;
134✔
1594

1595
        if (!$this->readDataOnly) {
134✔
1596
            // offset: 0; size: 2; index to XF record and flag for built-in style
1597
            $ixfe = self::getUInt2d($recordData, 0);
132✔
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
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
132✔
1604

1605
            if ($isBuiltIn) {
132✔
1606
                // offset: 2; size: 1; identifier for built-in style
1607
                $builtInId = ord($recordData[2]);
132✔
1608

1609
                switch ($builtInId) {
1610
                    case 0x00:
132✔
1611
                        // currently, we are not using this for anything
1612
                        break;
132✔
1613
                    default:
1614
                        break;
59✔
1615
                }
1616
            }
1617
            // user-defined; not supported by PhpSpreadsheet
1618
        }
1619
    }
1620

1621
    /**
1622
     * Read PALETTE record.
1623
     */
1624
    protected function readPalette(): void
89✔
1625
    {
1626
        $length = self::getUInt2d($this->data, $this->pos + 2);
89✔
1627
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
89✔
1628

1629
        // move stream pointer to next record
1630
        $this->pos += 4 + $length;
89✔
1631

1632
        if (!$this->readDataOnly) {
89✔
1633
            // offset: 0; size: 2; number of following colors
1634
            $nm = self::getUInt2d($recordData, 0);
89✔
1635

1636
            // list of RGB colors
1637
            for ($i = 0; $i < $nm; ++$i) {
89✔
1638
                $rgb = substr($recordData, 2 + 4 * $i, 4);
89✔
1639
                $this->palette[] = self::readRGB($rgb);
89✔
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
    protected function readSheet(): void
145✔
1657
    {
1658
        $length = self::getUInt2d($this->data, $this->pos + 2);
145✔
1659
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
145✔
1660

1661
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
1662
        // NOTE: not encrypted
1663
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
145✔
1664

1665
        // move stream pointer to next record
1666
        $this->pos += 4 + $length;
145✔
1667

1668
        // offset: 4; size: 1; sheet state
1669
        $sheetState = match (ord($recordData[4])) {
145✔
1670
            0x00 => Worksheet::SHEETSTATE_VISIBLE,
145✔
1671
            0x01 => Worksheet::SHEETSTATE_HIDDEN,
6✔
1672
            0x02 => Worksheet::SHEETSTATE_VERYHIDDEN,
2✔
1673
            default => Worksheet::SHEETSTATE_VISIBLE,
×
1674
        };
145✔
1675

1676
        // offset: 5; size: 1; sheet type
1677
        $sheetType = ord($recordData[5]);
145✔
1678

1679
        // offset: 6; size: var; sheet name
1680
        $rec_name = null;
145✔
1681
        if ($this->version == self::XLS_BIFF8) {
145✔
1682
            $string = self::readUnicodeStringShort(substr($recordData, 6));
138✔
1683
            $rec_name = $string['value'];
138✔
1684
        } elseif ($this->version == self::XLS_BIFF7) {
7✔
1685
            $string = $this->readByteStringShort(substr($recordData, 6));
7✔
1686
            $rec_name = $string['value'];
7✔
1687
        }
1688
        /** @var string $rec_name */
1689
        $this->sheets[] = [
145✔
1690
            'name' => $rec_name,
145✔
1691
            'offset' => $rec_offset,
145✔
1692
            'sheetState' => $sheetState,
145✔
1693
            'sheetType' => $sheetType,
145✔
1694
        ];
145✔
1695
    }
1696

1697
    /**
1698
     * Read EXTERNALBOOK record.
1699
     */
1700
    protected function readExternalBook(): void
103✔
1701
    {
1702
        $length = self::getUInt2d($this->data, $this->pos + 2);
103✔
1703
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
103✔
1704

1705
        // move stream pointer to next record
1706
        $this->pos += 4 + $length;
103✔
1707

1708
        // offset within record data
1709
        $offset = 0;
103✔
1710

1711
        // there are 4 types of records
1712
        if (strlen($recordData) > 4) {
103✔
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
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
103✔
1737
            // internal reference
1738
            // offset: 0; size: 2; number of sheet in this document
1739
            // offset: 2; size: 2; 0x01 0x04
1740
            $this->externalBooks[] = [
103✔
1741
                'type' => 'internal',
103✔
1742
            ];
103✔
1743
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
1✔
1744
            // add-in function
1745
            // offset: 0; size: 2; 0x0001
1746
            $this->externalBooks[] = [
1✔
1747
                'type' => 'addInFunction',
1✔
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
    protected function readExternName(): void
1✔
1763
    {
1764
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
1765
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
1766

1767
        // move stream pointer to next record
1768
        $this->pos += 4 + $length;
1✔
1769

1770
        // external sheet references provided for named cells
1771
        if ($this->version == self::XLS_BIFF8) {
1✔
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
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
1✔
1781

1782
            // offset: var; size: var; formula data
1783
            $offset = 6 + $nameString['size'];
1✔
1784
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
1✔
1785

1786
            $this->externalNames[] = [
1✔
1787
                'name' => $nameString['value'],
1✔
1788
                'formula' => $formula,
1✔
1789
            ];
1✔
1790
        }
1791
    }
1792

1793
    /**
1794
     * Read EXTERNSHEET record.
1795
     */
1796
    protected function readExternSheet(): void
104✔
1797
    {
1798
        $length = self::getUInt2d($this->data, $this->pos + 2);
104✔
1799
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
104✔
1800

1801
        // move stream pointer to next record
1802
        $this->pos += 4 + $length;
104✔
1803

1804
        // external sheet references provided for named cells
1805
        if ($this->version == self::XLS_BIFF8) {
104✔
1806
            // offset: 0; size: 2; number of following ref structures
1807
            $nm = self::getUInt2d($recordData, 0);
103✔
1808
            for ($i = 0; $i < $nm; ++$i) {
103✔
1809
                $this->ref[] = [
101✔
1810
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
1811
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
101✔
1812
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
1813
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
101✔
1814
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
1815
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
101✔
1816
                ];
101✔
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
    protected function readDefinedName(): void
20✔
1833
    {
1834
        $length = self::getUInt2d($this->data, $this->pos + 2);
20✔
1835
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
20✔
1836

1837
        // move stream pointer to next record
1838
        $this->pos += 4 + $length;
20✔
1839

1840
        if ($this->version == self::XLS_BIFF8) {
20✔
1841
            // retrieves named cells
1842

1843
            // offset: 0; size: 2; option flags
1844
            $opts = self::getUInt2d($recordData, 0);
19✔
1845

1846
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
1847
            $isBuiltInName = (0x0020 & $opts) >> 5;
19✔
1848

1849
            // offset: 2; size: 1; keyboard shortcut
1850

1851
            // offset: 3; size: 1; length of the name (character count)
1852
            $nlen = ord($recordData[3]);
19✔
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
            $flen = self::getUInt2d($recordData, 4);
19✔
1857

1858
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
1859
            $scope = self::getUInt2d($recordData, 8);
19✔
1860

1861
            // offset: 14; size: var; Name (Unicode string without length field)
1862
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
19✔
1863

1864
            // offset: var; size: $flen; formula data
1865
            $offset = 14 + $string['size'];
19✔
1866
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
19✔
1867

1868
            try {
1869
                $formula = $this->getFormulaFromStructure($formulaStructure);
19✔
1870
            } catch (PhpSpreadsheetException) {
1✔
1871
                $formula = '';
1✔
1872
                $isBuiltInName = 0;
1✔
1873
            }
1874

1875
            $this->definedname[] = [
19✔
1876
                'isBuiltInName' => $isBuiltInName,
19✔
1877
                'name' => $string['value'],
19✔
1878
                'formula' => $formula,
19✔
1879
                'scope' => $scope,
19✔
1880
            ];
19✔
1881
        }
1882
    }
1883

1884
    /**
1885
     * Read MSODRAWINGGROUP record.
1886
     */
1887
    protected function readMsoDrawingGroup(): void
21✔
1888
    {
1889
        //$length = self::getUInt2d($this->data, $this->pos + 2);
1890

1891
        // get spliced record data
1892
        $splicedRecordData = $this->getSplicedRecordData();
21✔
1893
        /** @var string */
1894
        $recordData = $splicedRecordData['recordData'];
21✔
1895

1896
        $this->drawingGroupData .= $recordData;
21✔
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
    protected function readSst(): void
128✔
1911
    {
1912
        // offset within (spliced) record data
1913
        $pos = 0;
128✔
1914

1915
        // Limit global SST position, further control for bad SST Length in BIFF8 data
1916
        $limitposSST = 0;
128✔
1917

1918
        // get spliced record data
1919
        $splicedRecordData = $this->getSplicedRecordData();
128✔
1920

1921
        $recordData = $splicedRecordData['recordData'];
128✔
1922
        /** @var mixed[] */
1923
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
128✔
1924

1925
        // offset: 0; size: 4; total number of strings in the workbook
1926
        $pos += 4;
128✔
1927

1928
        // offset: 4; size: 4; number of following strings ($nm)
1929
        /** @var string $recordData */
1930
        $nm = self::getInt4d($recordData, 4);
128✔
1931
        $pos += 4;
128✔
1932

1933
        // look up limit position
1934
        foreach ($spliceOffsets as $spliceOffset) {
128✔
1935
            // it can happen that the string is empty, therefore we need
1936
            // <= and not just <
1937
            if ($pos <= $spliceOffset) {
128✔
1938
                $limitposSST = $spliceOffset;
128✔
1939
            }
1940
        }
1941

1942
        // loop through the Unicode strings (16-bit length)
1943
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
128✔
1944
            // number of characters in the Unicode string
1945
            /** @var int $pos */
1946
            $numChars = self::getUInt2d($recordData, $pos);
78✔
1947
            /** @var int $pos */
1948
            $pos += 2;
78✔
1949

1950
            // option flags
1951
            /** @var string $recordData */
1952
            $optionFlags = ord($recordData[$pos]);
78✔
1953
            ++$pos;
78✔
1954

1955
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
1956
            $isCompressed = (($optionFlags & 0x01) == 0);
78✔
1957

1958
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
1959
            $hasAsian = (($optionFlags & 0x04) != 0);
78✔
1960

1961
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
1962
            $hasRichText = (($optionFlags & 0x08) != 0);
78✔
1963

1964
            $formattingRuns = 0;
78✔
1965
            if ($hasRichText) {
78✔
1966
                // number of Rich-Text formatting runs
1967
                $formattingRuns = self::getUInt2d($recordData, $pos);
8✔
1968
                $pos += 2;
8✔
1969
            }
1970

1971
            $extendedRunLength = 0;
78✔
1972
            if ($hasAsian) {
78✔
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
            $len = ($isCompressed) ? $numChars : $numChars * 2;
78✔
1980

1981
            // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
1982
            $limitpos = null;
78✔
1983
            foreach ($spliceOffsets as $spliceOffset) {
78✔
1984
                // it can happen that the string is empty, therefore we need
1985
                // <= and not just <
1986
                if ($pos <= $spliceOffset) {
78✔
1987
                    $limitpos = $spliceOffset;
78✔
1988

1989
                    break;
78✔
1990
                }
1991
            }
1992

1993
            /** @var int $limitpos */
1994
            if ($pos + $len <= $limitpos) {
78✔
1995
                // character array is not split between records
1996

1997
                $retstr = substr($recordData, $pos, $len);
78✔
1998
                $pos += $len;
78✔
1999
            } else {
2000
                // character array is split between records
2001

2002
                // first part of character array
2003
                $retstr = substr($recordData, $pos, $limitpos - $pos);
1✔
2004

2005
                $bytesRead = $limitpos - $pos;
1✔
2006

2007
                // remaining characters in Unicode string
2008
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
1✔
2009

2010
                $pos = $limitpos;
1✔
2011

2012
                // keep reading the characters
2013
                while ($charsLeft > 0) {
1✔
2014
                    // look up next limit position, in case the string span more than one continue record
2015
                    foreach ($spliceOffsets as $spliceOffset) {
1✔
2016
                        if ($pos < $spliceOffset) {
1✔
2017
                            $limitpos = $spliceOffset;
1✔
2018

2019
                            break;
1✔
2020
                        }
2021
                    }
2022

2023
                    // repeated option flags
2024
                    // OpenOffice.org documentation 5.21
2025
                    /** @var int $pos */
2026
                    $option = ord($recordData[$pos]);
1✔
2027
                    ++$pos;
1✔
2028

2029
                    /** @var int $limitpos */
2030
                    if ($isCompressed && ($option == 0)) {
1✔
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
                    } elseif (!$isCompressed && ($option != 0)) {
1✔
2039
                        // 1st fragment uncompressed
2040
                        // this fragment uncompressed
2041
                        /** @var int */
2042
                        $len = min($charsLeft * 2, $limitpos - $pos);
1✔
2043
                        $retstr .= substr($recordData, $pos, $len);
1✔
2044
                        $charsLeft -= $len / 2;
1✔
2045
                        $isCompressed = false;
1✔
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
                    $pos += $len;
1✔
2073
                }
2074
            }
2075

2076
            // convert to UTF-8
2077
            $retstr = self::encodeUTF16($retstr, $isCompressed);
78✔
2078

2079
            // read additional Rich-Text information, if any
2080
            $fmtRuns = [];
78✔
2081
            if ($hasRichText) {
78✔
2082
                // list of formatting runs
2083
                for ($j = 0; $j < $formattingRuns; ++$j) {
8✔
2084
                    // first formatted character; zero-based
2085
                    /** @var int $pos */
2086
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
8✔
2087

2088
                    // index to font record
2089
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
8✔
2090

2091
                    $fmtRuns[] = [
8✔
2092
                        'charPos' => $charPos,
8✔
2093
                        'fontIndex' => $fontIndex,
8✔
2094
                    ];
8✔
2095
                }
2096
                $pos += 4 * $formattingRuns;
8✔
2097
            }
2098

2099
            // read additional Asian phonetics information, if any
2100
            if ($hasAsian) {
78✔
2101
                // For Asian phonetic settings, we skip the extended string data
2102
                $pos += $extendedRunLength;
×
2103
            }
2104

2105
            // store the shared sting
2106
            $this->sst[] = [
78✔
2107
                'value' => $retstr,
78✔
2108
                'fmtRuns' => $fmtRuns,
78✔
2109
            ];
78✔
2110
        }
2111

2112
        // getSplicedRecordData() takes care of moving current position in data stream
2113
    }
2114

2115
    /**
2116
     * Read PRINTGRIDLINES record.
2117
     */
2118
    protected function readPrintGridlines(): void
128✔
2119
    {
2120
        $length = self::getUInt2d($this->data, $this->pos + 2);
128✔
2121
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
128✔
2122

2123
        // move stream pointer to next record
2124
        $this->pos += 4 + $length;
128✔
2125

2126
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
128✔
2127
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
2128
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
124✔
2129
            $this->phpSheet->setPrintGridlines($printGridlines);
124✔
2130
        }
2131
    }
2132

2133
    /**
2134
     * Read DEFAULTROWHEIGHT record.
2135
     */
2136
    protected function readDefaultRowHeight(): void
66✔
2137
    {
2138
        $length = self::getUInt2d($this->data, $this->pos + 2);
66✔
2139
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
66✔
2140

2141
        // move stream pointer to next record
2142
        $this->pos += 4 + $length;
66✔
2143

2144
        // offset: 0; size: 2; option flags
2145
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
2146
        $height = self::getUInt2d($recordData, 2);
66✔
2147
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
66✔
2148
    }
2149

2150
    /**
2151
     * Read SHEETPR record.
2152
     */
2153
    protected function readSheetPr(): void
130✔
2154
    {
2155
        $length = self::getUInt2d($this->data, $this->pos + 2);
130✔
2156
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
130✔
2157

2158
        // move stream pointer to next record
2159
        $this->pos += 4 + $length;
130✔
2160

2161
        // offset: 0; size: 2
2162

2163
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
2164
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
130✔
2165
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
130✔
2166

2167
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
2168
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
130✔
2169
        $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight);
130✔
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
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
130✔
2174
    }
2175

2176
    /**
2177
     * Read HORIZONTALPAGEBREAKS record.
2178
     */
2179
    protected function readHorizontalPageBreaks(): void
5✔
2180
    {
2181
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2182
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2183

2184
        // move stream pointer to next record
2185
        $this->pos += 4 + $length;
5✔
2186

2187
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
5✔
2188
            // offset: 0; size: 2; number of the following row index structures
2189
            $nm = self::getUInt2d($recordData, 0);
5✔
2190

2191
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2192
            for ($i = 0; $i < $nm; ++$i) {
5✔
2193
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2194
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2195
                //$cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2196

2197
                // not sure why two column indexes are necessary?
2198
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3✔
2199
            }
2200
        }
2201
    }
2202

2203
    /**
2204
     * Read VERTICALPAGEBREAKS record.
2205
     */
2206
    protected function readVerticalPageBreaks(): void
5✔
2207
    {
2208
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2209
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2210

2211
        // move stream pointer to next record
2212
        $this->pos += 4 + $length;
5✔
2213

2214
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
5✔
2215
            // offset: 0; size: 2; number of the following column index structures
2216
            $nm = self::getUInt2d($recordData, 0);
5✔
2217

2218
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2219
            for ($i = 0; $i < $nm; ++$i) {
5✔
2220
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2221
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2222
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2223

2224
                // not sure why two row indexes are necessary?
2225
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3✔
2226
            }
2227
        }
2228
    }
2229

2230
    /**
2231
     * Read HEADER record.
2232
     */
2233
    protected function readHeader(): void
128✔
2234
    {
2235
        $length = self::getUInt2d($this->data, $this->pos + 2);
128✔
2236
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
128✔
2237

2238
        // move stream pointer to next record
2239
        $this->pos += 4 + $length;
128✔
2240

2241
        if (!$this->readDataOnly) {
128✔
2242
            // offset: 0; size: var
2243
            // realized that $recordData can be empty even when record exists
2244
            if ($recordData) {
126✔
2245
                if ($this->version == self::XLS_BIFF8) {
79✔
2246
                    $string = self::readUnicodeStringLong($recordData);
78✔
2247
                } else {
2248
                    $string = $this->readByteStringShort($recordData);
1✔
2249
                }
2250

2251
                /** @var string[] $string */
2252
                $this->phpSheet
79✔
2253
                    ->getHeaderFooter()
79✔
2254
                    ->setOddHeader($string['value']);
79✔
2255
                $this->phpSheet
79✔
2256
                    ->getHeaderFooter()
79✔
2257
                    ->setEvenHeader($string['value']);
79✔
2258
            }
2259
        }
2260
    }
2261

2262
    /**
2263
     * Read FOOTER record.
2264
     */
2265
    protected function readFooter(): void
128✔
2266
    {
2267
        $length = self::getUInt2d($this->data, $this->pos + 2);
128✔
2268
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
128✔
2269

2270
        // move stream pointer to next record
2271
        $this->pos += 4 + $length;
128✔
2272

2273
        if (!$this->readDataOnly) {
128✔
2274
            // offset: 0; size: var
2275
            // realized that $recordData can be empty even when record exists
2276
            if ($recordData) {
126✔
2277
                if ($this->version == self::XLS_BIFF8) {
81✔
2278
                    $string = self::readUnicodeStringLong($recordData);
79✔
2279
                } else {
2280
                    $string = $this->readByteStringShort($recordData);
2✔
2281
                }
2282
                /** @var string */
2283
                $temp = $string['value'];
81✔
2284
                $this->phpSheet
81✔
2285
                    ->getHeaderFooter()
81✔
2286
                    ->setOddFooter($temp);
81✔
2287
                $this->phpSheet
81✔
2288
                    ->getHeaderFooter()
81✔
2289
                    ->setEvenFooter($temp);
81✔
2290
            }
2291
        }
2292
    }
2293

2294
    /**
2295
     * Read HCENTER record.
2296
     */
2297
    protected function readHcenter(): void
128✔
2298
    {
2299
        $length = self::getUInt2d($this->data, $this->pos + 2);
128✔
2300
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
128✔
2301

2302
        // move stream pointer to next record
2303
        $this->pos += 4 + $length;
128✔
2304

2305
        if (!$this->readDataOnly) {
128✔
2306
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
2307
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
126✔
2308

2309
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
126✔
2310
        }
2311
    }
2312

2313
    /**
2314
     * Read VCENTER record.
2315
     */
2316
    protected function readVcenter(): void
128✔
2317
    {
2318
        $length = self::getUInt2d($this->data, $this->pos + 2);
128✔
2319
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
128✔
2320

2321
        // move stream pointer to next record
2322
        $this->pos += 4 + $length;
128✔
2323

2324
        if (!$this->readDataOnly) {
128✔
2325
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
2326
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
126✔
2327

2328
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
126✔
2329
        }
2330
    }
2331

2332
    /**
2333
     * Read LEFTMARGIN record.
2334
     */
2335
    protected function readLeftMargin(): void
121✔
2336
    {
2337
        $length = self::getUInt2d($this->data, $this->pos + 2);
121✔
2338
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
121✔
2339

2340
        // move stream pointer to next record
2341
        $this->pos += 4 + $length;
121✔
2342

2343
        if (!$this->readDataOnly) {
121✔
2344
            // offset: 0; size: 8
2345
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
119✔
2346
        }
2347
    }
2348

2349
    /**
2350
     * Read RIGHTMARGIN record.
2351
     */
2352
    protected function readRightMargin(): void
121✔
2353
    {
2354
        $length = self::getUInt2d($this->data, $this->pos + 2);
121✔
2355
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
121✔
2356

2357
        // move stream pointer to next record
2358
        $this->pos += 4 + $length;
121✔
2359

2360
        if (!$this->readDataOnly) {
121✔
2361
            // offset: 0; size: 8
2362
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
119✔
2363
        }
2364
    }
2365

2366
    /**
2367
     * Read TOPMARGIN record.
2368
     */
2369
    protected function readTopMargin(): void
121✔
2370
    {
2371
        $length = self::getUInt2d($this->data, $this->pos + 2);
121✔
2372
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
121✔
2373

2374
        // move stream pointer to next record
2375
        $this->pos += 4 + $length;
121✔
2376

2377
        if (!$this->readDataOnly) {
121✔
2378
            // offset: 0; size: 8
2379
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
119✔
2380
        }
2381
    }
2382

2383
    /**
2384
     * Read BOTTOMMARGIN record.
2385
     */
2386
    protected function readBottomMargin(): void
121✔
2387
    {
2388
        $length = self::getUInt2d($this->data, $this->pos + 2);
121✔
2389
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
121✔
2390

2391
        // move stream pointer to next record
2392
        $this->pos += 4 + $length;
121✔
2393

2394
        if (!$this->readDataOnly) {
121✔
2395
            // offset: 0; size: 8
2396
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
119✔
2397
        }
2398
    }
2399

2400
    /**
2401
     * Read PAGESETUP record.
2402
     */
2403
    protected function readPageSetup(): void
130✔
2404
    {
2405
        $length = self::getUInt2d($this->data, $this->pos + 2);
130✔
2406
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
130✔
2407

2408
        // move stream pointer to next record
2409
        $this->pos += 4 + $length;
130✔
2410

2411
        if (!$this->readDataOnly) {
130✔
2412
            // offset: 0; size: 2; paper size
2413
            $paperSize = self::getUInt2d($recordData, 0);
128✔
2414

2415
            // offset: 2; size: 2; scaling factor
2416
            $scale = self::getUInt2d($recordData, 2);
128✔
2417

2418
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
2419
            $fitToWidth = self::getUInt2d($recordData, 6);
128✔
2420

2421
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
2422
            $fitToHeight = self::getUInt2d($recordData, 8);
128✔
2423

2424
            // offset: 10; size: 2; option flags
2425

2426
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
2427
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
128✔
2428

2429
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
2430
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
128✔
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
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
128✔
2435

2436
            if (!$isNotInit) {
128✔
2437
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
118✔
2438
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
118✔
2439
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
118✔
2440

2441
                $this->phpSheet->getPageSetup()->setScale($scale, false);
118✔
2442
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
118✔
2443
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
118✔
2444
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
118✔
2445
            }
2446

2447
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
2448
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
128✔
2449
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
128✔
2450

2451
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
2452
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
128✔
2453
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
128✔
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
    protected function readProtect(): void
7✔
2462
    {
2463
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
2464
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
2465

2466
        // move stream pointer to next record
2467
        $this->pos += 4 + $length;
7✔
2468

2469
        if ($this->readDataOnly) {
7✔
2470
            return;
×
2471
        }
2472

2473
        // offset: 0; size: 2;
2474

2475
        // bit 0, mask 0x01; 1 = sheet is protected
2476
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
7✔
2477
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
7✔
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
    protected function readObjectProtect(): void
2✔
2507
    {
2508
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
2509
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
2510

2511
        // move stream pointer to next record
2512
        $this->pos += 4 + $length;
2✔
2513

2514
        if ($this->readDataOnly) {
2✔
2515
            return;
×
2516
        }
2517

2518
        // offset: 0; size: 2;
2519

2520
        // bit: 0, mask 0x01; 1 = objects are protected
2521
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
2✔
2522

2523
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
2✔
2524
    }
2525

2526
    /**
2527
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
2528
     */
2529
    protected function readPassword(): void
3✔
2530
    {
2531
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
2532
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
2533

2534
        // move stream pointer to next record
2535
        $this->pos += 4 + $length;
3✔
2536

2537
        if (!$this->readDataOnly) {
3✔
2538
            // offset: 0; size: 2; 16-bit hash value of password
2539
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3✔
2540
            $this->phpSheet->getProtection()->setPassword($password, true);
3✔
2541
        }
2542
    }
2543

2544
    /**
2545
     * Read DEFCOLWIDTH record.
2546
     */
2547
    protected function readDefColWidth(): void
129✔
2548
    {
2549
        $length = self::getUInt2d($this->data, $this->pos + 2);
129✔
2550
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
129✔
2551

2552
        // move stream pointer to next record
2553
        $this->pos += 4 + $length;
129✔
2554

2555
        // offset: 0; size: 2; default column width
2556
        $width = self::getUInt2d($recordData, 0);
129✔
2557
        if ($width != 8) {
129✔
2558
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
5✔
2559
        }
2560
    }
2561

2562
    /**
2563
     * Read COLINFO record.
2564
     */
2565
    protected function readColInfo(): void
116✔
2566
    {
2567
        $length = self::getUInt2d($this->data, $this->pos + 2);
116✔
2568
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
116✔
2569

2570
        // move stream pointer to next record
2571
        $this->pos += 4 + $length;
116✔
2572

2573
        if (!$this->readDataOnly) {
116✔
2574
            // offset: 0; size: 2; index to first column in range
2575
            $firstColumnIndex = self::getUInt2d($recordData, 0);
114✔
2576

2577
            // offset: 2; size: 2; index to last column in range
2578
            $lastColumnIndex = self::getUInt2d($recordData, 2);
114✔
2579

2580
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
2581
            $width = self::getUInt2d($recordData, 4);
114✔
2582

2583
            // offset: 6; size: 2; index to XF record for default column formatting
2584
            $xfIndex = self::getUInt2d($recordData, 6);
114✔
2585

2586
            // offset: 8; size: 2; option flags
2587
            // bit: 0; mask: 0x0001; 1= columns are hidden
2588
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
114✔
2589

2590
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
2591
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
114✔
2592

2593
            // bit: 12; mask: 0x1000; 1 = collapsed
2594
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
114✔
2595

2596
            // offset: 10; size: 2; not used
2597

2598
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
114✔
2599
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
114✔
2600
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
14✔
2601

2602
                    break;
14✔
2603
                }
2604
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
105✔
2605
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
105✔
2606
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
105✔
2607
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
105✔
2608
                if (isset($this->mapCellXfIndex[$xfIndex])) {
105✔
2609
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
103✔
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
    protected function readRow(): void
74✔
2626
    {
2627
        $length = self::getUInt2d($this->data, $this->pos + 2);
74✔
2628
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
74✔
2629

2630
        // move stream pointer to next record
2631
        $this->pos += 4 + $length;
74✔
2632

2633
        if (!$this->readDataOnly) {
74✔
2634
            // offset: 0; size: 2; index of this row
2635
            $r = self::getUInt2d($recordData, 0);
72✔
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
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
72✔
2645

2646
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
2647
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
72✔
2648

2649
            if (!$useDefaultHeight) {
72✔
2650
                if (
2651
                    $this->phpSheet->getDefaultRowDimension()->getRowHeight() > 0
70✔
2652
                ) {
2653
                    $this->phpSheet->getRowDimension($r + 1)
59✔
2654
                        ->setCustomFormat(true, ($height === 255) ? -1 : ($height / 20));
59✔
2655
                } else {
2656
                    $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
11✔
2657
                }
2658
            }
2659

2660
            // offset: 8; size: 2; not used
2661

2662
            // offset: 10; size: 2; not used in BIFF5-BIFF8
2663

2664
            // offset: 12; size: 4; option flags and default row formatting
2665

2666
            // bit: 2-0: mask: 0x00000007; outline level of the row
2667
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
72✔
2668
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
72✔
2669

2670
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
2671
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
72✔
2672
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
72✔
2673

2674
            // bit: 5; mask: 0x00000020; 1 = row is hidden
2675
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
72✔
2676
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
72✔
2677

2678
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
2679
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
72✔
2680

2681
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
2682
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
72✔
2683

2684
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
72✔
2685
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
6✔
2686
            }
2687
        }
2688
    }
2689

2690
    /**
2691
     * Read RK record
2692
     * This record represents a cell that contains an RK value
2693
     * (encoded integer or floating-point value). If a
2694
     * floating-point value cannot be encoded to an RK value,
2695
     * a NUMBER record will be written. This record replaces the
2696
     * record INTEGER written in BIFF2.
2697
     *
2698
     * --    "OpenOffice.org's Documentation of the Microsoft
2699
     *         Excel File Format"
2700
     */
2701
    protected function readRk(): void
36✔
2702
    {
2703
        $length = self::getUInt2d($this->data, $this->pos + 2);
36✔
2704
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
36✔
2705

2706
        // move stream pointer to next record
2707
        $this->pos += 4 + $length;
36✔
2708

2709
        // offset: 0; size: 2; index to row
2710
        $row = self::getUInt2d($recordData, 0);
36✔
2711

2712
        // offset: 2; size: 2; index to column
2713
        $column = self::getUInt2d($recordData, 2);
36✔
2714
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
36✔
2715

2716
        // Read cell?
2717
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
36✔
2718
            // offset: 4; size: 2; index to XF record
2719
            $xfIndex = self::getUInt2d($recordData, 4);
36✔
2720

2721
            // offset: 6; size: 4; RK value
2722
            $rknum = self::getInt4d($recordData, 6);
36✔
2723
            $numValue = self::getIEEE754($rknum);
36✔
2724

2725
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
36✔
2726
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
36✔
2727
                // add style information
2728
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
32✔
2729
            }
2730

2731
            // add cell
2732
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
36✔
2733
        }
2734
    }
2735

2736
    /**
2737
     * Read LABELSST record
2738
     * This record represents a cell that contains a string. It
2739
     * replaces the LABEL record and RSTRING record used in
2740
     * BIFF2-BIFF5.
2741
     *
2742
     * --    "OpenOffice.org's Documentation of the Microsoft
2743
     *         Excel File Format"
2744
     */
2745
    protected function readLabelSst(): void
75✔
2746
    {
2747
        $length = self::getUInt2d($this->data, $this->pos + 2);
75✔
2748
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
75✔
2749

2750
        // move stream pointer to next record
2751
        $this->pos += 4 + $length;
75✔
2752

2753
        // offset: 0; size: 2; index to row
2754
        $row = self::getUInt2d($recordData, 0);
75✔
2755

2756
        // offset: 2; size: 2; index to column
2757
        $column = self::getUInt2d($recordData, 2);
75✔
2758
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
75✔
2759

2760
        $cell = null;
75✔
2761
        // Read cell?
2762
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
75✔
2763
            // offset: 4; size: 2; index to XF record
2764
            $xfIndex = self::getUInt2d($recordData, 4);
75✔
2765

2766
            // offset: 6; size: 4; index to SST record
2767
            $index = self::getInt4d($recordData, 6);
75✔
2768

2769
            // add cell
2770
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
75✔
2771
                // then we should treat as rich text
2772
                $richText = new RichText();
6✔
2773
                $charPos = 0;
6✔
2774
                $sstCount = count($this->sst[$index]['fmtRuns']);
6✔
2775
                for ($i = 0; $i <= $sstCount; ++$i) {
6✔
2776
                    /** @var mixed[][] $fmtRuns */
2777
                    if (isset($fmtRuns[$i])) {
6✔
2778
                        /** @var int[] */
2779
                        $temp = $fmtRuns[$i];
6✔
2780
                        $temp = $temp['charPos'];
6✔
2781
                        /** @var int $charPos */
2782
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $temp - $charPos);
6✔
2783
                        $charPos = $temp;
6✔
2784
                    } else {
2785
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
6✔
2786
                    }
2787

2788
                    if (StringHelper::countCharacters($text) > 0) {
6✔
2789
                        if ($i == 0) { // first text run, no style
6✔
2790
                            $richText->createText($text);
3✔
2791
                        } else {
2792
                            $textRun = $richText->createTextRun($text);
6✔
2793
                            /** @var int[][] $fmtRuns */
2794
                            if (isset($fmtRuns[$i - 1])) {
6✔
2795
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
6✔
2796
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
5✔
2797
                                } else {
2798
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
2799
                                    // check the OpenOffice documentation of the FONT record
2800
                                    /** @var int */
2801
                                    $temp = $fmtRuns[$i - 1]['fontIndex'];
4✔
2802
                                    $fontIndex = $temp - 1;
4✔
2803
                                }
2804
                                if (array_key_exists($fontIndex, $this->objFonts) === false) {
6✔
2805
                                    $fontIndex = count($this->objFonts) - 1;
1✔
2806
                                }
2807
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
6✔
2808
                            }
2809
                        }
2810
                    }
2811
                }
2812
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
6✔
2813
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
6✔
2814
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
6✔
2815
                }
2816
            } else {
2817
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
74✔
2818
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
74✔
2819
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
74✔
2820
                }
2821
            }
2822

2823
            if (!$this->readDataOnly && $cell !== null && isset($this->mapCellXfIndex[$xfIndex])) {
75✔
2824
                // add style information
2825
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
73✔
2826
            }
2827
        }
2828
    }
2829

2830
    /**
2831
     * Read MULRK record
2832
     * This record represents a cell range containing RK value
2833
     * cells. All cells are located in the same row.
2834
     *
2835
     * --    "OpenOffice.org's Documentation of the Microsoft
2836
     *         Excel File Format"
2837
     */
2838
    protected function readMulRk(): void
22✔
2839
    {
2840
        $length = self::getUInt2d($this->data, $this->pos + 2);
22✔
2841
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
22✔
2842

2843
        // move stream pointer to next record
2844
        $this->pos += 4 + $length;
22✔
2845

2846
        // offset: 0; size: 2; index to row
2847
        $row = self::getUInt2d($recordData, 0);
22✔
2848

2849
        // offset: 2; size: 2; index to first column
2850
        $colFirst = self::getUInt2d($recordData, 2);
22✔
2851

2852
        // offset: var; size: 2; index to last column
2853
        $colLast = self::getUInt2d($recordData, $length - 2);
22✔
2854
        $columns = $colLast - $colFirst + 1;
22✔
2855

2856
        // offset within record data
2857
        $offset = 4;
22✔
2858

2859
        for ($i = 1; $i <= $columns; ++$i) {
22✔
2860
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
22✔
2861

2862
            // Read cell?
2863
            if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
22✔
2864
                // offset: var; size: 2; index to XF record
2865
                $xfIndex = self::getUInt2d($recordData, $offset);
22✔
2866

2867
                // offset: var; size: 4; RK value
2868
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
22✔
2869
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
22✔
2870
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
22✔
2871
                    // add style
2872
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
20✔
2873
                }
2874

2875
                // add cell value
2876
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
22✔
2877
            }
2878

2879
            $offset += 6;
22✔
2880
        }
2881
    }
2882

2883
    /**
2884
     * Read NUMBER record
2885
     * This record represents a cell that contains a
2886
     * floating-point value.
2887
     *
2888
     * --    "OpenOffice.org's Documentation of the Microsoft
2889
     *         Excel File Format"
2890
     */
2891
    protected function readNumber(): void
59✔
2892
    {
2893
        $length = self::getUInt2d($this->data, $this->pos + 2);
59✔
2894
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
59✔
2895

2896
        // move stream pointer to next record
2897
        $this->pos += 4 + $length;
59✔
2898

2899
        // offset: 0; size: 2; index to row
2900
        $row = self::getUInt2d($recordData, 0);
59✔
2901

2902
        // offset: 2; size 2; index to column
2903
        $column = self::getUInt2d($recordData, 2);
59✔
2904
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
59✔
2905

2906
        // Read cell?
2907
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
59✔
2908
            // offset 4; size: 2; index to XF record
2909
            $xfIndex = self::getUInt2d($recordData, 4);
59✔
2910

2911
            $numValue = self::extractNumber(substr($recordData, 6, 8));
59✔
2912

2913
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
59✔
2914
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
59✔
2915
                // add cell style
2916
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
57✔
2917
            }
2918

2919
            // add cell value
2920
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
59✔
2921
        }
2922
    }
2923

2924
    /**
2925
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
2926
     * This record contains the token array and the result of a
2927
     * formula cell.
2928
     *
2929
     * --    "OpenOffice.org's Documentation of the Microsoft
2930
     *         Excel File Format"
2931
     */
2932
    protected function readFormula(): void
44✔
2933
    {
2934
        $length = self::getUInt2d($this->data, $this->pos + 2);
44✔
2935
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
44✔
2936

2937
        // move stream pointer to next record
2938
        $this->pos += 4 + $length;
44✔
2939

2940
        // offset: 0; size: 2; row index
2941
        $row = self::getUInt2d($recordData, 0);
44✔
2942

2943
        // offset: 2; size: 2; col index
2944
        $column = self::getUInt2d($recordData, 2);
44✔
2945
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
44✔
2946

2947
        // offset: 20: size: variable; formula structure
2948
        $formulaStructure = substr($recordData, 20);
44✔
2949

2950
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
2951
        $options = self::getUInt2d($recordData, 14);
44✔
2952

2953
        // bit: 0; mask: 0x0001; 1 = recalculate always
2954
        // bit: 1; mask: 0x0002; 1 = calculate on open
2955
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
2956
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
44✔
2957

2958
        // WARNING:
2959
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
2960
        // the formula data may be ordinary formula data, therefore we need to check
2961
        // explicitly for the tExp token (0x01)
2962
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
44✔
2963

2964
        if ($isPartOfSharedFormula) {
44✔
2965
            // part of shared formula which means there will be a formula with a tExp token and nothing else
2966
            // get the base cell, grab tExp token
2967
            $baseRow = self::getUInt2d($formulaStructure, 3);
1✔
2968
            $baseCol = self::getUInt2d($formulaStructure, 5);
1✔
2969
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
1✔
2970
        }
2971

2972
        // Read cell?
2973
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
44✔
2974
            if ($isPartOfSharedFormula) {
44✔
2975
                // formula is added to this cell after the sheet has been read
2976
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
1✔
2977
            }
2978

2979
            // offset: 16: size: 4; not used
2980

2981
            // offset: 4; size: 2; XF index
2982
            $xfIndex = self::getUInt2d($recordData, 4);
44✔
2983

2984
            // offset: 6; size: 8; result of the formula
2985
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
44✔
2986
                // String formula. Result follows in appended STRING record
2987
                $dataType = DataType::TYPE_STRING;
11✔
2988

2989
                // read possible SHAREDFMLA record
2990
                $code = self::getUInt2d($this->data, $this->pos);
11✔
2991
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
11✔
2992
                    $this->readSharedFmla();
×
2993
                }
2994

2995
                // read STRING record
2996
                $value = $this->readString();
11✔
2997
            } elseif (
2998
                (ord($recordData[6]) == 1)
38✔
2999
                && (ord($recordData[12]) == 255)
38✔
3000
                && (ord($recordData[13]) == 255)
38✔
3001
            ) {
3002
                // Boolean formula. Result is in +2; 0=false, 1=true
3003
                $dataType = DataType::TYPE_BOOL;
3✔
3004
                $value = (bool) ord($recordData[8]);
3✔
3005
            } elseif (
3006
                (ord($recordData[6]) == 2)
36✔
3007
                && (ord($recordData[12]) == 255)
36✔
3008
                && (ord($recordData[13]) == 255)
36✔
3009
            ) {
3010
                // Error formula. Error code is in +2
3011
                $dataType = DataType::TYPE_ERROR;
11✔
3012
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
11✔
3013
            } elseif (
3014
                (ord($recordData[6]) == 3)
36✔
3015
                && (ord($recordData[12]) == 255)
36✔
3016
                && (ord($recordData[13]) == 255)
36✔
3017
            ) {
3018
                // Formula result is a null string
3019
                $dataType = DataType::TYPE_NULL;
2✔
3020
                $value = '';
2✔
3021
            } else {
3022
                // forumla result is a number, first 14 bytes like _NUMBER record
3023
                $dataType = DataType::TYPE_NUMERIC;
36✔
3024
                $value = self::extractNumber(substr($recordData, 6, 8));
36✔
3025
            }
3026

3027
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
44✔
3028
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
44✔
3029
                // add cell style
3030
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
42✔
3031
            }
3032

3033
            // store the formula
3034
            if (!$isPartOfSharedFormula) {
44✔
3035
                // not part of shared formula
3036
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
3037
                try {
3038
                    if ($this->version != self::XLS_BIFF8) {
44✔
3039
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
1✔
3040
                    }
3041
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
43✔
3042
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
43✔
3043
                } catch (PhpSpreadsheetException) {
2✔
3044
                    $cell->setValueExplicit($value, $dataType);
2✔
3045
                }
3046
            } else {
3047
                if ($this->version == self::XLS_BIFF8) {
1✔
3048
                    // do nothing at this point, formula id added later in the code
3049
                } else {
3050
                    $cell->setValueExplicit($value, $dataType);
×
3051
                }
3052
            }
3053

3054
            // store the cached calculated value
3055
            $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC);
44✔
3056
        }
3057
    }
3058

3059
    /**
3060
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
3061
     * which usually contains relative references.
3062
     * These will be used to construct the formula in each shared formula part after the sheet is read.
3063
     */
3064
    protected function readSharedFmla(): void
1✔
3065
    {
3066
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
3067
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
3068

3069
        // move stream pointer to next record
3070
        $this->pos += 4 + $length;
1✔
3071

3072
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
3073
        //$cellRange = substr($recordData, 0, 6);
3074
        //$cellRange = Xls\Biff5::readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
3075

3076
        // offset: 6, size: 1; not used
3077

3078
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
3079
        //$no = ord($recordData[7]);
3080

3081
        // offset: 8, size: var; Binary token array of the shared formula
3082
        $formula = substr($recordData, 8);
1✔
3083

3084
        // at this point we only store the shared formula for later use
3085
        $this->sharedFormulas[$this->baseCell] = $formula;
1✔
3086
    }
3087

3088
    /**
3089
     * Read a STRING record from current stream position and advance the stream pointer to next record
3090
     * This record is used for storing result from FORMULA record when it is a string, and
3091
     * it occurs directly after the FORMULA record.
3092
     *
3093
     * @return string The string contents as UTF-8
3094
     */
3095
    protected function readString(): string
11✔
3096
    {
3097
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3098
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3099

3100
        // move stream pointer to next record
3101
        $this->pos += 4 + $length;
11✔
3102

3103
        if ($this->version == self::XLS_BIFF8) {
11✔
3104
            $string = self::readUnicodeStringLong($recordData);
11✔
3105
            $value = $string['value'];
11✔
3106
        } else {
3107
            $string = $this->readByteStringLong($recordData);
×
3108
            $value = $string['value'];
×
3109
        }
3110
        /** @var string $value */
3111

3112
        return $value;
11✔
3113
    }
3114

3115
    /**
3116
     * Read BOOLERR record
3117
     * This record represents a Boolean value or error value
3118
     * cell.
3119
     *
3120
     * --    "OpenOffice.org's Documentation of the Microsoft
3121
     *         Excel File Format"
3122
     */
3123
    protected function readBoolErr(): void
11✔
3124
    {
3125
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3126
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3127

3128
        // move stream pointer to next record
3129
        $this->pos += 4 + $length;
11✔
3130

3131
        // offset: 0; size: 2; row index
3132
        $row = self::getUInt2d($recordData, 0);
11✔
3133

3134
        // offset: 2; size: 2; column index
3135
        $column = self::getUInt2d($recordData, 2);
11✔
3136
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
11✔
3137

3138
        // Read cell?
3139
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
11✔
3140
            // offset: 4; size: 2; index to XF record
3141
            $xfIndex = self::getUInt2d($recordData, 4);
11✔
3142

3143
            // offset: 6; size: 1; the boolean value or error value
3144
            $boolErr = ord($recordData[6]);
11✔
3145

3146
            // offset: 7; size: 1; 0=boolean; 1=error
3147
            $isError = ord($recordData[7]);
11✔
3148

3149
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
11✔
3150
            switch ($isError) {
3151
                case 0: // boolean
11✔
3152
                    $value = (bool) $boolErr;
11✔
3153

3154
                    // add cell value
3155
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
11✔
3156

3157
                    break;
11✔
3158
                case 1: // error type
×
3159
                    $value = Xls\ErrorCode::lookup($boolErr);
×
3160

3161
                    // add cell value
3162
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
×
3163

3164
                    break;
×
3165
            }
3166

3167
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
11✔
3168
                // add cell style
3169
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
9✔
3170
            }
3171
        }
3172
    }
3173

3174
    /**
3175
     * Read MULBLANK record
3176
     * This record represents a cell range of empty cells. All
3177
     * cells are located in the same row.
3178
     *
3179
     * --    "OpenOffice.org's Documentation of the Microsoft
3180
     *         Excel File Format"
3181
     */
3182
    protected function readMulBlank(): void
26✔
3183
    {
3184
        $length = self::getUInt2d($this->data, $this->pos + 2);
26✔
3185
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
26✔
3186

3187
        // move stream pointer to next record
3188
        $this->pos += 4 + $length;
26✔
3189

3190
        // offset: 0; size: 2; index to row
3191
        $row = self::getUInt2d($recordData, 0);
26✔
3192

3193
        // offset: 2; size: 2; index to first column
3194
        $fc = self::getUInt2d($recordData, 2);
26✔
3195

3196
        // offset: 4; size: 2 x nc; list of indexes to XF records
3197
        // add style information
3198
        if (!$this->readDataOnly && $this->readEmptyCells) {
26✔
3199
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
24✔
3200
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
24✔
3201

3202
                // Read cell?
3203
                if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
24✔
3204
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
24✔
3205
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
24✔
3206
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
24✔
3207
                    }
3208
                }
3209
            }
3210
        }
3211

3212
        // offset: 6; size 2; index to last column (not needed)
3213
    }
3214

3215
    /**
3216
     * Read LABEL record
3217
     * This record represents a cell that contains a string. In
3218
     * BIFF8 it is usually replaced by the LABELSST record.
3219
     * Excel still uses this record, if it copies unformatted
3220
     * text cells to the clipboard.
3221
     *
3222
     * --    "OpenOffice.org's Documentation of the Microsoft
3223
     *         Excel File Format"
3224
     */
3225
    protected function readLabel(): void
4✔
3226
    {
3227
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
3228
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
3229

3230
        // move stream pointer to next record
3231
        $this->pos += 4 + $length;
4✔
3232

3233
        // offset: 0; size: 2; index to row
3234
        $row = self::getUInt2d($recordData, 0);
4✔
3235

3236
        // offset: 2; size: 2; index to column
3237
        $column = self::getUInt2d($recordData, 2);
4✔
3238
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4✔
3239

3240
        // Read cell?
3241
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4✔
3242
            // offset: 4; size: 2; XF index
3243
            $xfIndex = self::getUInt2d($recordData, 4);
4✔
3244

3245
            // add cell value
3246
            // todo: what if string is very long? continue record
3247
            if ($this->version == self::XLS_BIFF8) {
4✔
3248
                $string = self::readUnicodeStringLong(substr($recordData, 6));
2✔
3249
                $value = $string['value'];
2✔
3250
            } else {
3251
                $string = $this->readByteStringLong(substr($recordData, 6));
2✔
3252
                $value = $string['value'];
2✔
3253
            }
3254
            /** @var string $value */
3255
            if ($this->readEmptyCells || trim($value) !== '') {
4✔
3256
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4✔
3257
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4✔
3258

3259
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4✔
3260
                    // add cell style
3261
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4✔
3262
                }
3263
            }
3264
        }
3265
    }
3266

3267
    /**
3268
     * Read BLANK record.
3269
     */
3270
    protected function readBlank(): void
25✔
3271
    {
3272
        $length = self::getUInt2d($this->data, $this->pos + 2);
25✔
3273
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
25✔
3274

3275
        // move stream pointer to next record
3276
        $this->pos += 4 + $length;
25✔
3277

3278
        // offset: 0; size: 2; row index
3279
        $row = self::getUInt2d($recordData, 0);
25✔
3280

3281
        // offset: 2; size: 2; col index
3282
        $col = self::getUInt2d($recordData, 2);
25✔
3283
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
25✔
3284

3285
        // Read cell?
3286
        if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
25✔
3287
            // offset: 4; size: 2; XF index
3288
            $xfIndex = self::getUInt2d($recordData, 4);
25✔
3289

3290
            // add style information
3291
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
25✔
3292
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
25✔
3293
            }
3294
        }
3295
    }
3296

3297
    /**
3298
     * Read MSODRAWING record.
3299
     */
3300
    protected function readMsoDrawing(): void
18✔
3301
    {
3302
        //$length = self::getUInt2d($this->data, $this->pos + 2);
3303

3304
        // get spliced record data
3305
        $splicedRecordData = $this->getSplicedRecordData();
18✔
3306
        $recordData = $splicedRecordData['recordData'];
18✔
3307

3308
        $this->drawingData .= StringHelper::convertToString($recordData);
18✔
3309
    }
3310

3311
    /**
3312
     * Read OBJ record.
3313
     */
3314
    protected function readObj(): void
14✔
3315
    {
3316
        $length = self::getUInt2d($this->data, $this->pos + 2);
14✔
3317
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
14✔
3318

3319
        // move stream pointer to next record
3320
        $this->pos += 4 + $length;
14✔
3321

3322
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
14✔
3323
            return;
1✔
3324
        }
3325

3326
        // recordData consists of an array of subrecords looking like this:
3327
        //    ft: 2 bytes; ftCmo type (0x15)
3328
        //    cb: 2 bytes; size in bytes of ftCmo data
3329
        //    ot: 2 bytes; Object Type
3330
        //    id: 2 bytes; Object id number
3331
        //    grbit: 2 bytes; Option Flags
3332
        //    data: var; subrecord data
3333

3334
        // for now, we are just interested in the second subrecord containing the object type
3335
        $ftCmoType = self::getUInt2d($recordData, 0);
13✔
3336
        $cbCmoSize = self::getUInt2d($recordData, 2);
13✔
3337
        $otObjType = self::getUInt2d($recordData, 4);
13✔
3338
        $idObjID = self::getUInt2d($recordData, 6);
13✔
3339
        $grbitOpts = self::getUInt2d($recordData, 6);
13✔
3340

3341
        $this->objs[] = [
13✔
3342
            'ftCmoType' => $ftCmoType,
13✔
3343
            'cbCmoSize' => $cbCmoSize,
13✔
3344
            'otObjType' => $otObjType,
13✔
3345
            'idObjID' => $idObjID,
13✔
3346
            'grbitOpts' => $grbitOpts,
13✔
3347
        ];
13✔
3348
        $this->textObjRef = $idObjID;
13✔
3349
    }
3350

3351
    /**
3352
     * Read WINDOW2 record.
3353
     */
3354
    protected function readWindow2(): void
131✔
3355
    {
3356
        $length = self::getUInt2d($this->data, $this->pos + 2);
131✔
3357
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
131✔
3358

3359
        // move stream pointer to next record
3360
        $this->pos += 4 + $length;
131✔
3361

3362
        // offset: 0; size: 2; option flags
3363
        $options = self::getUInt2d($recordData, 0);
131✔
3364

3365
        // offset: 2; size: 2; index to first visible row
3366
        //$firstVisibleRow = self::getUInt2d($recordData, 2);
3367

3368
        // offset: 4; size: 2; index to first visible colum
3369
        //$firstVisibleColumn = self::getUInt2d($recordData, 4);
3370
        $zoomscaleInPageBreakPreview = 0;
131✔
3371
        $zoomscaleInNormalView = 0;
131✔
3372
        if ($this->version === self::XLS_BIFF8) {
131✔
3373
            // offset:  8; size: 2; not used
3374
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
3375
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
3376
            // offset: 14; size: 4; not used
3377
            if (!isset($recordData[10])) {
129✔
3378
                $zoomscaleInPageBreakPreview = 0;
×
3379
            } else {
3380
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
129✔
3381
            }
3382

3383
            if ($zoomscaleInPageBreakPreview === 0) {
129✔
3384
                $zoomscaleInPageBreakPreview = 60;
125✔
3385
            }
3386

3387
            if (!isset($recordData[12])) {
129✔
3388
                $zoomscaleInNormalView = 0;
×
3389
            } else {
3390
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
129✔
3391
            }
3392

3393
            if ($zoomscaleInNormalView === 0) {
129✔
3394
                $zoomscaleInNormalView = 100;
53✔
3395
            }
3396
        }
3397

3398
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
3399
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
131✔
3400
        $this->phpSheet->setShowGridlines($showGridlines);
131✔
3401

3402
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
3403
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
131✔
3404
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
131✔
3405

3406
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
3407
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
131✔
3408

3409
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
3410
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
131✔
3411

3412
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
3413
        $isActive = (bool) ((0x0400 & $options) >> 10);
131✔
3414
        if ($isActive) {
131✔
3415
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
127✔
3416
            $this->activeSheetSet = true;
127✔
3417
        }
3418

3419
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
3420
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
131✔
3421

3422
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
3423

3424
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
131✔
3425
            //NOTE: this setting is inferior to page layout view(Excel2007-)
3426
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
131✔
3427
            $this->phpSheet->getSheetView()->setView($view);
131✔
3428
            if ($this->version === self::XLS_BIFF8) {
131✔
3429
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
129✔
3430
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
129✔
3431
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
129✔
3432
            }
3433
        }
3434
    }
3435

3436
    /**
3437
     * Read PLV Record(Created by Excel2007 or upper).
3438
     */
3439
    protected function readPageLayoutView(): void
116✔
3440
    {
3441
        $length = self::getUInt2d($this->data, $this->pos + 2);
116✔
3442
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
116✔
3443

3444
        // move stream pointer to next record
3445
        $this->pos += 4 + $length;
116✔
3446

3447
        // offset: 0; size: 2; rt
3448
        //->ignore
3449
        //$rt = self::getUInt2d($recordData, 0);
3450
        // offset: 2; size: 2; grbitfr
3451
        //->ignore
3452
        //$grbitFrt = self::getUInt2d($recordData, 2);
3453
        // offset: 4; size: 8; reserved
3454
        //->ignore
3455

3456
        // offset: 12; size 2; zoom scale
3457
        $wScalePLV = self::getUInt2d($recordData, 12);
116✔
3458
        // offset: 14; size 2; grbit
3459
        $grbit = self::getUInt2d($recordData, 14);
116✔
3460

3461
        // decomprise grbit
3462
        $fPageLayoutView = $grbit & 0x01;
116✔
3463
        //$fRulerVisible = ($grbit >> 1) & 0x01; //no support
3464
        //$fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
3465

3466
        if ($fPageLayoutView === 1) {
116✔
3467
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
×
3468
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
×
3469
        }
3470
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
3471
    }
3472

3473
    /**
3474
     * Read SCL record.
3475
     */
3476
    protected function readScl(): void
6✔
3477
    {
3478
        $length = self::getUInt2d($this->data, $this->pos + 2);
6✔
3479
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
6✔
3480

3481
        // move stream pointer to next record
3482
        $this->pos += 4 + $length;
6✔
3483

3484
        // offset: 0; size: 2; numerator of the view magnification
3485
        $numerator = self::getUInt2d($recordData, 0);
6✔
3486

3487
        // offset: 2; size: 2; numerator of the view magnification
3488
        $denumerator = self::getUInt2d($recordData, 2);
6✔
3489

3490
        // set the zoom scale (in percent)
3491
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
6✔
3492
    }
3493

3494
    /**
3495
     * Read PANE record.
3496
     */
3497
    protected function readPane(): void
8✔
3498
    {
3499
        $length = self::getUInt2d($this->data, $this->pos + 2);
8✔
3500
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
8✔
3501

3502
        // move stream pointer to next record
3503
        $this->pos += 4 + $length;
8✔
3504

3505
        if (!$this->readDataOnly) {
8✔
3506
            // offset: 0; size: 2; position of vertical split
3507
            $px = self::getUInt2d($recordData, 0);
8✔
3508

3509
            // offset: 2; size: 2; position of horizontal split
3510
            $py = self::getUInt2d($recordData, 2);
8✔
3511

3512
            // offset: 4; size: 2; top most visible row in the bottom pane
3513
            $rwTop = self::getUInt2d($recordData, 4);
8✔
3514

3515
            // offset: 6; size: 2; first visible left column in the right pane
3516
            $colLeft = self::getUInt2d($recordData, 6);
8✔
3517

3518
            if ($this->frozen) {
8✔
3519
                // frozen panes
3520
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
8✔
3521
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
8✔
3522
                $this->phpSheet->freezePane($cell, $topLeftCell);
8✔
3523
            }
3524
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
3525
        }
3526
    }
3527

3528
    /**
3529
     * Read SELECTION record. There is one such record for each pane in the sheet.
3530
     */
3531
    protected function readSelection(): string
128✔
3532
    {
3533
        $length = self::getUInt2d($this->data, $this->pos + 2);
128✔
3534
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
128✔
3535
        $selectedCells = '';
128✔
3536

3537
        // move stream pointer to next record
3538
        $this->pos += 4 + $length;
128✔
3539

3540
        if (!$this->readDataOnly) {
128✔
3541
            // offset: 0; size: 1; pane identifier
3542
            //$paneId = ord($recordData[0]);
3543

3544
            // offset: 1; size: 2; index to row of the active cell
3545
            //$r = self::getUInt2d($recordData, 1);
3546

3547
            // offset: 3; size: 2; index to column of the active cell
3548
            //$c = self::getUInt2d($recordData, 3);
3549

3550
            // offset: 5; size: 2; index into the following cell range list to the
3551
            //  entry that contains the active cell
3552
            //$index = self::getUInt2d($recordData, 5);
3553

3554
            // offset: 7; size: var; cell range address list containing all selected cell ranges
3555
            $data = substr($recordData, 7);
126✔
3556
            $cellRangeAddressList = Xls\Biff5::readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
126✔
3557

3558
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
126✔
3559

3560
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
3561
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
126✔
3562
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
×
3563
            }
3564

3565
            // first row '1' + last row '65536' indicates that full column is selected
3566
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
126✔
3567
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
×
3568
            }
3569

3570
            // first column 'A' + last column 'IV' indicates that full row is selected
3571
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
126✔
3572
                $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
2✔
3573
            }
3574

3575
            $this->phpSheet->setSelectedCells($selectedCells);
126✔
3576
        }
3577

3578
        return $selectedCells;
128✔
3579
    }
3580

3581
    private function includeCellRangeFiltered(string $cellRangeAddress): bool
17✔
3582
    {
3583
        $includeCellRange = false;
17✔
3584
        $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
17✔
3585
        StringHelper::stringIncrement($rangeBoundaries[1][0]);
17✔
3586
        for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
17✔
3587
            for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) {
17✔
3588
                if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
17✔
3589
                    $includeCellRange = true;
17✔
3590

3591
                    break 2;
17✔
3592
                }
3593
            }
3594
        }
3595

3596
        return $includeCellRange;
17✔
3597
    }
3598

3599
    /**
3600
     * MERGEDCELLS.
3601
     *
3602
     * This record contains the addresses of merged cell ranges
3603
     * in the current sheet.
3604
     *
3605
     * --    "OpenOffice.org's Documentation of the Microsoft
3606
     *         Excel File Format"
3607
     */
3608
    protected function readMergedCells(): void
19✔
3609
    {
3610
        $length = self::getUInt2d($this->data, $this->pos + 2);
19✔
3611
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
19✔
3612

3613
        // move stream pointer to next record
3614
        $this->pos += 4 + $length;
19✔
3615

3616
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
19✔
3617
            $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($recordData);
17✔
3618
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
17✔
3619
                /** @var string $cellRangeAddress */
3620
                if (
3621
                    (str_contains($cellRangeAddress, ':'))
17✔
3622
                    && ($this->includeCellRangeFiltered($cellRangeAddress))
17✔
3623
                ) {
3624
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
17✔
3625
                }
3626
            }
3627
        }
3628
    }
3629

3630
    /**
3631
     * Read HYPERLINK record.
3632
     */
3633
    protected function readHyperLink(): void
7✔
3634
    {
3635
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
3636
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
3637

3638
        // move stream pointer forward to next record
3639
        $this->pos += 4 + $length;
7✔
3640

3641
        if (!$this->readDataOnly) {
7✔
3642
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
3643
            try {
3644
                $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed($recordData);
7✔
3645
            } catch (PhpSpreadsheetException) {
×
3646
                return;
×
3647
            }
3648

3649
            // offset: 8, size: 16; GUID of StdLink
3650

3651
            // offset: 24, size: 4; unknown value
3652

3653
            // offset: 28, size: 4; option flags
3654
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
3655
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
7✔
3656

3657
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
3658
            //$isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
3659

3660
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
3661
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
7✔
3662

3663
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
3664
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
7✔
3665

3666
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
3667
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
7✔
3668

3669
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
3670
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
7✔
3671

3672
            // offset within record data
3673
            $offset = 32;
7✔
3674

3675
            if ($hasDesc) {
7✔
3676
                // offset: 32; size: var; character count of description text
3677
                $dl = self::getInt4d($recordData, 32);
3✔
3678
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
3679
                //$desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
3680
                $offset += 4 + 2 * $dl;
3✔
3681
            }
3682
            if ($hasFrame) {
7✔
3683
                $fl = self::getInt4d($recordData, $offset);
×
3684
                $offset += 4 + 2 * $fl;
×
3685
            }
3686

3687
            // detect type of hyperlink (there are 4 types)
3688
            $hyperlinkType = null;
7✔
3689

3690
            if ($isUNC) {
7✔
3691
                $hyperlinkType = 'UNC';
×
3692
            } elseif (!$isFileLinkOrUrl) {
7✔
3693
                $hyperlinkType = 'workbook';
4✔
3694
            } elseif (ord($recordData[$offset]) == 0x03) {
7✔
3695
                $hyperlinkType = 'local';
×
3696
            } elseif (ord($recordData[$offset]) == 0xE0) {
7✔
3697
                $hyperlinkType = 'URL';
7✔
3698
            }
3699

3700
            switch ($hyperlinkType) {
3701
                case 'URL':
7✔
3702
                    // section 5.58.2: Hyperlink containing a URL
3703
                    // e.g. http://example.org/index.php
3704

3705
                    // offset: var; size: 16; GUID of URL Moniker
3706
                    $offset += 16;
7✔
3707
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
3708
                    $us = self::getInt4d($recordData, $offset);
7✔
3709
                    $offset += 4;
7✔
3710
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
3711
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
7✔
3712
                    $nullOffset = strpos($url, chr(0x00));
7✔
3713
                    if ($nullOffset) {
7✔
3714
                        $url = substr($url, 0, $nullOffset);
3✔
3715
                    }
3716
                    $url .= $hasText ? '#' : '';
7✔
3717
                    $offset += $us;
7✔
3718

3719
                    break;
7✔
3720
                case 'local':
4✔
3721
                    // section 5.58.3: Hyperlink to local file
3722
                    // examples:
3723
                    //   mydoc.txt
3724
                    //   ../../somedoc.xls#Sheet!A1
3725

3726
                    // offset: var; size: 16; GUI of File Moniker
3727
                    $offset += 16;
×
3728

3729
                    // offset: var; size: 2; directory up-level count.
3730
                    $upLevelCount = self::getUInt2d($recordData, $offset);
×
3731
                    $offset += 2;
×
3732

3733
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
3734
                    $sl = self::getInt4d($recordData, $offset);
×
3735
                    $offset += 4;
×
3736

3737
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
3738
                    $shortenedFilePath = substr($recordData, $offset, $sl);
×
3739
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
×
3740
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
×
3741

3742
                    $offset += $sl;
×
3743

3744
                    // offset: var; size: 24; unknown sequence
3745
                    $offset += 24;
×
3746

3747
                    // extended file path
3748
                    // offset: var; size: 4; size of the following file link field including string lenth mark
3749
                    $sz = self::getInt4d($recordData, $offset);
×
3750
                    $offset += 4;
×
3751

3752
                    $extendedFilePath = '';
×
3753
                    // only present if $sz > 0
3754
                    if ($sz > 0) {
×
3755
                        // offset: var; size: 4; size of the character array of the extended file path and name
3756
                        $xl = self::getInt4d($recordData, $offset);
×
3757
                        $offset += 4;
×
3758

3759
                        // offset: var; size 2; unknown
3760
                        $offset += 2;
×
3761

3762
                        // offset: var; size $xl; character array of the extended file path and name.
3763
                        $extendedFilePath = substr($recordData, $offset, $xl);
×
3764
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
×
3765
                        $offset += $xl;
×
3766
                    }
3767

3768
                    // construct the path
3769
                    $url = str_repeat('..\\', $upLevelCount);
×
3770
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
×
3771
                    $url .= $hasText ? '#' : '';
×
3772

3773
                    break;
×
3774
                case 'UNC':
4✔
3775
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
3776
                    // todo: implement
3777
                    return;
×
3778
                case 'workbook':
4✔
3779
                    // section 5.58.5: Hyperlink to the Current Workbook
3780
                    // e.g. Sheet2!B1:C2, stored in text mark field
3781
                    $url = 'sheet://';
4✔
3782

3783
                    break;
4✔
3784
                default:
3785
                    return;
×
3786
            }
3787

3788
            if ($hasText) {
7✔
3789
                // offset: var; size: 4; character count of text mark including trailing zero word
3790
                $tl = self::getInt4d($recordData, $offset);
4✔
3791
                $offset += 4;
4✔
3792
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
3793
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4✔
3794
                $url .= $text;
4✔
3795
            }
3796

3797
            // apply the hyperlink to all the relevant cells
3798
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
7✔
3799
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
7✔
3800
            }
3801
        }
3802
    }
3803

3804
    /**
3805
     * Read DATAVALIDATIONS record.
3806
     */
3807
    protected function readDataValidations(): void
5✔
3808
    {
3809
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3810
        //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3811

3812
        // move stream pointer forward to next record
3813
        $this->pos += 4 + $length;
5✔
3814
    }
3815

3816
    /**
3817
     * Read DATAVALIDATION record.
3818
     */
3819
    protected function readDataValidation(): void
5✔
3820
    {
3821
        (new Xls\DataValidationHelper())->readDataValidation2($this);
5✔
3822
    }
3823

3824
    /**
3825
     * Read SHEETLAYOUT record. Stores sheet tab color information.
3826
     */
3827
    protected function readSheetLayout(): void
5✔
3828
    {
3829
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3830
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
3831

3832
        // move stream pointer to next record
3833
        $this->pos += 4 + $length;
5✔
3834

3835
        if (!$this->readDataOnly) {
5✔
3836
            // offset: 0; size: 2; repeated record identifier 0x0862
3837

3838
            // offset: 2; size: 10; not used
3839

3840
            // offset: 12; size: 4; size of record data
3841
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
3842
            $sz = self::getInt4d($recordData, 12);
5✔
3843

3844
            switch ($sz) {
3845
                case 0x14:
5✔
3846
                    // offset: 16; size: 2; color index for sheet tab
3847
                    $colorIndex = self::getUInt2d($recordData, 16);
1✔
3848
                    /** @var string[] */
3849
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
1✔
3850
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
1✔
3851

3852
                    break;
1✔
3853
                case 0x28:
4✔
3854
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
3855
                    return;
4✔
3856
            }
3857
        }
3858
    }
3859

3860
    /**
3861
     * Read SHEETPROTECTION record (FEATHEADR).
3862
     */
3863
    protected function readSheetProtection(): void
120✔
3864
    {
3865
        $length = self::getUInt2d($this->data, $this->pos + 2);
120✔
3866
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
120✔
3867

3868
        // move stream pointer to next record
3869
        $this->pos += 4 + $length;
120✔
3870

3871
        if ($this->readDataOnly) {
120✔
3872
            return;
2✔
3873
        }
3874

3875
        // offset: 0; size: 2; repeated record header
3876

3877
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
3878

3879
        // offset: 4; size: 8; Currently not used and set to 0
3880

3881
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
3882
        $isf = self::getUInt2d($recordData, 12);
118✔
3883
        if ($isf != 2) {
118✔
3884
            return;
×
3885
        }
3886

3887
        // offset: 14; size: 1; =1 since this is a feat header
3888

3889
        // offset: 15; size: 4; size of rgbHdrSData
3890

3891
        // rgbHdrSData, assume "Enhanced Protection"
3892
        // offset: 19; size: 2; option flags
3893
        $options = self::getUInt2d($recordData, 19);
118✔
3894

3895
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
3896
        // Note - do not negate $bool
3897
        $bool = (0x0001 & $options) >> 0;
118✔
3898
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
118✔
3899

3900
        // bit: 1; mask 0x0002; edit scenarios
3901
        // Note - do not negate $bool
3902
        $bool = (0x0002 & $options) >> 1;
118✔
3903
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
118✔
3904

3905
        // bit: 2; mask 0x0004; format cells
3906
        $bool = (0x0004 & $options) >> 2;
118✔
3907
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
118✔
3908

3909
        // bit: 3; mask 0x0008; format columns
3910
        $bool = (0x0008 & $options) >> 3;
118✔
3911
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
118✔
3912

3913
        // bit: 4; mask 0x0010; format rows
3914
        $bool = (0x0010 & $options) >> 4;
118✔
3915
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
118✔
3916

3917
        // bit: 5; mask 0x0020; insert columns
3918
        $bool = (0x0020 & $options) >> 5;
118✔
3919
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
118✔
3920

3921
        // bit: 6; mask 0x0040; insert rows
3922
        $bool = (0x0040 & $options) >> 6;
118✔
3923
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
118✔
3924

3925
        // bit: 7; mask 0x0080; insert hyperlinks
3926
        $bool = (0x0080 & $options) >> 7;
118✔
3927
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
118✔
3928

3929
        // bit: 8; mask 0x0100; delete columns
3930
        $bool = (0x0100 & $options) >> 8;
118✔
3931
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
118✔
3932

3933
        // bit: 9; mask 0x0200; delete rows
3934
        $bool = (0x0200 & $options) >> 9;
118✔
3935
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
118✔
3936

3937
        // bit: 10; mask 0x0400; select locked cells
3938
        // Note that this is opposite of most of above.
3939
        $bool = (0x0400 & $options) >> 10;
118✔
3940
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
118✔
3941

3942
        // bit: 11; mask 0x0800; sort cell range
3943
        $bool = (0x0800 & $options) >> 11;
118✔
3944
        $this->phpSheet->getProtection()->setSort(!$bool);
118✔
3945

3946
        // bit: 12; mask 0x1000; auto filter
3947
        $bool = (0x1000 & $options) >> 12;
118✔
3948
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
118✔
3949

3950
        // bit: 13; mask 0x2000; pivot tables
3951
        $bool = (0x2000 & $options) >> 13;
118✔
3952
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
118✔
3953

3954
        // bit: 14; mask 0x4000; select unlocked cells
3955
        // Note that this is opposite of most of above.
3956
        $bool = (0x4000 & $options) >> 14;
118✔
3957
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
118✔
3958

3959
        // offset: 21; size: 2; not used
3960
    }
3961

3962
    /**
3963
     * Read RANGEPROTECTION record
3964
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
3965
     * where it is referred to as FEAT record.
3966
     */
3967
    protected function readRangeProtection(): void
2✔
3968
    {
3969
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
3970
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
3971

3972
        // move stream pointer to next record
3973
        $this->pos += 4 + $length;
2✔
3974

3975
        // local pointer in record data
3976
        $offset = 0;
2✔
3977

3978
        if (!$this->readDataOnly) {
2✔
3979
            $offset += 12;
2✔
3980

3981
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
3982
            $isf = self::getUInt2d($recordData, 12);
2✔
3983
            if ($isf != 2) {
2✔
3984
                // we only read FEAT records of type 2
3985
                return;
×
3986
            }
3987
            $offset += 2;
2✔
3988

3989
            $offset += 5;
2✔
3990

3991
            // offset: 19; size: 2; count of ref ranges this feature is on
3992
            $cref = self::getUInt2d($recordData, 19);
2✔
3993
            $offset += 2;
2✔
3994

3995
            $offset += 6;
2✔
3996

3997
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
3998
            $cellRanges = [];
2✔
3999
            for ($i = 0; $i < $cref; ++$i) {
2✔
4000
                try {
4001
                    $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
2✔
4002
                } catch (PhpSpreadsheetException) {
×
4003
                    return;
×
4004
                }
4005
                $cellRanges[] = $cellRange;
2✔
4006
                $offset += 8;
2✔
4007
            }
4008

4009
            // offset: var; size: var; variable length of feature specific data
4010
            //$rgbFeat = substr($recordData, $offset);
4011
            $offset += 4;
2✔
4012

4013
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
4014
            $wPassword = self::getInt4d($recordData, $offset);
2✔
4015
            $offset += 4;
2✔
4016

4017
            // Apply range protection to sheet
4018
            if ($cellRanges) {
2✔
4019
                $this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true);
2✔
4020
            }
4021
        }
4022
    }
4023

4024
    /**
4025
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
4026
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
4027
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
4028
     */
4029
    protected function readContinue(): void
1✔
4030
    {
4031
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
4032
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
4033

4034
        // check if we are reading drawing data
4035
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
4036
        if ($this->drawingData == '') {
1✔
4037
            // move stream pointer to next record
4038
            $this->pos += 4 + $length;
1✔
4039

4040
            return;
1✔
4041
        }
4042

4043
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
4044
        if ($length < 4) {
×
4045
            // move stream pointer to next record
4046
            $this->pos += 4 + $length;
×
4047

4048
            return;
×
4049
        }
4050

4051
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
4052
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
4053
        // we know that Escher stream may be split at least at
4054
        //        0xF003 MsofbtSpgrContainer
4055
        //        0xF004 MsofbtSpContainer
4056
        //        0xF00D MsofbtClientTextbox
4057
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
×
4058

4059
        $splitPoint = self::getUInt2d($recordData, 2);
×
4060
        if (in_array($splitPoint, $validSplitPoints)) {
×
4061
            // get spliced record data (and move pointer to next record)
4062
            $splicedRecordData = $this->getSplicedRecordData();
×
4063
            $this->drawingData .= StringHelper::convertToString($splicedRecordData['recordData']);
×
4064

4065
            return;
×
4066
        }
4067

4068
        // move stream pointer to next record
4069
        $this->pos += 4 + $length;
×
4070
    }
4071

4072
    /**
4073
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
4074
     * records are found. Splices the record data pieces and returns the combined string as if record data
4075
     * is in one piece.
4076
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
4077
     *
4078
     * @return mixed[]
4079
     */
4080
    private function getSplicedRecordData(): array
130✔
4081
    {
4082
        $data = '';
130✔
4083
        $spliceOffsets = [];
130✔
4084

4085
        $i = 0;
130✔
4086
        $spliceOffsets[0] = 0;
130✔
4087

4088
        do {
4089
            ++$i;
130✔
4090

4091
            // offset: 0; size: 2; identifier
4092
            //$identifier = self::getUInt2d($this->data, $this->pos);
4093
            // offset: 2; size: 2; length
4094
            $length = self::getUInt2d($this->data, $this->pos + 2);
130✔
4095
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
130✔
4096

4097
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
130✔
4098

4099
            $this->pos += 4 + $length;
130✔
4100
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
130✔
4101
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
130✔
4102

4103
        return [
130✔
4104
            'recordData' => $data,
130✔
4105
            'spliceOffsets' => $spliceOffsets,
130✔
4106
        ];
130✔
4107
    }
4108

4109
    /**
4110
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
4111
     *
4112
     * @param string $formulaStructure The complete binary data for the formula
4113
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4114
     *
4115
     * @return string Human readable formula
4116
     */
4117
    protected function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string
73✔
4118
    {
4119
        // offset: 0; size: 2; size of the following formula data
4120
        $sz = self::getUInt2d($formulaStructure, 0);
73✔
4121

4122
        // offset: 2; size: sz
4123
        $formulaData = substr($formulaStructure, 2, $sz);
73✔
4124

4125
        // offset: 2 + sz; size: variable (optional)
4126
        if (strlen($formulaStructure) > 2 + $sz) {
73✔
4127
            $additionalData = substr($formulaStructure, 2 + $sz);
1✔
4128
        } else {
4129
            $additionalData = '';
72✔
4130
        }
4131

4132
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
73✔
4133
    }
4134

4135
    /**
4136
     * Take formula data and additional data for formula and return human readable formula.
4137
     *
4138
     * @param string $formulaData The binary data for the formula itself
4139
     * @param string $additionalData Additional binary data going with the formula
4140
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4141
     *
4142
     * @return string Human readable formula
4143
     */
4144
    private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string
73✔
4145
    {
4146
        // start parsing the formula data
4147
        $tokens = [];
73✔
4148

4149
        while ($formulaData !== '' && $token = $this->getNextToken($formulaData, $baseCell)) {
73✔
4150
            $tokens[] = $token;
73✔
4151
            /** @var int[] $token */
4152
            $formulaData = substr($formulaData, $token['size']);
73✔
4153
        }
4154

4155
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
73✔
4156

4157
        return $formulaString;
73✔
4158
    }
4159

4160
    /**
4161
     * Take array of tokens together with additional data for formula and return human readable formula.
4162
     *
4163
     * @param mixed[][] $tokens
4164
     * @param string $additionalData Additional binary data going with the formula
4165
     *
4166
     * @return string Human readable formula
4167
     */
4168
    private function createFormulaFromTokens(array $tokens, string $additionalData): string
73✔
4169
    {
4170
        // empty formula?
4171
        if (empty($tokens)) {
73✔
4172
            return '';
5✔
4173
        }
4174

4175
        $formulaStrings = [];
73✔
4176
        foreach ($tokens as $token) {
73✔
4177
            // initialize spaces
4178
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
73✔
4179
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
73✔
4180
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
73✔
4181
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
73✔
4182
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
73✔
4183
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
73✔
4184
            /** @var string */
4185
            $tokenData = $token['data'] ?? '';
73✔
4186
            switch ($token['name']) {
73✔
4187
                case 'tAdd': // addition
73✔
4188
                case 'tConcat': // addition
73✔
4189
                case 'tDiv': // division
73✔
4190
                case 'tEQ': // equality
73✔
4191
                case 'tGE': // greater than or equal
73✔
4192
                case 'tGT': // greater than
73✔
4193
                case 'tIsect': // intersection
73✔
4194
                case 'tLE': // less than or equal
73✔
4195
                case 'tList': // less than or equal
73✔
4196
                case 'tLT': // less than
73✔
4197
                case 'tMul': // multiplication
73✔
4198
                case 'tNE': // multiplication
73✔
4199
                case 'tPower': // power
73✔
4200
                case 'tRange': // range
73✔
4201
                case 'tSub': // subtraction
73✔
4202
                    $op2 = array_pop($formulaStrings);
36✔
4203
                    $op1 = array_pop($formulaStrings);
36✔
4204
                    $formulaStrings[] = "$op1$space1$space0{$tokenData}$op2";
36✔
4205
                    unset($space0, $space1);
36✔
4206

4207
                    break;
36✔
4208
                case 'tUplus': // unary plus
73✔
4209
                case 'tUminus': // unary minus
73✔
4210
                    $op = array_pop($formulaStrings);
4✔
4211
                    $formulaStrings[] = "$space1$space0{$tokenData}$op";
4✔
4212
                    unset($space0, $space1);
4✔
4213

4214
                    break;
4✔
4215
                case 'tPercent': // percent sign
73✔
4216
                    $op = array_pop($formulaStrings);
1✔
4217
                    $formulaStrings[] = "$op$space1$space0{$tokenData}";
1✔
4218
                    unset($space0, $space1);
1✔
4219

4220
                    break;
1✔
4221
                case 'tAttrVolatile': // indicates volatile function
73✔
4222
                case 'tAttrIf':
73✔
4223
                case 'tAttrSkip':
73✔
4224
                case 'tAttrChoose':
73✔
4225
                    // token is only important for Excel formula evaluator
4226
                    // do nothing
4227
                    break;
3✔
4228
                case 'tAttrSpace': // space / carriage return
73✔
4229
                    // space will be used when next token arrives, do not alter formulaString stack
4230
                    /** @var string[][] $token */
4231
                    switch ($token['data']['spacetype']) {
×
4232
                        case 'type0':
×
4233
                            $space0 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4234

4235
                            break;
×
4236
                        case 'type1':
×
4237
                            $space1 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4238

4239
                            break;
×
4240
                        case 'type2':
×
4241
                            $space2 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4242

4243
                            break;
×
4244
                        case 'type3':
×
4245
                            $space3 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4246

4247
                            break;
×
4248
                        case 'type4':
×
4249
                            $space4 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4250

4251
                            break;
×
4252
                        case 'type5':
×
4253
                            $space5 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4254

4255
                            break;
×
4256
                    }
4257

4258
                    break;
×
4259
                case 'tAttrSum': // SUM function with one parameter
73✔
4260
                    $op = array_pop($formulaStrings);
15✔
4261
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
15✔
4262
                    unset($space0, $space1);
15✔
4263

4264
                    break;
15✔
4265
                case 'tFunc': // function with fixed number of arguments
73✔
4266
                case 'tFuncV': // function with variable number of arguments
73✔
4267
                    /** @var string[] */
4268
                    $temp1 = $token['data'];
39✔
4269
                    $temp2 = $temp1['function'];
39✔
4270
                    if ($temp2 != '') {
39✔
4271
                        // normal function
4272
                        $ops = []; // array of operators
38✔
4273
                        $temp3 = (int) $temp1['args'];
38✔
4274
                        for ($i = 0; $i < $temp3; ++$i) {
38✔
4275
                            $ops[] = array_pop($formulaStrings);
29✔
4276
                        }
4277
                        $ops = array_reverse($ops);
38✔
4278
                        $formulaStrings[] = "$space1$space0{$temp2}(" . implode(',', $ops) . ')';
38✔
4279
                        unset($space0, $space1);
38✔
4280
                    } else {
4281
                        // add-in function
4282
                        $ops = []; // array of operators
1✔
4283
                        /** @var int[] */
4284
                        $temp = $token['data'];
1✔
4285
                        for ($i = 0; $i < $temp['args'] - 1; ++$i) {
1✔
4286
                            $ops[] = array_pop($formulaStrings);
1✔
4287
                        }
4288
                        $ops = array_reverse($ops);
1✔
4289
                        $function = array_pop($formulaStrings);
1✔
4290
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
1✔
4291
                        unset($space0, $space1);
1✔
4292
                    }
4293

4294
                    break;
39✔
4295
                case 'tParen': // parenthesis
73✔
4296
                    $expression = array_pop($formulaStrings);
1✔
4297
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
1✔
4298
                    unset($space2, $space3, $space4, $space5);
1✔
4299

4300
                    break;
1✔
4301
                case 'tArray': // array constant
73✔
4302
                    $constantArray = Xls\Biff8::readBIFF8ConstantArray($additionalData);
1✔
4303
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
1✔
4304
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
1✔
4305
                    unset($space0, $space1);
1✔
4306

4307
                    break;
1✔
4308
                case 'tMemArea':
73✔
4309
                    // bite off chunk of additional data
4310
                    $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($additionalData);
×
4311
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
×
4312
                    $formulaStrings[] = "$space1$space0{$tokenData}";
×
4313
                    unset($space0, $space1);
×
4314

4315
                    break;
×
4316
                case 'tArea': // cell range address
73✔
4317
                case 'tBool': // boolean
71✔
4318
                case 'tErr': // error code
70✔
4319
                case 'tInt': // integer
69✔
4320
                case 'tMemErr':
53✔
4321
                case 'tMemFunc':
53✔
4322
                case 'tMissArg':
53✔
4323
                case 'tName':
53✔
4324
                case 'tNameX':
53✔
4325
                case 'tNum': // number
52✔
4326
                case 'tRef': // single cell reference
50✔
4327
                case 'tRef3d': // 3d cell reference
40✔
4328
                case 'tArea3d': // 3d cell range reference
37✔
4329
                case 'tRefN':
27✔
4330
                case 'tAreaN':
26✔
4331
                case 'tStr': // string
25✔
4332
                    $formulaStrings[] = "$space1$space0{$tokenData}";
73✔
4333
                    unset($space0, $space1);
73✔
4334

4335
                    break;
73✔
4336
            }
4337
        }
4338
        $formulaString = $formulaStrings[0];
73✔
4339

4340
        return $formulaString;
73✔
4341
    }
4342

4343
    /**
4344
     * Fetch next token from binary formula data.
4345
     *
4346
     * @param string $formulaData Formula data
4347
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4348
     *
4349
     * @return mixed[]
4350
     */
4351
    private function getNextToken(string $formulaData, string $baseCell = 'A1'): array
73✔
4352
    {
4353
        // offset: 0; size: 1; token id
4354
        $id = ord($formulaData[0]); // token id
73✔
4355
        $name = false; // initialize token name
73✔
4356

4357
        switch ($id) {
4358
            case 0x03:
73✔
4359
                $name = 'tAdd';
16✔
4360
                $size = 1;
16✔
4361
                $data = '+';
16✔
4362

4363
                break;
16✔
4364
            case 0x04:
73✔
4365
                $name = 'tSub';
14✔
4366
                $size = 1;
14✔
4367
                $data = '-';
14✔
4368

4369
                break;
14✔
4370
            case 0x05:
73✔
4371
                $name = 'tMul';
4✔
4372
                $size = 1;
4✔
4373
                $data = '*';
4✔
4374

4375
                break;
4✔
4376
            case 0x06:
73✔
4377
                $name = 'tDiv';
13✔
4378
                $size = 1;
13✔
4379
                $data = '/';
13✔
4380

4381
                break;
13✔
4382
            case 0x07:
73✔
4383
                $name = 'tPower';
1✔
4384
                $size = 1;
1✔
4385
                $data = '^';
1✔
4386

4387
                break;
1✔
4388
            case 0x08:
73✔
4389
                $name = 'tConcat';
4✔
4390
                $size = 1;
4✔
4391
                $data = '&';
4✔
4392

4393
                break;
4✔
4394
            case 0x09:
73✔
4395
                $name = 'tLT';
1✔
4396
                $size = 1;
1✔
4397
                $data = '<';
1✔
4398

4399
                break;
1✔
4400
            case 0x0A:
73✔
4401
                $name = 'tLE';
1✔
4402
                $size = 1;
1✔
4403
                $data = '<=';
1✔
4404

4405
                break;
1✔
4406
            case 0x0B:
73✔
4407
                $name = 'tEQ';
4✔
4408
                $size = 1;
4✔
4409
                $data = '=';
4✔
4410

4411
                break;
4✔
4412
            case 0x0C:
73✔
4413
                $name = 'tGE';
1✔
4414
                $size = 1;
1✔
4415
                $data = '>=';
1✔
4416

4417
                break;
1✔
4418
            case 0x0D:
73✔
4419
                $name = 'tGT';
2✔
4420
                $size = 1;
2✔
4421
                $data = '>';
2✔
4422

4423
                break;
2✔
4424
            case 0x0E:
73✔
4425
                $name = 'tNE';
2✔
4426
                $size = 1;
2✔
4427
                $data = '<>';
2✔
4428

4429
                break;
2✔
4430
            case 0x0F:
73✔
4431
                $name = 'tIsect';
×
4432
                $size = 1;
×
4433
                $data = ' ';
×
4434

4435
                break;
×
4436
            case 0x10:
73✔
4437
                $name = 'tList';
1✔
4438
                $size = 1;
1✔
4439
                $data = ',';
1✔
4440

4441
                break;
1✔
4442
            case 0x11:
73✔
4443
                $name = 'tRange';
×
4444
                $size = 1;
×
4445
                $data = ':';
×
4446

4447
                break;
×
4448
            case 0x12:
73✔
4449
                $name = 'tUplus';
1✔
4450
                $size = 1;
1✔
4451
                $data = '+';
1✔
4452

4453
                break;
1✔
4454
            case 0x13:
73✔
4455
                $name = 'tUminus';
4✔
4456
                $size = 1;
4✔
4457
                $data = '-';
4✔
4458

4459
                break;
4✔
4460
            case 0x14:
73✔
4461
                $name = 'tPercent';
1✔
4462
                $size = 1;
1✔
4463
                $data = '%';
1✔
4464

4465
                break;
1✔
4466
            case 0x15:    //    parenthesis
73✔
4467
                $name = 'tParen';
1✔
4468
                $size = 1;
1✔
4469
                $data = null;
1✔
4470

4471
                break;
1✔
4472
            case 0x16:    //    missing argument
73✔
4473
                $name = 'tMissArg';
×
4474
                $size = 1;
×
4475
                $data = '';
×
4476

4477
                break;
×
4478
            case 0x17:    //    string
73✔
4479
                $name = 'tStr';
25✔
4480
                // offset: 1; size: var; Unicode string, 8-bit string length
4481
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
25✔
4482
                $size = 1 + $string['size'];
25✔
4483
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
25✔
4484

4485
                break;
25✔
4486
            case 0x19:    //    Special attribute
72✔
4487
                // offset: 1; size: 1; attribute type flags:
4488
                switch (ord($formulaData[1])) {
17✔
4489
                    case 0x01:
17✔
4490
                        $name = 'tAttrVolatile';
3✔
4491
                        $size = 4;
3✔
4492
                        $data = null;
3✔
4493

4494
                        break;
3✔
4495
                    case 0x02:
15✔
4496
                        $name = 'tAttrIf';
1✔
4497
                        $size = 4;
1✔
4498
                        $data = null;
1✔
4499

4500
                        break;
1✔
4501
                    case 0x04:
15✔
4502
                        $name = 'tAttrChoose';
1✔
4503
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
4504
                        $nc = self::getUInt2d($formulaData, 2);
1✔
4505
                        // offset: 4; size: 2 * $nc
4506
                        // offset: 4 + 2 * $nc; size: 2
4507
                        $size = 2 * $nc + 6;
1✔
4508
                        $data = null;
1✔
4509

4510
                        break;
1✔
4511
                    case 0x08:
15✔
4512
                        $name = 'tAttrSkip';
1✔
4513
                        $size = 4;
1✔
4514
                        $data = null;
1✔
4515

4516
                        break;
1✔
4517
                    case 0x10:
15✔
4518
                        $name = 'tAttrSum';
15✔
4519
                        $size = 4;
15✔
4520
                        $data = null;
15✔
4521

4522
                        break;
15✔
4523
                    case 0x40:
×
4524
                    case 0x41:
×
4525
                        $name = 'tAttrSpace';
×
4526
                        $size = 4;
×
4527
                        // offset: 2; size: 2; space type and position
4528
                        $spacetype = match (ord($formulaData[2])) {
×
4529
                            0x00 => 'type0',
×
4530
                            0x01 => 'type1',
×
4531
                            0x02 => 'type2',
×
4532
                            0x03 => 'type3',
×
4533
                            0x04 => 'type4',
×
4534
                            0x05 => 'type5',
×
4535
                            default => throw new Exception('Unrecognized space type in tAttrSpace token'),
×
4536
                        };
×
4537
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
4538
                        $spacecount = ord($formulaData[3]);
×
4539

4540
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
×
4541

4542
                        break;
×
4543
                    default:
4544
                        throw new Exception('Unrecognized attribute flag in tAttr token');
×
4545
                }
4546

4547
                break;
17✔
4548
            case 0x1C:    //    error code
72✔
4549
                // offset: 1; size: 1; error code
4550
                $name = 'tErr';
5✔
4551
                $size = 2;
5✔
4552
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5✔
4553

4554
                break;
5✔
4555
            case 0x1D:    //    boolean
71✔
4556
                // offset: 1; size: 1; 0 = false, 1 = true;
4557
                $name = 'tBool';
3✔
4558
                $size = 2;
3✔
4559
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
3✔
4560

4561
                break;
3✔
4562
            case 0x1E:    //    integer
71✔
4563
                // offset: 1; size: 2; unsigned 16-bit integer
4564
                $name = 'tInt';
46✔
4565
                $size = 3;
46✔
4566
                $data = self::getUInt2d($formulaData, 1);
46✔
4567

4568
                break;
46✔
4569
            case 0x1F:    //    number
64✔
4570
                // offset: 1; size: 8;
4571
                $name = 'tNum';
9✔
4572
                $size = 9;
9✔
4573
                $data = self::extractNumber(substr($formulaData, 1));
9✔
4574
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
9✔
4575

4576
                break;
9✔
4577
            case 0x20:    //    array constant
63✔
4578
            case 0x40:
63✔
4579
            case 0x60:
63✔
4580
                // offset: 1; size: 7; not used
4581
                $name = 'tArray';
1✔
4582
                $size = 8;
1✔
4583
                $data = null;
1✔
4584

4585
                break;
1✔
4586
            case 0x21:    //    function with fixed number of arguments
63✔
4587
            case 0x41:
63✔
4588
            case 0x61:
59✔
4589
                $name = 'tFunc';
22✔
4590
                $size = 3;
22✔
4591
                // offset: 1; size: 2; index to built-in sheet function
4592
                $mapping = Xls\Mappings::TFUNC_MAPPINGS[self::getUInt2d($formulaData, 1)] ?? null;
22✔
4593
                if ($mapping === null) {
22✔
4594
                    throw new Exception('Unrecognized function in formula');
1✔
4595
                }
4596
                $data = ['function' => $mapping[0], 'args' => $mapping[1]];
22✔
4597

4598
                break;
22✔
4599
            case 0x22:    //    function with variable number of arguments
59✔
4600
            case 0x42:
59✔
4601
            case 0x62:
55✔
4602
                $name = 'tFuncV';
23✔
4603
                $size = 4;
23✔
4604
                // offset: 1; size: 1; number of arguments
4605
                $args = ord($formulaData[1]);
23✔
4606
                // offset: 2: size: 2; index to built-in sheet function
4607
                $index = self::getUInt2d($formulaData, 2);
23✔
4608
                $function = Xls\Mappings::TFUNCV_MAPPINGS[$index] ?? null;
23✔
4609
                if ($function === null) {
23✔
4610
                    throw new Exception('Unrecognized function in formula');
×
4611
                }
4612
                $data = ['function' => $function, 'args' => $args];
23✔
4613

4614
                break;
23✔
4615
            case 0x23:    //    index to defined name
55✔
4616
            case 0x43:
55✔
4617
            case 0x63:
55✔
4618
                $name = 'tName';
1✔
4619
                $size = 5;
1✔
4620
                // offset: 1; size: 2; one-based index to definedname record
4621
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
1✔
4622
                // offset: 2; size: 2; not used
4623
                /** @var string[] */
4624
                $data = $this->definedname[$definedNameIndex]['name'] ?? ''; //* @phpstan-ignore-line
1✔
4625

4626
                break;
1✔
4627
            case 0x24:    //    single cell reference e.g. A5
54✔
4628
            case 0x44:
54✔
4629
            case 0x64:
45✔
4630
                $name = 'tRef';
25✔
4631
                $size = 5;
25✔
4632
                $data = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 1, 4));
25✔
4633

4634
                break;
25✔
4635
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
43✔
4636
            case 0x45:
21✔
4637
            case 0x65:
21✔
4638
                $name = 'tArea';
29✔
4639
                $size = 9;
29✔
4640
                $data = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
29✔
4641

4642
                break;
29✔
4643
            case 0x26:    //    Constant reference sub-expression
20✔
4644
            case 0x46:
20✔
4645
            case 0x66:
20✔
4646
                $name = 'tMemArea';
×
4647
                // offset: 1; size: 4; not used
4648
                // offset: 5; size: 2; size of the following subexpression
4649
                $subSize = self::getUInt2d($formulaData, 5);
×
4650
                $size = 7 + $subSize;
×
4651
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4652

4653
                break;
×
4654
            case 0x27:    //    Deleted constant reference sub-expression
20✔
4655
            case 0x47:
20✔
4656
            case 0x67:
20✔
4657
                $name = 'tMemErr';
×
4658
                // offset: 1; size: 4; not used
4659
                // offset: 5; size: 2; size of the following subexpression
4660
                $subSize = self::getUInt2d($formulaData, 5);
×
4661
                $size = 7 + $subSize;
×
4662
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4663

4664
                break;
×
4665
            case 0x29:    //    Variable reference sub-expression
20✔
4666
            case 0x49:
20✔
4667
            case 0x69:
20✔
4668
                $name = 'tMemFunc';
×
4669
                // offset: 1; size: 2; size of the following sub-expression
4670
                $subSize = self::getUInt2d($formulaData, 1);
×
4671
                $size = 3 + $subSize;
×
4672
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
×
4673

4674
                break;
×
4675
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
20✔
4676
            case 0x4C:
20✔
4677
            case 0x6C:
19✔
4678
                $name = 'tRefN';
4✔
4679
                $size = 5;
4✔
4680
                $data = Xls\Biff8::readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
4✔
4681

4682
                break;
4✔
4683
            case 0x2D:    //    Relative 2d range reference
17✔
4684
            case 0x4D:
16✔
4685
            case 0x6D:
16✔
4686
                $name = 'tAreaN';
1✔
4687
                $size = 9;
1✔
4688
                $data = Xls\Biff8::readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
1✔
4689

4690
                break;
1✔
4691
            case 0x39:    //    External name
16✔
4692
            case 0x59:
15✔
4693
            case 0x79:
15✔
4694
                $name = 'tNameX';
1✔
4695
                $size = 7;
1✔
4696
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
4697
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
4698
                $index = self::getUInt2d($formulaData, 3);
1✔
4699
                // assume index is to EXTERNNAME record
4700
                $data = $this->externalNames[$index - 1]['name'] ?? '';
1✔
4701

4702
                // offset: 5; size: 2; not used
4703
                break;
1✔
4704
            case 0x3A:    //    3d reference to cell
15✔
4705
            case 0x5A:
12✔
4706
            case 0x7A:
12✔
4707
                $name = 'tRef3d';
4✔
4708
                $size = 7;
4✔
4709

4710
                try {
4711
                    // offset: 1; size: 2; index to REF entry
4712
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
4✔
4713
                    // offset: 3; size: 4; cell address
4714
                    $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 3, 4));
4✔
4715

4716
                    $data = "$sheetRange!$cellAddress";
4✔
4717
                } catch (PhpSpreadsheetException) {
×
4718
                    // deleted sheet reference
4719
                    $data = '#REF!';
×
4720
                }
4721

4722
                break;
4✔
4723
            case 0x3B:    //    3d reference to cell range
11✔
4724
            case 0x5B:
1✔
4725
            case 0x7B:
1✔
4726
                $name = 'tArea3d';
10✔
4727
                $size = 11;
10✔
4728

4729
                try {
4730
                    // offset: 1; size: 2; index to REF entry
4731
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
10✔
4732
                    // offset: 3; size: 8; cell address
4733
                    $cellRangeAddress = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
10✔
4734

4735
                    $data = "$sheetRange!$cellRangeAddress";
10✔
4736
                } catch (PhpSpreadsheetException) {
×
4737
                    // deleted sheet reference
4738
                    $data = '#REF!';
×
4739
                }
4740

4741
                break;
10✔
4742
                // Unknown cases    // don't know how to deal with
4743
            default:
4744
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
1✔
4745
        }
4746

4747
        return [
73✔
4748
            'id' => $id,
73✔
4749
            'name' => $name,
73✔
4750
            'size' => $size,
73✔
4751
            'data' => $data,
73✔
4752
        ];
73✔
4753
    }
4754

4755
    /**
4756
     * Get a sheet range like Sheet1:Sheet3 from REF index
4757
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
4758
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
4759
     * in which case an Exception is thrown.
4760
     */
4761
    protected function readSheetRangeByRefIndex(int $index): string|false
14✔
4762
    {
4763
        if (isset($this->ref[$index])) {
14✔
4764
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
14✔
4765

4766
            switch ($type) {
4767
                case 'internal':
14✔
4768
                    // check if we have a deleted 3d reference
4769
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
14✔
4770
                        throw new Exception('Deleted sheet reference');
×
4771
                    }
4772

4773
                    // we have normal sheet range (collapsed or uncollapsed)
4774
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
14✔
4775
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
14✔
4776

4777
                    if ($firstSheetName == $lastSheetName) {
14✔
4778
                        // collapsed sheet range
4779
                        $sheetRange = $firstSheetName;
14✔
4780
                    } else {
4781
                        $sheetRange = "$firstSheetName:$lastSheetName";
×
4782
                    }
4783

4784
                    // escape the single-quotes
4785
                    $sheetRange = str_replace("'", "''", $sheetRange);
14✔
4786

4787
                    // if there are special characters, we need to enclose the range in single-quotes
4788
                    // todo: check if we have identified the whole set of special characters
4789
                    // it seems that the following characters are not accepted for sheet names
4790
                    // and we may assume that they are not present: []*/:\?
4791
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
14✔
4792
                        $sheetRange = "'$sheetRange'";
4✔
4793
                    }
4794

4795
                    return $sheetRange;
14✔
4796
                default:
4797
                    // TODO: external sheet support
4798
                    throw new Exception('Xls reader only supports internal sheets in formulas');
×
4799
            }
4800
        }
4801

4802
        return false;
×
4803
    }
4804

4805
    /**
4806
     * Read byte string (8-bit string length)
4807
     * OpenOffice documentation: 2.5.2.
4808
     *
4809
     * @return array{value: mixed, size: int}
4810
     */
4811
    protected function readByteStringShort(string $subData): array
7✔
4812
    {
4813
        // offset: 0; size: 1; length of the string (character count)
4814
        $ln = ord($subData[0]);
7✔
4815

4816
        // offset: 1: size: var; character array (8-bit characters)
4817
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7✔
4818

4819
        return [
7✔
4820
            'value' => $value,
7✔
4821
            'size' => 1 + $ln, // size in bytes of data structure
7✔
4822
        ];
7✔
4823
    }
4824

4825
    /**
4826
     * Read byte string (16-bit string length)
4827
     * OpenOffice documentation: 2.5.2.
4828
     *
4829
     * @return array{value: mixed, size: int}
4830
     */
4831
    protected function readByteStringLong(string $subData): array
2✔
4832
    {
4833
        // offset: 0; size: 2; length of the string (character count)
4834
        $ln = self::getUInt2d($subData, 0);
2✔
4835

4836
        // offset: 2: size: var; character array (8-bit characters)
4837
        $value = $this->decodeCodepage(substr($subData, 2));
2✔
4838

4839
        //return $string;
4840
        return [
2✔
4841
            'value' => $value,
2✔
4842
            'size' => 2 + $ln, // size in bytes of data structure
2✔
4843
        ];
2✔
4844
    }
4845

4846
    protected function parseRichText(string $is): RichText
3✔
4847
    {
4848
        $value = new RichText();
3✔
4849
        $value->createText($is);
3✔
4850

4851
        return $value;
3✔
4852
    }
4853

4854
    /**
4855
     * Phpstan 1.4.4 complains that this property is never read.
4856
     * So, we might be able to get rid of it altogether.
4857
     * For now, however, this function makes it readable,
4858
     * which satisfies Phpstan.
4859
     *
4860
     * @return mixed[]
4861
     *
4862
     * @codeCoverageIgnore
4863
     */
4864
    public function getMapCellStyleXfIndex(): array
4865
    {
4866
        return $this->mapCellStyleXfIndex;
4867
    }
4868

4869
    /**
4870
     * Parse conditional formatting blocks.
4871
     *
4872
     * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
4873
     *
4874
     * @return mixed[]
4875
     */
4876
    protected function readCFHeader(): array
24✔
4877
    {
4878
        return (new Xls\ConditionalFormatting())->readCFHeader2($this);
24✔
4879
    }
4880

4881
    /** @param string[] $cellRangeAddresses */
4882
    protected function readCFRule(array $cellRangeAddresses): void
24✔
4883
    {
4884
        (new Xls\ConditionalFormatting())->readCFRule2($cellRangeAddresses, $this);
24✔
4885
    }
4886

4887
    public function getVersion(): int
5✔
4888
    {
4889
        return $this->version;
5✔
4890
    }
4891
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc