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

PHPOffice / PhpSpreadsheet / 22959268165

11 Mar 2026 03:03PM UTC coverage: 96.914% (+0.01%) from 96.904%
22959268165

Pull #4833

github

web-flow
Merge 6ae5eee83 into a1dacfdf7
Pull Request #4833: Optimize XLS (BIFF8) reader performance with reduced per-cell overhead

74 of 77 new or added lines in 3 files covered. (96.1%)

88 existing lines in 1 file now uncovered.

47709 of 49228 relevant lines covered (96.91%)

383.96 hits per line

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

89.01
/src/PhpSpreadsheet/Reader/Xls.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Cell\DataType;
9
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
10
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
12
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern;
13
use PhpOffice\PhpSpreadsheet\RichText\RichText;
14
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
15
use PhpOffice\PhpSpreadsheet\Shared\Date;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher;
17
use PhpOffice\PhpSpreadsheet\Shared\File;
18
use PhpOffice\PhpSpreadsheet\Shared\OLE;
19
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
20
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
21
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22
use PhpOffice\PhpSpreadsheet\Style\Alignment;
23
use PhpOffice\PhpSpreadsheet\Style\Border;
24
use PhpOffice\PhpSpreadsheet\Style\Borders;
25
use PhpOffice\PhpSpreadsheet\Style\Conditional;
26
use PhpOffice\PhpSpreadsheet\Style\Fill;
27
use PhpOffice\PhpSpreadsheet\Style\Font;
28
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
29
use PhpOffice\PhpSpreadsheet\Style\Protection;
30
use PhpOffice\PhpSpreadsheet\Style\Style;
31
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
32
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
33
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
34

35
// Original file header of ParseXL (used as the base for this class):
36
// --------------------------------------------------------------------------------
37
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
38
// trex005, and mmp11 (SourceForge.net)
39
// https://sourceforge.net/projects/phpexcelreader/
40
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
41
//     Modelled moreso after Perl Excel Parse/Write modules
42
//     Added Parse_Excel_Spreadsheet object
43
//         Reads a whole worksheet or tab as row,column array or as
44
//         associated hash of indexed rows and named column fields
45
//     Added variables for worksheet (tab) indexes and names
46
//     Added an object call for loading individual woorksheets
47
//     Changed default indexing defaults to 0 based arrays
48
//     Fixed date/time and percent formats
49
//     Includes patches found at SourceForge...
50
//         unicode patch by nobody
51
//         unpack("d") machine depedency patch by matchy
52
//         boundsheet utf16 patch by bjaenichen
53
//     Renamed functions for shorter names
54
//     General code cleanup and rigor, including <80 column width
55
//     Included a testcase Excel file and PHP example calls
56
//     Code works for PHP 5.x
57

58
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
59
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
60
//     Decoding of formula conditions, results, and tokens.
61
//     Support for user-defined named cells added as an array "namedcells"
62
//         Patch code for user-defined named cells supports single cells only.
63
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
64
//         external sheet reference structure
65
class Xls extends XlsBase
66
{
67
    /**
68
     * Summary Information stream data.
69
     */
70
    protected ?string $summaryInformation = null;
71

72
    /**
73
     * Extended Summary Information stream data.
74
     */
75
    protected ?string $documentSummaryInformation = null;
76

77
    /**
78
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
79
     */
80
    protected string $data;
81

82
    /**
83
     * Size in bytes of $this->data.
84
     */
85
    protected int $dataSize;
86

87
    /**
88
     * Current position in stream.
89
     */
90
    protected int $pos;
91

92
    /**
93
     * Workbook to be returned by the reader.
94
     */
95
    protected Spreadsheet $spreadsheet;
96

97
    /**
98
     * Worksheet that is currently being built by the reader.
99
     */
100
    protected Worksheet $phpSheet;
101

102
    /**
103
     * Cached sheet title for the current sheet being parsed.
104
     * Avoids repeated getTitle() calls in per-cell read filter checks.
105
     */
106
    protected string $phpSheetTitle = '';
107

108
    /**
109
     * Cached read filter reference.
110
     * Avoids repeated getReadFilter() calls in per-cell read filter checks.
111
     */
112
    protected IReadFilter $cachedReadFilter;
113

114
    /**
115
     * BIFF version.
116
     */
117
    protected int $version = 0;
118

119
    /**
120
     * Shared formats.
121
     *
122
     * @var mixed[]
123
     */
124
    protected array $formats;
125

126
    /**
127
     * Shared fonts.
128
     *
129
     * @var Font[]
130
     */
131
    protected array $objFonts;
132

133
    /**
134
     * Color palette.
135
     *
136
     * @var string[][]
137
     */
138
    protected array $palette;
139

140
    /**
141
     * Worksheets.
142
     *
143
     * @var array<array{name: string, offset: int, sheetState: string, sheetType: int|string}>
144
     */
145
    protected array $sheets;
146

147
    /**
148
     * External books.
149
     *
150
     * @var mixed[][]
151
     */
152
    protected array $externalBooks;
153

154
    /**
155
     * REF structures. Only applies to BIFF8.
156
     *
157
     * @var array<int, array{'externalBookIndex': int, 'firstSheetIndex': int, 'lastSheetIndex': int}>
158
     */
159
    protected array $ref;
160

161
    /**
162
     * External names.
163
     *
164
     * @var array<array<string, mixed>|string>
165
     */
166
    protected array $externalNames;
167

168
    /**
169
     * Defined names.
170
     *
171
     * @var array{isBuiltInName: int, name: string, formula: string, scope: int}
172
     */
173
    protected array $definedname;
174

175
    /**
176
     * Shared strings. Only applies to BIFF8.
177
     *
178
     * @var array<array{value: string, fmtRuns: mixed[]}>
179
     */
180
    protected array $sst;
181

182
    /**
183
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
184
     */
185
    protected bool $frozen;
186

187
    /**
188
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
189
     */
190
    protected bool $isFitToPages;
191

192
    /**
193
     * Objects. One OBJ record contributes with one entry.
194
     *
195
     * @var mixed[]
196
     */
197
    protected array $objs;
198

199
    /**
200
     * Text Objects. One TXO record corresponds with one entry.
201
     *
202
     * @var array<array{text: string, format: string, alignment: int, rotation: int}>
203
     */
204
    protected array $textObjects;
205

206
    /**
207
     * Cell Annotations (BIFF8).
208
     *
209
     * @var mixed[]
210
     */
211
    protected array $cellNotes;
212

213
    /**
214
     * The combined MSODRAWINGGROUP data.
215
     */
216
    protected string $drawingGroupData;
217

218
    /**
219
     * The combined MSODRAWING data (per sheet).
220
     */
221
    protected string $drawingData;
222

223
    /**
224
     * Keep track of XF index.
225
     */
226
    protected int $xfIndex;
227

228
    /**
229
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
230
     *
231
     * @var int[]
232
     */
233
    protected array $mapCellXfIndex;
234

235
    /**
236
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
237
     *
238
     * @var int[]
239
     */
240
    protected array $mapCellStyleXfIndex;
241

242
    /**
243
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
244
     *
245
     * @var mixed[]
246
     */
247
    protected array $sharedFormulas;
248

249
    /**
250
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
251
     * refers to a shared formula.
252
     *
253
     * @var mixed[]
254
     */
255
    protected array $sharedFormulaParts;
256

257
    /**
258
     * The type of encryption in use.
259
     */
260
    protected int $encryption = 0;
261

262
    /**
263
     * The position in the stream after which contents are encrypted.
264
     */
265
    protected int $encryptionStartPos = 0;
266

267
    protected string $encryptionPassword = 'VelvetSweatshop';
268

269
    /**
270
     * The current RC4 decryption object.
271
     */
272
    protected ?Xls\RC4 $rc4Key = null;
273

274
    /**
275
     * The position in the stream that the RC4 decryption object was left at.
276
     */
277
    protected int $rc4Pos = 0;
278

279
    /**
280
     * The current MD5 context state.
281
     * It is set via call-by-reference to verifyPassword.
282
     */
283
    private string $md5Ctxt = '';
284

285
    protected int $textObjRef;
286

287
    protected string $baseCell;
288

289
    protected bool $activeSheetSet = false;
290

291
    /**
292
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
293
     *
294
     * @return string[]
295
     */
296
    public function listWorksheetNames(string $filename): array
7✔
297
    {
298
        return (new Xls\ListFunctions())->listWorksheetNames2($filename, $this);
7✔
299
    }
300

301
    /**
302
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
303
     *
304
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
305
     */
306
    public function listWorksheetInfo(string $filename): array
7✔
307
    {
308
        return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this);
7✔
309
    }
310

311
    /**
312
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
313
     *
314
     * @return array<int, array{worksheetName: string, dimensionsMinR: int, dimensionsMinC: int, dimensionsMaxR: int, dimensionsMaxC: int, lastColumnLetter: string}>
315
     */
316
    public function listWorksheetDimensions(string $filename): array
2✔
317
    {
318
        return (new Xls\ListFunctions())->listWorksheetDimensions2($filename, $this);
2✔
319
    }
320

321
    /**
322
     * Loads PhpSpreadsheet from file.
323
     */
324
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
140✔
325
    {
326
        return (new Xls\LoadSpreadsheet())->loadSpreadsheetFromFile2($filename, $this);
140✔
327
    }
328

329
    /**
330
     * Read record data from stream, decrypting as required.
331
     *
332
     * @param string $data Data stream to read from
333
     * @param int $pos Position to start reading from
334
     * @param int $len Record data length
335
     *
336
     * @return string Record data
337
     */
338
    protected function readRecordData(string $data, int $pos, int $len): string
153✔
339
    {
340
        $data = substr($data, $pos, $len);
153✔
341

342
        // File not encrypted, or record before encryption start point
343
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
153✔
344
            return $data;
153✔
345
        }
346

347
        $recordData = '';
2✔
348
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
2✔
349
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
2✔
350
            $block = (int) floor($pos / self::REKEY_BLOCK);
2✔
351
            $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK);
2✔
352

353
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
354
            // at a point earlier in the current block, re-use it as we can save some time.
355
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
2✔
356
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
2✔
357
                $step = $pos % self::REKEY_BLOCK;
2✔
358
            } else {
359
                $step = $pos - $this->rc4Pos;
2✔
360
            }
361
            $this->rc4Key->RC4(str_repeat("\0", $step));
2✔
362

363
            // Decrypt record data (re-keying at the end of every block)
364
            while ($block != $endBlock) {
2✔
365
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1✔
366
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1✔
367
                $data = substr($data, $step);
1✔
368
                $pos += $step;
1✔
369
                $len -= $step;
1✔
370
                ++$block;
1✔
371
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1✔
372
            }
373
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
2✔
374

375
            // Keep track of the position of this decryptor.
376
            // We'll try and re-use it later if we can to speed things up
377
            $this->rc4Pos = $pos + $len;
2✔
378
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
×
379
            throw new Exception('XOr encryption not supported');
×
380
        }
381

382
        return $recordData;
2✔
383
    }
384

385
    /**
386
     * Use OLE reader to extract the relevant data streams from the OLE file.
387
     */
388
    protected function loadOLE(string $filename): void
153✔
389
    {
390
        // OLE reader
391
        $ole = new OLERead();
153✔
392
        // get excel data,
393
        $ole->read($filename);
153✔
394
        // Get workbook data: workbook stream + sheet streams
395
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
153✔
396
        // Get summary information data
397
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
153✔
398
        // Get additional document summary information data
399
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
153✔
400
    }
401

402
    /**
403
     * Read summary information.
404
     */
405
    protected function readSummaryInformation(): void
140✔
406
    {
407
        if (!isset($this->summaryInformation)) {
140✔
408
            return;
3✔
409
        }
410

411
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
412
        // offset: 2; size: 2;
413
        // offset: 4; size: 2; OS version
414
        // offset: 6; size: 2; OS indicator
415
        // offset: 8; size: 16
416
        // offset: 24; size: 4; section count
417
        //$secCount = self::getInt4d($this->summaryInformation, 24);
418

419
        // 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
420
        // offset: 44; size: 4
421
        $secOffset = self::getInt4d($this->summaryInformation, 44);
137✔
422

423
        // section header
424
        // offset: $secOffset; size: 4; section length
425
        //$secLength = self::getInt4d($this->summaryInformation, $secOffset);
426

427
        // offset: $secOffset+4; size: 4; property count
428
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
137✔
429

430
        // initialize code page (used to resolve string values)
431
        $codePage = 'CP1252';
137✔
432

433
        // offset: ($secOffset+8); size: var
434
        // loop through property decarations and properties
435
        for ($i = 0; $i < $countProperties; ++$i) {
137✔
436
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
437
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
137✔
438

439
            // Use value of property id as appropriate
440
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
441
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
137✔
442

443
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
137✔
444

445
            // initialize property value
446
            $value = null;
137✔
447

448
            // extract property value based on property type
449
            switch ($type) {
450
                case 0x02: // 2 byte signed integer
137✔
451
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
137✔
452

453
                    break;
137✔
454
                case 0x03: // 4 byte signed integer
137✔
455
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
133✔
456

457
                    break;
133✔
458
                case 0x13: // 4 byte unsigned integer
137✔
459
                    // not needed yet, fix later if necessary
460
                    break;
1✔
461
                case 0x1E: // null-terminated string prepended by dword string length
137✔
462
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
135✔
463
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
135✔
464
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
135✔
465
                    $value = rtrim($value);
135✔
466

467
                    break;
135✔
468
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
137✔
469
                    // PHP-time
470
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
137✔
471

472
                    break;
137✔
473
                case 0x47: // Clipboard format
2✔
474
                    // not needed yet, fix later if necessary
475
                    break;
×
476
            }
477

478
            switch ($id) {
479
                case 0x01:    //    Code Page
137✔
480
                    $codePage = CodePage::numberToName((int) $value);
137✔
481

482
                    break;
137✔
483
                case 0x02:    //    Title
137✔
484
                    $this->spreadsheet->getProperties()->setTitle("$value");
81✔
485

486
                    break;
81✔
487
                case 0x03:    //    Subject
137✔
488
                    $this->spreadsheet->getProperties()->setSubject("$value");
17✔
489

490
                    break;
17✔
491
                case 0x04:    //    Author (Creator)
137✔
492
                    $this->spreadsheet->getProperties()->setCreator("$value");
123✔
493

494
                    break;
123✔
495
                case 0x05:    //    Keywords
137✔
496
                    $this->spreadsheet->getProperties()->setKeywords("$value");
17✔
497

498
                    break;
17✔
499
                case 0x06:    //    Comments (Description)
137✔
500
                    $this->spreadsheet->getProperties()->setDescription("$value");
17✔
501

502
                    break;
17✔
503
                case 0x07:    //    Template
137✔
504
                    //    Not supported by PhpSpreadsheet
505
                    break;
×
506
                case 0x08:    //    Last Saved By (LastModifiedBy)
137✔
507
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
135✔
508

509
                    break;
135✔
510
                case 0x09:    //    Revision
137✔
511
                    //    Not supported by PhpSpreadsheet
512
                    break;
3✔
513
                case 0x0A:    //    Total Editing Time
137✔
514
                    //    Not supported by PhpSpreadsheet
515
                    break;
3✔
516
                case 0x0B:    //    Last Printed
137✔
517
                    //    Not supported by PhpSpreadsheet
518
                    break;
7✔
519
                case 0x0C:    //    Created Date/Time
137✔
520
                    $this->spreadsheet->getProperties()->setCreated($value);
130✔
521

522
                    break;
130✔
523
                case 0x0D:    //    Modified Date/Time
137✔
524
                    $this->spreadsheet->getProperties()->setModified($value);
136✔
525

526
                    break;
136✔
527
                case 0x0E:    //    Number of Pages
134✔
528
                    //    Not supported by PhpSpreadsheet
529
                    break;
×
530
                case 0x0F:    //    Number of Words
134✔
531
                    //    Not supported by PhpSpreadsheet
532
                    break;
×
533
                case 0x10:    //    Number of Characters
134✔
534
                    //    Not supported by PhpSpreadsheet
535
                    break;
×
536
                case 0x11:    //    Thumbnail
134✔
537
                    //    Not supported by PhpSpreadsheet
538
                    break;
×
539
                case 0x12:    //    Name of creating application
134✔
540
                    //    Not supported by PhpSpreadsheet
541
                    break;
50✔
542
                case 0x13:    //    Security
134✔
543
                    //    Not supported by PhpSpreadsheet
544
                    break;
133✔
545
            }
546
        }
547
    }
548

549
    /**
550
     * Read additional document summary information.
551
     */
552
    protected function readDocumentSummaryInformation(): void
140✔
553
    {
554
        if (!isset($this->documentSummaryInformation)) {
140✔
555
            return;
4✔
556
        }
557

558
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
559
        //    offset: 2;    size: 2;
560
        //    offset: 4;    size: 2;    OS version
561
        //    offset: 6;    size: 2;    OS indicator
562
        //    offset: 8;    size: 16
563
        //    offset: 24;    size: 4;    section count
564
        //$secCount = self::getInt4d($this->documentSummaryInformation, 24);
565

566
        // 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
567
        // offset: 44;    size: 4;    first section offset
568
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
136✔
569

570
        //    section header
571
        //    offset: $secOffset;    size: 4;    section length
572
        //$secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
573

574
        //    offset: $secOffset+4;    size: 4;    property count
575
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
136✔
576

577
        // initialize code page (used to resolve string values)
578
        $codePage = 'CP1252';
136✔
579

580
        //    offset: ($secOffset+8);    size: var
581
        //    loop through property decarations and properties
582
        for ($i = 0; $i < $countProperties; ++$i) {
136✔
583
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
584
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
136✔
585

586
            // Use value of property id as appropriate
587
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
588
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
136✔
589

590
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
136✔
591

592
            // initialize property value
593
            $value = null;
136✔
594

595
            // extract property value based on property type
596
            switch ($type) {
597
                case 0x02:    //    2 byte signed integer
136✔
598
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
136✔
599

600
                    break;
136✔
601
                case 0x03:    //    4 byte signed integer
133✔
602
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
130✔
603

604
                    break;
130✔
605
                case 0x0B:  // Boolean
133✔
606
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
133✔
607
                    $value = ($value == 0 ? false : true);
133✔
608

609
                    break;
133✔
610
                case 0x13:    //    4 byte unsigned integer
132✔
611
                    // not needed yet, fix later if necessary
612
                    break;
1✔
613
                case 0x1E:    //    null-terminated string prepended by dword string length
131✔
614
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
52✔
615
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
52✔
616
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
52✔
617
                    $value = rtrim($value);
52✔
618

619
                    break;
52✔
620
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
131✔
621
                    // PHP-Time
622
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
×
623

624
                    break;
×
625
                case 0x47:    //    Clipboard format
131✔
626
                    // not needed yet, fix later if necessary
627
                    break;
×
628
            }
629

630
            switch ($id) {
631
                case 0x01:    //    Code Page
136✔
632
                    $codePage = CodePage::numberToName((int) $value);
136✔
633

634
                    break;
136✔
635
                case 0x02:    //    Category
133✔
636
                    $this->spreadsheet->getProperties()->setCategory("$value");
17✔
637

638
                    break;
17✔
639
                case 0x03:    //    Presentation Target
133✔
640
                    //    Not supported by PhpSpreadsheet
641
                    break;
×
642
                case 0x04:    //    Bytes
133✔
643
                    //    Not supported by PhpSpreadsheet
644
                    break;
×
645
                case 0x05:    //    Lines
133✔
646
                    //    Not supported by PhpSpreadsheet
647
                    break;
×
648
                case 0x06:    //    Paragraphs
133✔
649
                    //    Not supported by PhpSpreadsheet
650
                    break;
×
651
                case 0x07:    //    Slides
133✔
652
                    //    Not supported by PhpSpreadsheet
653
                    break;
×
654
                case 0x08:    //    Notes
133✔
655
                    //    Not supported by PhpSpreadsheet
656
                    break;
×
657
                case 0x09:    //    Hidden Slides
133✔
658
                    //    Not supported by PhpSpreadsheet
659
                    break;
×
660
                case 0x0A:    //    MM Clips
133✔
661
                    //    Not supported by PhpSpreadsheet
662
                    break;
×
663
                case 0x0B:    //    Scale Crop
133✔
664
                    //    Not supported by PhpSpreadsheet
665
                    break;
133✔
666
                case 0x0C:    //    Heading Pairs
133✔
667
                    //    Not supported by PhpSpreadsheet
668
                    break;
131✔
669
                case 0x0D:    //    Titles of Parts
133✔
670
                    //    Not supported by PhpSpreadsheet
671
                    break;
131✔
672
                case 0x0E:    //    Manager
133✔
673
                    $this->spreadsheet->getProperties()->setManager("$value");
2✔
674

675
                    break;
2✔
676
                case 0x0F:    //    Company
133✔
677
                    $this->spreadsheet->getProperties()->setCompany("$value");
42✔
678

679
                    break;
42✔
680
                case 0x10:    //    Links up-to-date
133✔
681
                    //    Not supported by PhpSpreadsheet
682
                    break;
133✔
683
            }
684
        }
685
    }
686

687
    /**
688
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
689
     */
690
    protected function readDefault(): void
151✔
691
    {
692
        $length = self::getUInt2d($this->data, $this->pos + 2);
151✔
693

694
        // move stream pointer to next record
695
        $this->pos += 4 + $length;
151✔
696
    }
697

698
    /**
699
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
700
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
701
     */
702
    protected function readNote(): void
3✔
703
    {
704
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
705
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
706

707
        // move stream pointer to next record
708
        $this->pos += 4 + $length;
3✔
709

710
        if ($this->readDataOnly) {
3✔
711
            return;
×
712
        }
713

714
        $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($recordData, 0, 4));
3✔
715
        if ($this->version == self::XLS_BIFF8) {
3✔
716
            $noteObjID = self::getUInt2d($recordData, 6);
2✔
717
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
2✔
718
            $noteAuthor = $noteAuthor['value'];
2✔
719
            $this->cellNotes[$noteObjID] = [
2✔
720
                'cellRef' => $cellAddress,
2✔
721
                'objectID' => $noteObjID,
2✔
722
                'author' => $noteAuthor,
2✔
723
            ];
2✔
724
        } else {
725
            $extension = false;
1✔
726
            if ($cellAddress === '$B$' . AddressRange::MAX_ROW_XLS) {
1✔
727
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
728
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
729
                //        max 2048 bytes will probably throw a wobbly.
730
                //$row = self::getUInt2d($recordData, 0);
731
                $extension = true;
×
732
                $arrayKeys = array_keys($this->phpSheet->getComments());
×
733
                $cellAddress = array_pop($arrayKeys);
×
734
            }
735

736
            $cellAddress = str_replace('$', '', (string) $cellAddress);
1✔
737
            //$noteLength = self::getUInt2d($recordData, 4);
738
            $noteText = trim(substr($recordData, 6));
1✔
739

740
            if ($extension) {
1✔
741
                //    Concatenate this extension with the currently set comment for the cell
742
                $comment = $this->phpSheet->getComment($cellAddress);
×
743
                $commentText = $comment->getText()->getPlainText();
×
744
                $comment->setText($this->parseRichText($commentText . $noteText));
×
745
            } else {
746
                //    Set comment for the cell
747
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1✔
748
//                                                    ->setAuthor($author)
749
            }
750
        }
751
    }
752

753
    /**
754
     * The TEXT Object record contains the text associated with a cell annotation.
755
     */
756
    protected function readTextObject(): void
2✔
757
    {
758
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
759
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
760

761
        // move stream pointer to next record
762
        $this->pos += 4 + $length;
2✔
763

764
        if ($this->readDataOnly) {
2✔
765
            return;
×
766
        }
767

768
        // recordData consists of an array of subrecords looking like this:
769
        //    grbit: 2 bytes; Option Flags
770
        //    rot: 2 bytes; rotation
771
        //    cchText: 2 bytes; length of the text (in the first continue record)
772
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
773
        // followed by the continuation records containing the actual text and formatting
774
        $grbitOpts = self::getUInt2d($recordData, 0);
2✔
775
        $rot = self::getUInt2d($recordData, 2);
2✔
776
        //$cchText = self::getUInt2d($recordData, 10);
777
        $cbRuns = self::getUInt2d($recordData, 12);
2✔
778
        $text = $this->getSplicedRecordData();
2✔
779

780
        /** @var int[] */
781
        $tempSplice = $text['spliceOffsets'];
2✔
782
        /** @var int */
783
        $temp = $tempSplice[0];
2✔
784
        /** @var int */
785
        $temp1 = $tempSplice[1];
2✔
786
        $textByte = $temp1 - $temp - 1;
2✔
787
        /** @var string */
788
        $textRecordData = $text['recordData'];
2✔
789
        $textStr = substr($textRecordData, $temp + 1, $textByte);
2✔
790
        // get 1 byte
791
        $is16Bit = ord($textRecordData[0]);
2✔
792
        // it is possible to use a compressed format,
793
        // which omits the high bytes of all characters, if they are all zero
794
        if (($is16Bit & 0x01) === 0) {
2✔
795
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
2✔
796
        } else {
797
            $textStr = $this->decodeCodepage($textStr);
×
798
        }
799

800
        $this->textObjects[$this->textObjRef] = [
2✔
801
            'text' => $textStr,
2✔
802
            'format' => substr($textRecordData, $tempSplice[1], $cbRuns),
2✔
803
            'alignment' => $grbitOpts,
2✔
804
            'rotation' => $rot,
2✔
805
        ];
2✔
806
    }
807

808
    /**
809
     * Read BOF.
810
     */
811
    protected function readBof(): void
153✔
812
    {
813
        $length = self::getUInt2d($this->data, $this->pos + 2);
153✔
814
        $recordData = substr($this->data, $this->pos + 4, $length);
153✔
815

816
        // move stream pointer to next record
817
        $this->pos += 4 + $length;
153✔
818

819
        // offset: 2; size: 2; type of the following data
820
        $substreamType = self::getUInt2d($recordData, 2);
153✔
821

822
        switch ($substreamType) {
823
            case self::XLS_WORKBOOKGLOBALS:
153✔
824
                $version = self::getUInt2d($recordData, 0);
153✔
825
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
153✔
826
                    throw new Exception('Cannot read this Excel file. Version is too old.');
×
827
                }
828
                $this->version = $version;
153✔
829

830
                break;
153✔
831
            case self::XLS_WORKSHEET:
142✔
832
                // do not use this version information for anything
833
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
834
                break;
142✔
835
            default:
836
                // substream, e.g. chart
837
                // just skip the entire substream
838
                do {
839
                    $code = self::getUInt2d($this->data, $this->pos);
×
840
                    $this->readDefault();
×
841
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
×
842

843
                break;
×
844
        }
845
    }
846

847
    public function setEncryptionPassword(string $encryptionPassword): self
1✔
848
    {
849
        $this->encryptionPassword = $encryptionPassword;
1✔
850

851
        return $this;
1✔
852
    }
853

854
    /**
855
     * FILEPASS.
856
     *
857
     * This record is part of the File Protection Block. It
858
     * contains information about the read/write password of the
859
     * file. All record contents following this record will be
860
     * encrypted.
861
     *
862
     * --    "OpenOffice.org's Documentation of the Microsoft
863
     *         Excel File Format"
864
     *
865
     * The decryption functions and objects used from here on in
866
     * are based on the source of Spreadsheet-ParseExcel:
867
     * https://metacpan.org/release/Spreadsheet-ParseExcel
868
     */
869
    protected function readFilepass(): void
4✔
870
    {
871
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
872

873
        if ($length < 54) {
4✔
874
            throw new Exception('Unexpected file pass record length');
×
875
        }
876

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

879
        // move stream pointer to next record
880
        $this->pos += 4 + $length;
4✔
881

882
        if (substr($recordData, 0, 2) !== "\x01\x00" || substr($recordData, 4, 2) !== "\x01\x00") {
4✔
883
            throw new Exception('Unsupported encryption algorithm');
1✔
884
        }
885
        if (!$this->verifyPassword($this->encryptionPassword, substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
3✔
886
            throw new Exception('Decryption password incorrect');
1✔
887
        }
888

889
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
2✔
890

891
        // Decryption required from the record after next onwards
892
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
2✔
893
    }
894

895
    /**
896
     * Make an RC4 decryptor for the given block.
897
     *
898
     * @param int $block Block for which to create decrypto
899
     * @param string $valContext MD5 context state
900
     */
901
    private function makeKey(int $block, string $valContext): Xls\RC4
3✔
902
    {
903
        $pwarray = str_repeat("\0", 64);
3✔
904

905
        for ($i = 0; $i < 5; ++$i) {
3✔
906
            $pwarray[$i] = $valContext[$i];
3✔
907
        }
908

909
        $pwarray[5] = chr($block & 0xFF);
3✔
910
        $pwarray[6] = chr(($block >> 8) & 0xFF);
3✔
911
        $pwarray[7] = chr(($block >> 16) & 0xFF);
3✔
912
        $pwarray[8] = chr(($block >> 24) & 0xFF);
3✔
913

914
        $pwarray[9] = "\x80";
3✔
915
        $pwarray[56] = "\x48";
3✔
916

917
        $md5 = new Xls\MD5();
3✔
918
        $md5->add($pwarray);
3✔
919

920
        $s = $md5->getContext();
3✔
921

922
        return new Xls\RC4($s);
3✔
923
    }
924

925
    /**
926
     * Verify RC4 file password.
927
     *
928
     * @param string $password Password to check
929
     * @param string $docid Document id
930
     * @param string $salt_data Salt data
931
     * @param string $hashedsalt_data Hashed salt data
932
     * @param string $valContext Set to the MD5 context of the value
933
     *
934
     * @return bool Success
935
     */
936
    private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, string &$valContext): bool
3✔
937
    {
938
        $pwarray = str_repeat("\0", 64);
3✔
939

940
        $iMax = strlen($password);
3✔
941
        for ($i = 0; $i < $iMax; ++$i) {
3✔
942
            $o = ord(substr($password, $i, 1));
3✔
943
            $pwarray[2 * $i] = chr($o & 0xFF);
3✔
944
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xFF);
3✔
945
        }
946
        $pwarray[2 * $i] = chr(0x80);
3✔
947
        $pwarray[56] = chr(($i << 4) & 0xFF);
3✔
948

949
        $md5 = new Xls\MD5();
3✔
950
        $md5->add($pwarray);
3✔
951

952
        $mdContext1 = $md5->getContext();
3✔
953

954
        $offset = 0;
3✔
955
        $keyoffset = 0;
3✔
956
        $tocopy = 5;
3✔
957

958
        $md5->reset();
3✔
959

960
        while ($offset != 16) {
3✔
961
            if ((64 - $offset) < 5) {
3✔
962
                $tocopy = 64 - $offset;
3✔
963
            }
964
            for ($i = 0; $i <= $tocopy; ++$i) {
3✔
965
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
3✔
966
            }
967
            $offset += $tocopy;
3✔
968

969
            if ($offset == 64) {
3✔
970
                $md5->add($pwarray);
3✔
971
                $keyoffset = $tocopy;
3✔
972
                $tocopy = 5 - $tocopy;
3✔
973
                $offset = 0;
3✔
974

975
                continue;
3✔
976
            }
977

978
            $keyoffset = 0;
3✔
979
            $tocopy = 5;
3✔
980
            for ($i = 0; $i < 16; ++$i) {
3✔
981
                $pwarray[$offset + $i] = $docid[$i];
3✔
982
            }
983
            $offset += 16;
3✔
984
        }
985

986
        $pwarray[16] = "\x80";
3✔
987
        for ($i = 0; $i < 47; ++$i) {
3✔
988
            $pwarray[17 + $i] = "\0";
3✔
989
        }
990
        $pwarray[56] = "\x80";
3✔
991
        $pwarray[57] = "\x0a";
3✔
992

993
        $md5->add($pwarray);
3✔
994
        $valContext = $md5->getContext();
3✔
995

996
        $key = $this->makeKey(0, $valContext);
3✔
997

998
        $salt = $key->RC4($salt_data);
3✔
999
        $hashedsalt = $key->RC4($hashedsalt_data);
3✔
1000

1001
        $salt .= "\x80" . str_repeat("\0", 47);
3✔
1002
        $salt[56] = "\x80";
3✔
1003

1004
        $md5->reset();
3✔
1005
        $md5->add($salt);
3✔
1006
        $mdContext2 = $md5->getContext();
3✔
1007

1008
        return $mdContext2 == $hashedsalt;
3✔
1009
    }
1010

1011
    /**
1012
     * CODEPAGE.
1013
     *
1014
     * This record stores the text encoding used to write byte
1015
     * strings, stored as MS Windows code page identifier.
1016
     *
1017
     * --    "OpenOffice.org's Documentation of the Microsoft
1018
     *         Excel File Format"
1019
     */
1020
    protected function readCodepage(): void
147✔
1021
    {
1022
        $length = self::getUInt2d($this->data, $this->pos + 2);
147✔
1023
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
147✔
1024

1025
        // move stream pointer to next record
1026
        $this->pos += 4 + $length;
147✔
1027

1028
        // offset: 0; size: 2; code page identifier
1029
        $codepage = self::getUInt2d($recordData, 0);
147✔
1030

1031
        $this->codepage = CodePage::numberToName($codepage);
147✔
1032
    }
1033

1034
    /**
1035
     * DATEMODE.
1036
     *
1037
     * This record specifies the base date for displaying date
1038
     * values. All dates are stored as count of days past this
1039
     * base date. In BIFF2-BIFF4 this record is part of the
1040
     * Calculation Settings Block. In BIFF5-BIFF8 it is
1041
     * stored in the Workbook Globals Substream.
1042
     *
1043
     * --    "OpenOffice.org's Documentation of the Microsoft
1044
     *         Excel File Format"
1045
     */
1046
    protected function readDateMode(): void
137✔
1047
    {
1048
        $length = self::getUInt2d($this->data, $this->pos + 2);
137✔
1049
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
137✔
1050

1051
        // move stream pointer to next record
1052
        $this->pos += 4 + $length;
137✔
1053

1054
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
1055
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
137✔
1056
        $this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
137✔
1057
        if (ord($recordData[0]) == 1) {
137✔
1058
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1059
            $this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1060
        }
1061
    }
1062

1063
    /**
1064
     * Read a FONT record.
1065
     */
1066
    protected function readFont(): void
137✔
1067
    {
1068
        $length = self::getUInt2d($this->data, $this->pos + 2);
137✔
1069
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
137✔
1070

1071
        // move stream pointer to next record
1072
        $this->pos += 4 + $length;
137✔
1073

1074
        if (!$this->readDataOnly) {
137✔
1075
            $objFont = new Font();
135✔
1076

1077
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
1078
            $size = self::getUInt2d($recordData, 0);
135✔
1079
            $objFont->setSize($size / 20);
135✔
1080

1081
            // offset: 2; size: 2; option flags
1082
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
1083
            // bit: 1; mask 0x0002; italic
1084
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
135✔
1085
            if ($isItalic) {
135✔
1086
                $objFont->setItalic(true);
58✔
1087
            }
1088

1089
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
1090
            // bit: 3; mask 0x0008; strikethrough
1091
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
135✔
1092
            if ($isStrike) {
135✔
1093
                $objFont->setStrikethrough(true);
×
1094
            }
1095

1096
            // offset: 4; size: 2; colour index
1097
            $colorIndex = self::getUInt2d($recordData, 4);
135✔
1098
            $objFont->colorIndex = $colorIndex;
135✔
1099

1100
            // offset: 6; size: 2; font weight
1101
            $weight = self::getUInt2d($recordData, 6); // regular=400 bold=700
135✔
1102
            if ($weight >= 550) {
135✔
1103
                $objFont->setBold(true);
69✔
1104
            }
1105

1106
            // offset: 8; size: 2; escapement type
1107
            $escapement = self::getUInt2d($recordData, 8);
135✔
1108
            CellFont::escapement($objFont, $escapement);
135✔
1109

1110
            // offset: 10; size: 1; underline type
1111
            $underlineType = ord($recordData[10]);
135✔
1112
            CellFont::underline($objFont, $underlineType);
135✔
1113

1114
            // offset: 11; size: 1; font family
1115
            // offset: 12; size: 1; character set
1116
            // offset: 13; size: 1; not used
1117
            // offset: 14; size: var; font name
1118
            if ($this->version == self::XLS_BIFF8) {
135✔
1119
                $string = self::readUnicodeStringShort(substr($recordData, 14));
133✔
1120
            } else {
1121
                $string = $this->readByteStringShort(substr($recordData, 14));
2✔
1122
            }
1123
            /** @var string[] $string */
1124
            $objFont->setName($string['value']);
135✔
1125

1126
            $this->objFonts[] = $objFont;
135✔
1127
        }
1128
    }
1129

1130
    /**
1131
     * FORMAT.
1132
     *
1133
     * This record contains information about a number format.
1134
     * All FORMAT records occur together in a sequential list.
1135
     *
1136
     * In BIFF2-BIFF4 other records referencing a FORMAT record
1137
     * contain a zero-based index into this list. From BIFF5 on
1138
     * the FORMAT record contains the index itself that will be
1139
     * used by other records.
1140
     *
1141
     * --    "OpenOffice.org's Documentation of the Microsoft
1142
     *         Excel File Format"
1143
     */
1144
    protected function readFormat(): void
71✔
1145
    {
1146
        $length = self::getUInt2d($this->data, $this->pos + 2);
71✔
1147
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
71✔
1148

1149
        // move stream pointer to next record
1150
        $this->pos += 4 + $length;
71✔
1151

1152
        if (!$this->readDataOnly) {
71✔
1153
            $indexCode = self::getUInt2d($recordData, 0);
69✔
1154

1155
            if ($this->version == self::XLS_BIFF8) {
69✔
1156
                $string = self::readUnicodeStringLong(substr($recordData, 2));
67✔
1157
            } else {
1158
                // BIFF7
1159
                $string = $this->readByteStringShort(substr($recordData, 2));
2✔
1160
            }
1161

1162
            $formatString = $string['value'];
69✔
1163
            // Apache Open Office sets wrong case writing to xls - issue 2239
1164
            if ($formatString === 'GENERAL') {
69✔
1165
                $formatString = NumberFormat::FORMAT_GENERAL;
1✔
1166
            }
1167
            $this->formats[$indexCode] = $formatString;
69✔
1168
        }
1169
    }
1170

1171
    /**
1172
     * XF - Extended Format.
1173
     *
1174
     * This record contains formatting information for cells, rows, columns or styles.
1175
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
1176
     * and 1 cell XF.
1177
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
1178
     * and XF record 15 is a cell XF
1179
     * We only read the first cell style XF and skip the remaining cell style XF records
1180
     * We read all cell XF records.
1181
     *
1182
     * --    "OpenOffice.org's Documentation of the Microsoft
1183
     *         Excel File Format"
1184
     */
1185
    protected function readXf(): void
138✔
1186
    {
1187
        $length = self::getUInt2d($this->data, $this->pos + 2);
138✔
1188
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
138✔
1189

1190
        // move stream pointer to next record
1191
        $this->pos += 4 + $length;
138✔
1192

1193
        $objStyle = new Style();
138✔
1194

1195
        if (!$this->readDataOnly) {
138✔
1196
            // offset:  0; size: 2; Index to FONT record
1197
            if (self::getUInt2d($recordData, 0) < 4) {
136✔
1198
                $fontIndex = self::getUInt2d($recordData, 0);
136✔
1199
            } else {
1200
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
1201
                // check the OpenOffice documentation of the FONT record
1202
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
63✔
1203
            }
1204
            if (isset($this->objFonts[$fontIndex])) {
136✔
1205
                $objStyle->setFont($this->objFonts[$fontIndex]);
135✔
1206
            }
1207

1208
            // offset:  2; size: 2; Index to FORMAT record
1209
            $numberFormatIndex = self::getUInt2d($recordData, 2);
136✔
1210
            if (isset($this->formats[$numberFormatIndex])) {
136✔
1211
                // then we have user-defined format code
1212
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
63✔
1213
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
136✔
1214
                // then we have built-in format code
1215
                $numberFormat = ['formatCode' => $code];
136✔
1216
            } else {
1217
                // we set the general format code
1218
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
4✔
1219
            }
1220
            /** @var string[] $numberFormat */
1221
            $objStyle->getNumberFormat()
136✔
1222
                ->setFormatCode($numberFormat['formatCode']);
136✔
1223

1224
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
1225
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
1226
            $xfTypeProt = self::getUInt2d($recordData, 4);
136✔
1227
            // bit 0; mask 0x01; 1 = cell is locked
1228
            $isLocked = (0x01 & $xfTypeProt) >> 0;
136✔
1229
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
136✔
1230

1231
            // bit 1; mask 0x02; 1 = Formula is hidden
1232
            $isHidden = (0x02 & $xfTypeProt) >> 1;
136✔
1233
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
136✔
1234

1235
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
1236
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
136✔
1237

1238
            // offset:  6; size: 1; Alignment and text break
1239
            // bit 2-0, mask 0x07; horizontal alignment
1240
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
136✔
1241
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
136✔
1242

1243
            // bit 3, mask 0x08; wrap text
1244
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
136✔
1245
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
136✔
1246

1247
            // bit 6-4, mask 0x70; vertical alignment
1248
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
136✔
1249
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
136✔
1250

1251
            if ($this->version == self::XLS_BIFF8) {
136✔
1252
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
1253
                $angle = ord($recordData[7]);
134✔
1254
                $rotation = 0;
134✔
1255
                if ($angle <= 90) {
134✔
1256
                    $rotation = $angle;
134✔
1257
                } elseif ($angle <= 180) {
4✔
1258
                    $rotation = 90 - $angle;
×
1259
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
4✔
1260
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
4✔
1261
                }
1262
                $objStyle->getAlignment()->setTextRotation($rotation);
134✔
1263

1264
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
1265
                // bit: 3-0; mask: 0x0F; indent level
1266
                $indent = (0x0F & ord($recordData[8])) >> 0;
134✔
1267
                $objStyle->getAlignment()->setIndent($indent);
134✔
1268

1269
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
1270
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
134✔
1271
                switch ($shrinkToFit) {
1272
                    case 0:
134✔
1273
                        $objStyle->getAlignment()->setShrinkToFit(false);
134✔
1274

1275
                        break;
134✔
1276
                    case 1:
1✔
1277
                        $objStyle->getAlignment()->setShrinkToFit(true);
1✔
1278

1279
                        break;
1✔
1280
                }
1281
                $readOrder = (0xC0 & ord($recordData[8])) >> 6;
134✔
1282
                $objStyle->getAlignment()->setReadOrder($readOrder);
134✔
1283

1284
                // offset:  9; size: 1; Flags used for attribute groups
1285

1286
                // offset: 10; size: 4; Cell border lines and background area
1287
                // bit: 3-0; mask: 0x0000000F; left style
1288
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
134✔
1289
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
134✔
1290
                }
1291
                // bit: 7-4; mask: 0x000000F0; right style
1292
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
134✔
1293
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
134✔
1294
                }
1295
                // bit: 11-8; mask: 0x00000F00; top style
1296
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
134✔
1297
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
134✔
1298
                }
1299
                // bit: 15-12; mask: 0x0000F000; bottom style
1300
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
134✔
1301
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
134✔
1302
                }
1303
                // bit: 22-16; mask: 0x007F0000; left color
1304
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
134✔
1305

1306
                // bit: 29-23; mask: 0x3F800000; right color
1307
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
134✔
1308

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

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

1315
                if ($diagonalUp === false) {
134✔
1316
                    if ($diagonalDown === false) {
134✔
1317
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
134✔
1318
                    } else {
1319
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
1✔
1320
                    }
1321
                } elseif ($diagonalDown === false) {
1✔
1322
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
1✔
1323
                } else {
1324
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
1✔
1325
                }
1326

1327
                // offset: 14; size: 4;
1328
                // bit: 6-0; mask: 0x0000007F; top color
1329
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
134✔
1330

1331
                // bit: 13-7; mask: 0x00003F80; bottom color
1332
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
134✔
1333

1334
                // bit: 20-14; mask: 0x001FC000; diagonal color
1335
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
134✔
1336

1337
                // bit: 24-21; mask: 0x01E00000; diagonal style
1338
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
134✔
1339
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
134✔
1340
                }
1341

1342
                // bit: 31-26; mask: 0xFC000000 fill pattern
1343
                if ($fillType = FillPattern::lookup((self::FC000000 & self::getInt4d($recordData, 14)) >> 26)) {
134✔
1344
                    $objStyle->getFill()->setFillType($fillType);
134✔
1345
                }
1346
                // offset: 18; size: 2; pattern and background colour
1347
                // bit: 6-0; mask: 0x007F; color index for pattern color
1348
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
134✔
1349

1350
                // bit: 13-7; mask: 0x3F80; color index for pattern background
1351
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
134✔
1352
            } else {
1353
                // BIFF5
1354

1355
                // offset: 7; size: 1; Text orientation and flags
1356
                $orientationAndFlags = ord($recordData[7]);
2✔
1357

1358
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
1359
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2✔
1360
                switch ($xfOrientation) {
1361
                    case 0:
2✔
1362
                        $objStyle->getAlignment()->setTextRotation(0);
2✔
1363

1364
                        break;
2✔
1365
                    case 1:
1✔
1366
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
1✔
1367

1368
                        break;
1✔
1369
                    case 2:
×
1370
                        $objStyle->getAlignment()->setTextRotation(90);
×
1371

1372
                        break;
×
1373
                    case 3:
×
1374
                        $objStyle->getAlignment()->setTextRotation(-90);
×
1375

1376
                        break;
×
1377
                }
1378

1379
                // offset: 8; size: 4; cell border lines and background area
1380
                $borderAndBackground = self::getInt4d($recordData, 8);
2✔
1381

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

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

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

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

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

1397
                // offset: 12; size: 4; cell border lines
1398
                $borderLines = self::getInt4d($recordData, 12);
2✔
1399

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

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

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

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

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

1415
                // bit: 29-23; mask: 0x3F800000; right line color index
1416
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2✔
1417
            }
1418

1419
            // add cellStyleXf or cellXf and update mapping
1420
            if ($isCellStyleXf) {
136✔
1421
                // we only read one style XF record which is always the first
1422
                if ($this->xfIndex == 0) {
136✔
1423
                    $this->spreadsheet->addCellStyleXf($objStyle);
136✔
1424
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
136✔
1425
                }
1426
            } else {
1427
                // we read all cell XF records
1428
                $this->spreadsheet->addCellXf($objStyle);
136✔
1429
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
136✔
1430
            }
1431

1432
            // update XF index for when we read next record
1433
            ++$this->xfIndex;
136✔
1434
        }
1435
    }
1436

1437
    protected function readXfExt(): void
54✔
1438
    {
1439
        $length = self::getUInt2d($this->data, $this->pos + 2);
54✔
1440
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
54✔
1441

1442
        // move stream pointer to next record
1443
        $this->pos += 4 + $length;
54✔
1444

1445
        if (!$this->readDataOnly) {
54✔
1446
            // offset: 0; size: 2; 0x087D = repeated header
1447

1448
            // offset: 2; size: 2
1449

1450
            // offset: 4; size: 8; not used
1451

1452
            // offset: 12; size: 2; record version
1453

1454
            // offset: 14; size: 2; index to XF record which this record modifies
1455
            $ixfe = self::getUInt2d($recordData, 14);
52✔
1456

1457
            // offset: 16; size: 2; not used
1458

1459
            // offset: 18; size: 2; number of extension properties that follow
1460
            //$cexts = self::getUInt2d($recordData, 18);
1461

1462
            // start reading the actual extension data
1463
            $offset = 20;
52✔
1464
            while ($offset < $length) {
52✔
1465
                // extension type
1466
                $extType = self::getUInt2d($recordData, $offset);
52✔
1467

1468
                // extension length
1469
                $cb = self::getUInt2d($recordData, $offset + 2);
52✔
1470

1471
                // extension data
1472
                $extData = substr($recordData, $offset + 4, $cb);
52✔
1473

1474
                switch ($extType) {
1475
                    case 4:        // fill start color
52✔
1476
                        $xclfType = self::getUInt2d($extData, 0); // color type
52✔
1477
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
52✔
1478

1479
                        if ($xclfType == 2) {
52✔
1480
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1481

1482
                            // modify the relevant style property
1483
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1484
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
7✔
1485
                                $fill->getStartColor()->setRGB($rgb);
7✔
1486
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
7✔
1487
                            }
1488
                        }
1489

1490
                        break;
52✔
1491
                    case 5:        // fill end color
50✔
1492
                        $xclfType = self::getUInt2d($extData, 0); // color type
5✔
1493
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
5✔
1494

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

1498
                            // modify the relevant style property
1499
                            if (isset($this->mapCellXfIndex[$ixfe])) {
5✔
1500
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
5✔
1501
                                $fill->getEndColor()->setRGB($rgb);
5✔
1502
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
5✔
1503
                            }
1504
                        }
1505

1506
                        break;
5✔
1507
                    case 7:        // border color top
50✔
1508
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1509
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1510

1511
                        if ($xclfType == 2) {
50✔
1512
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1513

1514
                            // modify the relevant style property
1515
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1516
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
4✔
1517
                                $top->getColor()->setRGB($rgb);
4✔
1518
                                $top->colorIndex = null; // normal color index does not apply, discard
4✔
1519
                            }
1520
                        }
1521

1522
                        break;
50✔
1523
                    case 8:        // border color bottom
50✔
1524
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1525
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1526

1527
                        if ($xclfType == 2) {
50✔
1528
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1529

1530
                            // modify the relevant style property
1531
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1532
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
5✔
1533
                                $bottom->getColor()->setRGB($rgb);
5✔
1534
                                $bottom->colorIndex = null; // normal color index does not apply, discard
5✔
1535
                            }
1536
                        }
1537

1538
                        break;
50✔
1539
                    case 9:        // border color left
50✔
1540
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1541
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1542

1543
                        if ($xclfType == 2) {
50✔
1544
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1545

1546
                            // modify the relevant style property
1547
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1548
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
4✔
1549
                                $left->getColor()->setRGB($rgb);
4✔
1550
                                $left->colorIndex = null; // normal color index does not apply, discard
4✔
1551
                            }
1552
                        }
1553

1554
                        break;
50✔
1555
                    case 10:        // border color right
50✔
1556
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1557
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1558

1559
                        if ($xclfType == 2) {
50✔
1560
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1561

1562
                            // modify the relevant style property
1563
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1564
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
4✔
1565
                                $right->getColor()->setRGB($rgb);
4✔
1566
                                $right->colorIndex = null; // normal color index does not apply, discard
4✔
1567
                            }
1568
                        }
1569

1570
                        break;
50✔
1571
                    case 11:        // border color diagonal
50✔
1572
                        $xclfType = self::getUInt2d($extData, 0); // color type
×
1573
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
×
1574

1575
                        if ($xclfType == 2) {
×
1576
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
×
1577

1578
                            // modify the relevant style property
1579
                            if (isset($this->mapCellXfIndex[$ixfe])) {
×
1580
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
×
1581
                                $diagonal->getColor()->setRGB($rgb);
×
1582
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
×
1583
                            }
1584
                        }
1585

1586
                        break;
×
1587
                    case 13:    // font color
50✔
1588
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1589
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1590

1591
                        if ($xclfType == 2) {
50✔
1592
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1593

1594
                            // modify the relevant style property
1595
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1596
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
9✔
1597
                                $font->getColor()->setRGB($rgb);
9✔
1598
                                $font->colorIndex = null; // normal color index does not apply, discard
9✔
1599
                            }
1600
                        }
1601

1602
                        break;
50✔
1603
                }
1604

1605
                $offset += $cb;
52✔
1606
            }
1607
        }
1608
    }
1609

1610
    /**
1611
     * Read STYLE record.
1612
     */
1613
    protected function readStyle(): void
138✔
1614
    {
1615
        $length = self::getUInt2d($this->data, $this->pos + 2);
138✔
1616
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
138✔
1617

1618
        // move stream pointer to next record
1619
        $this->pos += 4 + $length;
138✔
1620

1621
        if (!$this->readDataOnly) {
138✔
1622
            // offset: 0; size: 2; index to XF record and flag for built-in style
1623
            $ixfe = self::getUInt2d($recordData, 0);
136✔
1624

1625
            // bit: 11-0; mask 0x0FFF; index to XF record
1626
            //$xfIndex = (0x0FFF & $ixfe) >> 0;
1627

1628
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
1629
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
136✔
1630

1631
            if ($isBuiltIn) {
136✔
1632
                // offset: 2; size: 1; identifier for built-in style
1633
                $builtInId = ord($recordData[2]);
136✔
1634

1635
                switch ($builtInId) {
1636
                    case 0x00:
136✔
1637
                        // currently, we are not using this for anything
1638
                        break;
136✔
1639
                    default:
1640
                        break;
60✔
1641
                }
1642
            }
1643
            // user-defined; not supported by PhpSpreadsheet
1644
        }
1645
    }
1646

1647
    /**
1648
     * Read PALETTE record.
1649
     */
1650
    protected function readPalette(): void
92✔
1651
    {
1652
        $length = self::getUInt2d($this->data, $this->pos + 2);
92✔
1653
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
92✔
1654

1655
        // move stream pointer to next record
1656
        $this->pos += 4 + $length;
92✔
1657

1658
        if (!$this->readDataOnly) {
92✔
1659
            // offset: 0; size: 2; number of following colors
1660
            $nm = self::getUInt2d($recordData, 0);
92✔
1661

1662
            // list of RGB colors
1663
            for ($i = 0; $i < $nm; ++$i) {
92✔
1664
                $rgb = substr($recordData, 2 + 4 * $i, 4);
92✔
1665
                $this->palette[] = self::readRGB($rgb);
92✔
1666
            }
1667
        }
1668
    }
1669

1670
    /**
1671
     * SHEET.
1672
     *
1673
     * This record is  located in the  Workbook Globals
1674
     * Substream  and represents a sheet inside the workbook.
1675
     * One SHEET record is written for each sheet. It stores the
1676
     * sheet name and a stream offset to the BOF record of the
1677
     * respective Sheet Substream within the Workbook Stream.
1678
     *
1679
     * --    "OpenOffice.org's Documentation of the Microsoft
1680
     *         Excel File Format"
1681
     */
1682
    protected function readSheet(): void
151✔
1683
    {
1684
        $length = self::getUInt2d($this->data, $this->pos + 2);
151✔
1685
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
151✔
1686

1687
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
1688
        // NOTE: not encrypted
1689
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
151✔
1690

1691
        // move stream pointer to next record
1692
        $this->pos += 4 + $length;
151✔
1693

1694
        // offset: 4; size: 1; sheet state
1695
        $sheetState = match (ord($recordData[4])) {
151✔
1696
            0x00 => Worksheet::SHEETSTATE_VISIBLE,
151✔
1697
            0x01 => Worksheet::SHEETSTATE_HIDDEN,
6✔
1698
            0x02 => Worksheet::SHEETSTATE_VERYHIDDEN,
2✔
1699
            default => Worksheet::SHEETSTATE_VISIBLE,
×
1700
        };
151✔
1701

1702
        // offset: 5; size: 1; sheet type
1703
        $sheetType = ord($recordData[5]);
151✔
1704

1705
        // offset: 6; size: var; sheet name
1706
        $rec_name = null;
151✔
1707
        if ($this->version == self::XLS_BIFF8) {
151✔
1708
            $string = self::readUnicodeStringShort(substr($recordData, 6));
144✔
1709
            $rec_name = $string['value'];
144✔
1710
        } elseif ($this->version == self::XLS_BIFF7) {
7✔
1711
            $string = $this->readByteStringShort(substr($recordData, 6));
7✔
1712
            $rec_name = $string['value'];
7✔
1713
        }
1714
        /** @var string $rec_name */
1715
        $this->sheets[] = [
151✔
1716
            'name' => $rec_name,
151✔
1717
            'offset' => $rec_offset,
151✔
1718
            'sheetState' => $sheetState,
151✔
1719
            'sheetType' => $sheetType,
151✔
1720
        ];
151✔
1721
    }
1722

1723
    /**
1724
     * Read EXTERNALBOOK record.
1725
     */
1726
    protected function readExternalBook(): void
106✔
1727
    {
1728
        $length = self::getUInt2d($this->data, $this->pos + 2);
106✔
1729
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
106✔
1730

1731
        // move stream pointer to next record
1732
        $this->pos += 4 + $length;
106✔
1733

1734
        // offset within record data
1735
        $offset = 0;
106✔
1736

1737
        // there are 4 types of records
1738
        if (strlen($recordData) > 4) {
106✔
1739
            // external reference
1740
            // offset: 0; size: 2; number of sheet names ($nm)
1741
            $nm = self::getUInt2d($recordData, 0);
×
1742
            $offset += 2;
×
1743

1744
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
1745
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
×
1746
            $offset += $encodedUrlString['size'];
×
1747

1748
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
1749
            $externalSheetNames = [];
×
1750
            for ($i = 0; $i < $nm; ++$i) {
×
1751
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
×
1752
                $externalSheetNames[] = $externalSheetNameString['value'];
×
1753
                $offset += $externalSheetNameString['size'];
×
1754
            }
1755

1756
            // store the record data
1757
            $this->externalBooks[] = [
×
1758
                'type' => 'external',
×
1759
                'encodedUrl' => $encodedUrlString['value'],
×
1760
                'externalSheetNames' => $externalSheetNames,
×
1761
            ];
×
1762
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
106✔
1763
            // internal reference
1764
            // offset: 0; size: 2; number of sheet in this document
1765
            // offset: 2; size: 2; 0x01 0x04
1766
            $this->externalBooks[] = [
106✔
1767
                'type' => 'internal',
106✔
1768
            ];
106✔
1769
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
1✔
1770
            // add-in function
1771
            // offset: 0; size: 2; 0x0001
1772
            $this->externalBooks[] = [
1✔
1773
                'type' => 'addInFunction',
1✔
1774
            ];
1✔
1775
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
×
1776
            // DDE links, OLE links
1777
            // offset: 0; size: 2; 0x0000
1778
            // offset: 2; size: var; encoded source document name
1779
            $this->externalBooks[] = [
×
1780
                'type' => 'DDEorOLE',
×
1781
            ];
×
1782
        }
1783
    }
1784

1785
    /**
1786
     * Read EXTERNNAME record.
1787
     */
1788
    protected function readExternName(): void
1✔
1789
    {
1790
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
1791
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
1792

1793
        // move stream pointer to next record
1794
        $this->pos += 4 + $length;
1✔
1795

1796
        // external sheet references provided for named cells
1797
        if ($this->version == self::XLS_BIFF8) {
1✔
1798
            // offset: 0; size: 2; options
1799
            //$options = self::getUInt2d($recordData, 0);
1800

1801
            // offset: 2; size: 2;
1802

1803
            // offset: 4; size: 2; not used
1804

1805
            // offset: 6; size: var
1806
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
1✔
1807

1808
            // offset: var; size: var; formula data
1809
            $offset = 6 + $nameString['size'];
1✔
1810
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
1✔
1811

1812
            $this->externalNames[] = [
1✔
1813
                'name' => $nameString['value'],
1✔
1814
                'formula' => $formula,
1✔
1815
            ];
1✔
1816
        }
1817
    }
1818

1819
    /**
1820
     * Read EXTERNSHEET record.
1821
     */
1822
    protected function readExternSheet(): void
107✔
1823
    {
1824
        $length = self::getUInt2d($this->data, $this->pos + 2);
107✔
1825
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
107✔
1826

1827
        // move stream pointer to next record
1828
        $this->pos += 4 + $length;
107✔
1829

1830
        // external sheet references provided for named cells
1831
        if ($this->version == self::XLS_BIFF8) {
107✔
1832
            // offset: 0; size: 2; number of following ref structures
1833
            $nm = self::getUInt2d($recordData, 0);
106✔
1834
            for ($i = 0; $i < $nm; ++$i) {
106✔
1835
                $this->ref[] = [
104✔
1836
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
1837
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
104✔
1838
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
1839
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
104✔
1840
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
1841
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
104✔
1842
                ];
104✔
1843
            }
1844
        }
1845
    }
1846

1847
    /**
1848
     * DEFINEDNAME.
1849
     *
1850
     * This record is part of a Link Table. It contains the name
1851
     * and the token array of an internal defined name. Token
1852
     * arrays of defined names contain tokens with aberrant
1853
     * token classes.
1854
     *
1855
     * --    "OpenOffice.org's Documentation of the Microsoft
1856
     *         Excel File Format"
1857
     */
1858
    protected function readDefinedName(): void
21✔
1859
    {
1860
        $length = self::getUInt2d($this->data, $this->pos + 2);
21✔
1861
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
21✔
1862

1863
        // move stream pointer to next record
1864
        $this->pos += 4 + $length;
21✔
1865

1866
        if ($this->version == self::XLS_BIFF8) {
21✔
1867
            // retrieves named cells
1868

1869
            // offset: 0; size: 2; option flags
1870
            $opts = self::getUInt2d($recordData, 0);
20✔
1871

1872
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
1873
            $isBuiltInName = (0x0020 & $opts) >> 5;
20✔
1874

1875
            // offset: 2; size: 1; keyboard shortcut
1876

1877
            // offset: 3; size: 1; length of the name (character count)
1878
            $nlen = ord($recordData[3]);
20✔
1879

1880
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
1881
            // note: there can also be additional data, this is not included in $flen
1882
            $flen = self::getUInt2d($recordData, 4);
20✔
1883

1884
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
1885
            $scope = self::getUInt2d($recordData, 8);
20✔
1886

1887
            // offset: 14; size: var; Name (Unicode string without length field)
1888
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
20✔
1889

1890
            // offset: var; size: $flen; formula data
1891
            $offset = 14 + $string['size'];
20✔
1892
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
20✔
1893

1894
            try {
1895
                $formula = $this->getFormulaFromStructure($formulaStructure);
20✔
1896
            } catch (PhpSpreadsheetException) {
1✔
1897
                $formula = '';
1✔
1898
                $isBuiltInName = 0;
1✔
1899
            }
1900

1901
            $this->definedname[] = [
20✔
1902
                'isBuiltInName' => $isBuiltInName,
20✔
1903
                'name' => $string['value'],
20✔
1904
                'formula' => $formula,
20✔
1905
                'scope' => $scope,
20✔
1906
            ];
20✔
1907
        }
1908
    }
1909

1910
    /**
1911
     * Read MSODRAWINGGROUP record.
1912
     */
1913
    protected function readMsoDrawingGroup(): void
21✔
1914
    {
1915
        //$length = self::getUInt2d($this->data, $this->pos + 2);
1916

1917
        // get spliced record data
1918
        $splicedRecordData = $this->getSplicedRecordData();
21✔
1919
        /** @var string */
1920
        $recordData = $splicedRecordData['recordData'];
21✔
1921

1922
        $this->drawingGroupData .= $recordData;
21✔
1923
    }
1924

1925
    /**
1926
     * SST - Shared String Table.
1927
     *
1928
     * This record contains a list of all strings used anywhere
1929
     * in the workbook. Each string occurs only once. The
1930
     * workbook uses indexes into the list to reference the
1931
     * strings.
1932
     *
1933
     * --    "OpenOffice.org's Documentation of the Microsoft
1934
     *         Excel File Format"
1935
     */
1936
    protected function readSst(): void
132✔
1937
    {
1938
        // offset within (spliced) record data
1939
        $pos = 0;
132✔
1940

1941
        // Limit global SST position, further control for bad SST Length in BIFF8 data
1942
        $limitposSST = 0;
132✔
1943

1944
        // get spliced record data
1945
        $splicedRecordData = $this->getSplicedRecordData();
132✔
1946

1947
        $recordData = $splicedRecordData['recordData'];
132✔
1948
        /** @var mixed[] */
1949
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
132✔
1950

1951
        // offset: 0; size: 4; total number of strings in the workbook
1952
        $pos += 4;
132✔
1953

1954
        // offset: 4; size: 4; number of following strings ($nm)
1955
        /** @var string $recordData */
1956
        $nm = self::getInt4d($recordData, 4);
132✔
1957
        $pos += 4;
132✔
1958

1959
        // look up limit position (last splice offset where pos fits)
1960
        $spliceCount = count($spliceOffsets);
132✔
1961
        for ($si = 0; $si < $spliceCount; ++$si) {
132✔
1962
            if ($pos <= $spliceOffsets[$si]) {
132✔
1963
                $limitposSST = $spliceOffsets[$si];
132✔
1964
            }
1965
        }
1966

1967
        // loop through the Unicode strings (16-bit length)
1968
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
132✔
1969
            // number of characters in the Unicode string
1970
            /** @var int $pos */
1971
            $numChars = self::getUInt2d($recordData, $pos);
80✔
1972
            /** @var int $pos */
1973
            $pos += 2;
80✔
1974

1975
            // option flags
1976
            /** @var string $recordData */
1977
            $optionFlags = ord($recordData[$pos]);
80✔
1978
            ++$pos;
80✔
1979

1980
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
1981
            $isCompressed = (($optionFlags & 0x01) == 0);
80✔
1982

1983
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
1984
            $hasAsian = (($optionFlags & 0x04) != 0);
80✔
1985

1986
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
1987
            $hasRichText = (($optionFlags & 0x08) != 0);
80✔
1988

1989
            $formattingRuns = 0;
80✔
1990
            if ($hasRichText) {
80✔
1991
                // number of Rich-Text formatting runs
1992
                $formattingRuns = self::getUInt2d($recordData, $pos);
8✔
1993
                $pos += 2;
8✔
1994
            }
1995

1996
            $extendedRunLength = 0;
80✔
1997
            if ($hasAsian) {
80✔
1998
                // size of Asian phonetic setting
1999
                $extendedRunLength = self::getInt4d($recordData, $pos);
×
2000
                $pos += 4;
×
2001
            }
2002

2003
            // expected byte length of character array if not split
2004
            $len = ($isCompressed) ? $numChars : $numChars * 2;
80✔
2005

2006
            // look up limit position - find the first splice offset at or beyond current pos
2007
            $limitpos = null;
80✔
2008
            for ($si = 0; $si < $spliceCount; ++$si) {
80✔
2009
                if ($pos <= $spliceOffsets[$si]) {
80✔
2010
                    $limitpos = $spliceOffsets[$si];
80✔
2011

2012
                    break;
80✔
2013
                }
2014
            }
2015

2016
            /** @var int $limitpos */
2017
            if ($pos + $len <= $limitpos) {
80✔
2018
                // character array is not split between records
2019

2020
                $retstr = substr($recordData, $pos, $len);
80✔
2021
                $pos += $len;
80✔
2022
            } else {
2023
                // character array is split between records
2024

2025
                // first part of character array
2026
                $retstr = substr($recordData, $pos, $limitpos - $pos);
1✔
2027

2028
                $bytesRead = $limitpos - $pos;
1✔
2029

2030
                // remaining characters in Unicode string
2031
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
1✔
2032

2033
                $pos = $limitpos;
1✔
2034

2035
                // keep reading the characters
2036
                while ($charsLeft > 0) {
1✔
2037
                    // look up next limit position, in case the string spans more than one continue record
2038
                    for ($si = 0; $si < $spliceCount; ++$si) {
1✔
2039
                        if ($pos < $spliceOffsets[$si]) {
1✔
2040
                            $limitpos = $spliceOffsets[$si];
1✔
2041

2042
                            break;
1✔
2043
                        }
2044
                    }
2045

2046
                    // repeated option flags
2047
                    // OpenOffice.org documentation 5.21
2048
                    /** @var int $pos */
2049
                    $option = ord($recordData[$pos]);
1✔
2050
                    ++$pos;
1✔
2051

2052
                    /** @var int $limitpos */
2053
                    if ($isCompressed && ($option == 0)) {
1✔
2054
                        // 1st fragment compressed
2055
                        // this fragment compressed
2056
                        /** @var int */
2057
                        $len = min($charsLeft, $limitpos - $pos);
×
2058
                        $retstr .= substr($recordData, $pos, $len);
×
2059
                        $charsLeft -= $len;
×
2060
                        $isCompressed = true;
×
2061
                    } elseif (!$isCompressed && ($option != 0)) {
1✔
2062
                        // 1st fragment uncompressed
2063
                        // this fragment uncompressed
2064
                        /** @var int */
2065
                        $len = min($charsLeft * 2, $limitpos - $pos);
1✔
2066
                        $retstr .= substr($recordData, $pos, $len);
1✔
2067
                        $charsLeft -= $len / 2;
1✔
2068
                        $isCompressed = false;
1✔
2069
                    } elseif (!$isCompressed && ($option == 0)) {
×
2070
                        // 1st fragment uncompressed
2071
                        // this fragment compressed
2072
                        $len = min($charsLeft, $limitpos - $pos);
×
2073
                        // Pad each byte with a null byte to expand to UTF-16LE
NEW
2074
                        $fragment = substr($recordData, $pos, $len);
×
NEW
2075
                        $retstr .= implode("\x00", str_split($fragment, 1)) . "\x00";
×
2076
                        $charsLeft -= $len;
×
UNCOV
2077
                        $isCompressed = false;
×
2078
                    } else {
2079
                        // 1st fragment compressed
2080
                        // this fragment uncompressed
2081
                        // Pad existing compressed string bytes with null bytes
NEW
2082
                        $retstr = implode("\x00", str_split($retstr, 1)) . "\x00";
×
2083
                        /** @var int */
2084
                        $len = min($charsLeft * 2, $limitpos - $pos);
×
2085
                        $retstr .= substr($recordData, $pos, $len);
×
2086
                        $charsLeft -= $len / 2;
×
UNCOV
2087
                        $isCompressed = false;
×
2088
                    }
2089

2090
                    $pos += $len;
1✔
2091
                }
2092
            }
2093

2094
            // convert to UTF-8
2095
            $retstr = self::encodeUTF16($retstr, $isCompressed);
80✔
2096

2097
            // read additional Rich-Text information, if any
2098
            $fmtRuns = [];
80✔
2099
            if ($hasRichText) {
80✔
2100
                // list of formatting runs
2101
                for ($j = 0; $j < $formattingRuns; ++$j) {
8✔
2102
                    // first formatted character; zero-based
2103
                    /** @var int $pos */
2104
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
8✔
2105

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

2109
                    $fmtRuns[] = [
8✔
2110
                        'charPos' => $charPos,
8✔
2111
                        'fontIndex' => $fontIndex,
8✔
2112
                    ];
8✔
2113
                }
2114
                $pos += 4 * $formattingRuns;
8✔
2115
            }
2116

2117
            // read additional Asian phonetics information, if any
2118
            if ($hasAsian) {
80✔
2119
                // For Asian phonetic settings, we skip the extended string data
UNCOV
2120
                $pos += $extendedRunLength;
×
2121
            }
2122

2123
            // store the shared sting
2124
            $this->sst[] = [
80✔
2125
                'value' => $retstr,
80✔
2126
                'fmtRuns' => $fmtRuns,
80✔
2127
            ];
80✔
2128
        }
2129

2130
        // getSplicedRecordData() takes care of moving current position in data stream
2131
    }
2132

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

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

2144
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
132✔
2145
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
2146
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
128✔
2147
            $this->phpSheet->setPrintGridlines($printGridlines);
128✔
2148
        }
2149
    }
2150

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

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

2162
        // offset: 0; size: 2; option flags
2163
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
2164
        $height = self::getUInt2d($recordData, 2);
67✔
2165
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
67✔
2166
    }
2167

2168
    /**
2169
     * Read SHEETPR record.
2170
     */
2171
    protected function readSheetPr(): void
134✔
2172
    {
2173
        $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
2174
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
2175

2176
        // move stream pointer to next record
2177
        $this->pos += 4 + $length;
134✔
2178

2179
        // offset: 0; size: 2
2180

2181
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
2182
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
134✔
2183
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
134✔
2184

2185
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
2186
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
134✔
2187
        $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight);
134✔
2188

2189
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
2190
        // this corresponds to radio button setting in page setup dialog in Excel
2191
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
134✔
2192
    }
2193

2194
    /**
2195
     * Read HORIZONTALPAGEBREAKS record.
2196
     */
2197
    protected function readHorizontalPageBreaks(): void
5✔
2198
    {
2199
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2200
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2201

2202
        // move stream pointer to next record
2203
        $this->pos += 4 + $length;
5✔
2204

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

2209
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2210
            for ($i = 0; $i < $nm; ++$i) {
5✔
2211
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2212
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2213
                //$cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2214

2215
                // not sure why two column indexes are necessary?
2216
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3✔
2217
            }
2218
        }
2219
    }
2220

2221
    /**
2222
     * Read VERTICALPAGEBREAKS record.
2223
     */
2224
    protected function readVerticalPageBreaks(): void
5✔
2225
    {
2226
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2227
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2228

2229
        // move stream pointer to next record
2230
        $this->pos += 4 + $length;
5✔
2231

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

2236
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2237
            for ($i = 0; $i < $nm; ++$i) {
5✔
2238
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2239
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2240
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2241

2242
                // not sure why two row indexes are necessary?
2243
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3✔
2244
            }
2245
        }
2246
    }
2247

2248
    /**
2249
     * Read HEADER record.
2250
     */
2251
    protected function readHeader(): void
132✔
2252
    {
2253
        $length = self::getUInt2d($this->data, $this->pos + 2);
132✔
2254
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
132✔
2255

2256
        // move stream pointer to next record
2257
        $this->pos += 4 + $length;
132✔
2258

2259
        if (!$this->readDataOnly) {
132✔
2260
            // offset: 0; size: var
2261
            // realized that $recordData can be empty even when record exists
2262
            if ($recordData) {
130✔
2263
                if ($this->version == self::XLS_BIFF8) {
82✔
2264
                    $string = self::readUnicodeStringLong($recordData);
81✔
2265
                } else {
2266
                    $string = $this->readByteStringShort($recordData);
1✔
2267
                }
2268

2269
                /** @var string[] $string */
2270
                $this->phpSheet
82✔
2271
                    ->getHeaderFooter()
82✔
2272
                    ->setOddHeader($string['value']);
82✔
2273
                $this->phpSheet
82✔
2274
                    ->getHeaderFooter()
82✔
2275
                    ->setEvenHeader($string['value']);
82✔
2276
            }
2277
        }
2278
    }
2279

2280
    /**
2281
     * Read FOOTER record.
2282
     */
2283
    protected function readFooter(): void
132✔
2284
    {
2285
        $length = self::getUInt2d($this->data, $this->pos + 2);
132✔
2286
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
132✔
2287

2288
        // move stream pointer to next record
2289
        $this->pos += 4 + $length;
132✔
2290

2291
        if (!$this->readDataOnly) {
132✔
2292
            // offset: 0; size: var
2293
            // realized that $recordData can be empty even when record exists
2294
            if ($recordData) {
130✔
2295
                if ($this->version == self::XLS_BIFF8) {
84✔
2296
                    $string = self::readUnicodeStringLong($recordData);
82✔
2297
                } else {
2298
                    $string = $this->readByteStringShort($recordData);
2✔
2299
                }
2300
                /** @var string */
2301
                $temp = $string['value'];
84✔
2302
                $this->phpSheet
84✔
2303
                    ->getHeaderFooter()
84✔
2304
                    ->setOddFooter($temp);
84✔
2305
                $this->phpSheet
84✔
2306
                    ->getHeaderFooter()
84✔
2307
                    ->setEvenFooter($temp);
84✔
2308
            }
2309
        }
2310
    }
2311

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

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

2323
        if (!$this->readDataOnly) {
132✔
2324
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
2325
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
130✔
2326

2327
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
130✔
2328
        }
2329
    }
2330

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

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

2342
        if (!$this->readDataOnly) {
132✔
2343
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
2344
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
130✔
2345

2346
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
130✔
2347
        }
2348
    }
2349

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

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

2361
        if (!$this->readDataOnly) {
125✔
2362
            // offset: 0; size: 8
2363
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
123✔
2364
        }
2365
    }
2366

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

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

2378
        if (!$this->readDataOnly) {
125✔
2379
            // offset: 0; size: 8
2380
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
123✔
2381
        }
2382
    }
2383

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

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

2395
        if (!$this->readDataOnly) {
125✔
2396
            // offset: 0; size: 8
2397
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
123✔
2398
        }
2399
    }
2400

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

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

2412
        if (!$this->readDataOnly) {
125✔
2413
            // offset: 0; size: 8
2414
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
123✔
2415
        }
2416
    }
2417

2418
    /**
2419
     * Read PAGESETUP record.
2420
     */
2421
    protected function readPageSetup(): void
134✔
2422
    {
2423
        $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
2424
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
2425

2426
        // move stream pointer to next record
2427
        $this->pos += 4 + $length;
134✔
2428

2429
        if (!$this->readDataOnly) {
134✔
2430
            // offset: 0; size: 2; paper size
2431
            $paperSize = self::getUInt2d($recordData, 0);
132✔
2432

2433
            // offset: 2; size: 2; scaling factor
2434
            $scale = self::getUInt2d($recordData, 2);
132✔
2435

2436
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
2437
            $fitToWidth = self::getUInt2d($recordData, 6);
132✔
2438

2439
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
2440
            $fitToHeight = self::getUInt2d($recordData, 8);
132✔
2441

2442
            // offset: 10; size: 2; option flags
2443

2444
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
2445
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
132✔
2446

2447
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
2448
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
132✔
2449

2450
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
2451
            // when this bit is set, do not use flags for those properties
2452
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
132✔
2453

2454
            if (!$isNotInit) {
132✔
2455
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
121✔
2456
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
121✔
2457
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
121✔
2458

2459
                $this->phpSheet->getPageSetup()->setScale($scale, false);
121✔
2460
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
121✔
2461
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
121✔
2462
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
121✔
2463
            }
2464

2465
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
2466
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
132✔
2467
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
132✔
2468

2469
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
2470
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
132✔
2471
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
132✔
2472
        }
2473
    }
2474

2475
    /**
2476
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
2477
     *   if this record is omitted, then it also means no sheet protection.
2478
     */
2479
    protected function readProtect(): void
7✔
2480
    {
2481
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
2482
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
2483

2484
        // move stream pointer to next record
2485
        $this->pos += 4 + $length;
7✔
2486

2487
        if ($this->readDataOnly) {
7✔
UNCOV
2488
            return;
×
2489
        }
2490

2491
        // offset: 0; size: 2;
2492

2493
        // bit 0, mask 0x01; 1 = sheet is protected
2494
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
7✔
2495
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
7✔
2496
    }
2497

2498
    /**
2499
     * SCENPROTECT.
2500
     */
UNCOV
2501
    protected function readScenProtect(): void
×
2502
    {
2503
        $length = self::getUInt2d($this->data, $this->pos + 2);
×
UNCOV
2504
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
×
2505

2506
        // move stream pointer to next record
UNCOV
2507
        $this->pos += 4 + $length;
×
2508

2509
        if ($this->readDataOnly) {
×
UNCOV
2510
            return;
×
2511
        }
2512

2513
        // offset: 0; size: 2;
2514

2515
        // bit: 0, mask 0x01; 1 = scenarios are protected
UNCOV
2516
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
×
2517

UNCOV
2518
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
×
2519
    }
2520

2521
    /**
2522
     * OBJECTPROTECT.
2523
     */
2524
    protected function readObjectProtect(): void
2✔
2525
    {
2526
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
2527
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
2528

2529
        // move stream pointer to next record
2530
        $this->pos += 4 + $length;
2✔
2531

2532
        if ($this->readDataOnly) {
2✔
UNCOV
2533
            return;
×
2534
        }
2535

2536
        // offset: 0; size: 2;
2537

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

2541
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
2✔
2542
    }
2543

2544
    /**
2545
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
2546
     */
2547
    protected function readPassword(): void
3✔
2548
    {
2549
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
2550
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
2551

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

2555
        if (!$this->readDataOnly) {
3✔
2556
            // offset: 0; size: 2; 16-bit hash value of password
2557
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3✔
2558
            $this->phpSheet->getProtection()->setPassword($password, true);
3✔
2559
        }
2560
    }
2561

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

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

2573
        // offset: 0; size: 2; default column width
2574
        $width = self::getUInt2d($recordData, 0);
133✔
2575
        if ($width != 8) {
133✔
2576
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
5✔
2577
        }
2578
    }
2579

2580
    /**
2581
     * Read COLINFO record.
2582
     */
2583
    protected function readColInfo(): void
119✔
2584
    {
2585
        $length = self::getUInt2d($this->data, $this->pos + 2);
119✔
2586
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
119✔
2587

2588
        // move stream pointer to next record
2589
        $this->pos += 4 + $length;
119✔
2590

2591
        if (!$this->readDataOnly) {
119✔
2592
            // offset: 0; size: 2; index to first column in range
2593
            $firstColumnIndex = self::getUInt2d($recordData, 0);
117✔
2594

2595
            // offset: 2; size: 2; index to last column in range
2596
            $lastColumnIndex = self::getUInt2d($recordData, 2);
117✔
2597

2598
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
2599
            $width = self::getUInt2d($recordData, 4);
117✔
2600

2601
            // offset: 6; size: 2; index to XF record for default column formatting
2602
            $xfIndex = self::getUInt2d($recordData, 6);
117✔
2603

2604
            // offset: 8; size: 2; option flags
2605
            // bit: 0; mask: 0x0001; 1= columns are hidden
2606
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
117✔
2607

2608
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
2609
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
117✔
2610

2611
            // bit: 12; mask: 0x1000; 1 = collapsed
2612
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
117✔
2613

2614
            // offset: 10; size: 2; not used
2615

2616
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
117✔
2617
                if ($lastColumnIndex == AddressRange::MAX_COLUMN_INT_XLS - 1 || $lastColumnIndex == AddressRange::MAX_COLUMN_INT) {
117✔
2618
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
1✔
2619

2620
                    break;
1✔
2621
                }
2622
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
117✔
2623
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
117✔
2624
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
117✔
2625
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
117✔
2626
                if (isset($this->mapCellXfIndex[$xfIndex])) {
117✔
2627
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
115✔
2628
                }
2629
            }
2630
        }
2631
    }
2632

2633
    /**
2634
     * ROW.
2635
     *
2636
     * This record contains the properties of a single row in a
2637
     * sheet. Rows and cells in a sheet are divided into blocks
2638
     * of 32 rows.
2639
     *
2640
     * --    "OpenOffice.org's Documentation of the Microsoft
2641
     *         Excel File Format"
2642
     */
2643
    protected function readRow(): void
75✔
2644
    {
2645
        $length = self::getUInt2d($this->data, $this->pos + 2);
75✔
2646
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
75✔
2647

2648
        // move stream pointer to next record
2649
        $this->pos += 4 + $length;
75✔
2650

2651
        if (!$this->readDataOnly) {
75✔
2652
            // offset: 0; size: 2; index of this row
2653
            $r = self::getUInt2d($recordData, 0);
73✔
2654

2655
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
2656

2657
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
2658

2659
            // offset: 6; size: 2;
2660

2661
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
2662
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
73✔
2663

2664
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
2665
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
73✔
2666

2667
            if (!$useDefaultHeight) {
73✔
2668
                if (
2669
                    $this->phpSheet->getDefaultRowDimension()->getRowHeight() > 0
71✔
2670
                ) {
2671
                    $this->phpSheet->getRowDimension($r + 1)
60✔
2672
                        ->setCustomFormat(true, ($height === 255) ? -1 : ($height / 20));
60✔
2673
                } else {
2674
                    $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
11✔
2675
                }
2676
            }
2677

2678
            // offset: 8; size: 2; not used
2679

2680
            // offset: 10; size: 2; not used in BIFF5-BIFF8
2681

2682
            // offset: 12; size: 4; option flags and default row formatting
2683

2684
            // bit: 2-0: mask: 0x00000007; outline level of the row
2685
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
73✔
2686
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
73✔
2687

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

2692
            // bit: 5; mask: 0x00000020; 1 = row is hidden
2693
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
73✔
2694
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
73✔
2695

2696
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
2697
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
73✔
2698

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

2702
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
73✔
2703
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
6✔
2704
            }
2705
        }
2706
    }
2707

2708
    /**
2709
     * Read RK record
2710
     * This record represents a cell that contains an RK value
2711
     * (encoded integer or floating-point value). If a
2712
     * floating-point value cannot be encoded to an RK value,
2713
     * a NUMBER record will be written. This record replaces the
2714
     * record INTEGER written in BIFF2.
2715
     *
2716
     * --    "OpenOffice.org's Documentation of the Microsoft
2717
     *         Excel File Format"
2718
     */
2719
    protected function readRk(): void
37✔
2720
    {
2721
        $length = self::getUInt2d($this->data, $this->pos + 2);
37✔
2722
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
37✔
2723

2724
        // move stream pointer to next record
2725
        $this->pos += 4 + $length;
37✔
2726

2727
        // offset: 0; size: 2; index to row
2728
        $row = self::getUInt2d($recordData, 0);
37✔
2729

2730
        // offset: 2; size: 2; index to column
2731
        $column = self::getUInt2d($recordData, 2);
37✔
2732
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
37✔
2733
        $cellCoordinate = $columnString . ($row + 1);
37✔
2734

2735
        // Read cell?
2736
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
37✔
2737
            // offset: 4; size: 2; index to XF record
2738
            $xfIndex = self::getUInt2d($recordData, 4);
37✔
2739

2740
            // offset: 6; size: 4; RK value
2741
            $rknum = self::getInt4d($recordData, 6);
37✔
2742
            $numValue = self::getIEEE754($rknum);
37✔
2743

2744
            $cell = $this->phpSheet->getCell($cellCoordinate);
37✔
2745
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
37✔
2746
                // add style information
2747
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
33✔
2748
            }
2749

2750
            // add cell
2751
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
37✔
2752
        }
2753
    }
2754

2755
    /**
2756
     * Read LABELSST record
2757
     * This record represents a cell that contains a string. It
2758
     * replaces the LABEL record and RSTRING record used in
2759
     * BIFF2-BIFF5.
2760
     *
2761
     * --    "OpenOffice.org's Documentation of the Microsoft
2762
     *         Excel File Format"
2763
     */
2764
    protected function readLabelSst(): void
77✔
2765
    {
2766
        $length = self::getUInt2d($this->data, $this->pos + 2);
77✔
2767
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
77✔
2768

2769
        // move stream pointer to next record
2770
        $this->pos += 4 + $length;
77✔
2771

2772
        // offset: 0; size: 2; index to row
2773
        $row = self::getUInt2d($recordData, 0);
77✔
2774

2775
        // offset: 2; size: 2; index to column
2776
        $column = self::getUInt2d($recordData, 2);
77✔
2777
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
77✔
2778
        $cellCoordinate = $columnString . ($row + 1);
77✔
2779

2780
        // Read cell?
2781
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
77✔
2782
            // offset: 4; size: 2; index to XF record
2783
            $xfIndex = self::getUInt2d($recordData, 4);
77✔
2784

2785
            // offset: 6; size: 4; index to SST record
2786
            $index = self::getInt4d($recordData, 6);
77✔
2787

2788
            // cache SST entry locally to avoid repeated array lookups
2789
            $sstValue = $this->sst[$index]['value'];
77✔
2790
            $fmtRuns = $this->sst[$index]['fmtRuns'];
77✔
2791

2792
            // add cell
2793
            if ($fmtRuns && !$this->readDataOnly) {
77✔
2794
                // then we should treat as rich text
2795
                $richText = new RichText();
6✔
2796
                $charPos = 0;
6✔
2797
                $sstCount = count($fmtRuns);
6✔
2798
                $sstValueLength = StringHelper::countCharacters($sstValue);
6✔
2799
                $lastFontIndex = count($this->objFonts) - 1;
6✔
2800
                for ($i = 0; $i <= $sstCount; ++$i) {
6✔
2801
                    /** @var mixed[][] $fmtRuns */
2802
                    if (isset($fmtRuns[$i])) {
6✔
2803
                        /** @var int[] */
2804
                        $temp = $fmtRuns[$i];
6✔
2805
                        $temp = $temp['charPos'];
6✔
2806
                        /** @var int $charPos */
2807
                        $text = StringHelper::substring($sstValue, $charPos, $temp - $charPos);
6✔
2808
                        $charPos = $temp;
6✔
2809
                    } else {
2810
                        $text = StringHelper::substring($sstValue, $charPos, $sstValueLength);
6✔
2811
                    }
2812

2813
                    if (StringHelper::countCharacters($text) > 0) {
6✔
2814
                        if ($i == 0) { // first text run, no style
6✔
2815
                            $richText->createText($text);
3✔
2816
                        } else {
2817
                            $textRun = $richText->createTextRun($text);
6✔
2818
                            /** @var int[][] $fmtRuns */
2819
                            if (isset($fmtRuns[$i - 1])) {
6✔
2820
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
6✔
2821
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
5✔
2822
                                } else {
2823
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
2824
                                    // check the OpenOffice documentation of the FONT record
2825
                                    /** @var int */
2826
                                    $temp = $fmtRuns[$i - 1]['fontIndex'];
4✔
2827
                                    $fontIndex = $temp - 1;
4✔
2828
                                }
2829
                                if ($fontIndex > $lastFontIndex) {
6✔
2830
                                    $fontIndex = $lastFontIndex;
1✔
2831
                                }
2832
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
6✔
2833
                            }
2834
                        }
2835
                    }
2836
                }
2837
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
6✔
2838
                    $cell = $this->phpSheet->getCell($cellCoordinate);
6✔
2839
                    if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
6✔
2840
                        $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
6✔
2841
                    }
2842
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
6✔
2843
                }
2844
            } else {
2845
                if ($this->readEmptyCells || trim($sstValue) !== '') {
76✔
2846
                    $cell = $this->phpSheet->getCell($cellCoordinate);
76✔
2847
                    if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
76✔
2848
                        $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
74✔
2849
                    }
2850
                    $cell->setValueExplicit($sstValue, DataType::TYPE_STRING);
76✔
2851
                }
2852
            }
2853
        }
2854
    }
2855

2856
    /**
2857
     * Read MULRK record
2858
     * This record represents a cell range containing RK value
2859
     * cells. All cells are located in the same row.
2860
     *
2861
     * --    "OpenOffice.org's Documentation of the Microsoft
2862
     *         Excel File Format"
2863
     */
2864
    protected function readMulRk(): void
22✔
2865
    {
2866
        $length = self::getUInt2d($this->data, $this->pos + 2);
22✔
2867
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
22✔
2868

2869
        // move stream pointer to next record
2870
        $this->pos += 4 + $length;
22✔
2871

2872
        // offset: 0; size: 2; index to row
2873
        $row = self::getUInt2d($recordData, 0);
22✔
2874

2875
        // offset: 2; size: 2; index to first column
2876
        $colFirst = self::getUInt2d($recordData, 2);
22✔
2877

2878
        // offset: var; size: 2; index to last column
2879
        $colLast = self::getUInt2d($recordData, $length - 2);
22✔
2880
        $columns = $colLast - $colFirst + 1;
22✔
2881

2882
        // offset within record data
2883
        $offset = 4;
22✔
2884

2885
        $rowIndex = $row + 1;
22✔
2886
        for ($i = 1; $i <= $columns; ++$i) {
22✔
2887
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
22✔
2888

2889
            // Read cell?
2890
            if ($this->cachedReadFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
22✔
2891
                // offset: var; size: 2; index to XF record
2892
                $xfIndex = self::getUInt2d($recordData, $offset);
22✔
2893

2894
                // offset: var; size: 4; RK value
2895
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
22✔
2896
                $cell = $this->phpSheet->getCell($columnString . $rowIndex);
22✔
2897
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
22✔
2898
                    // add style
2899
                    $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
20✔
2900
                }
2901

2902
                // add cell value
2903
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
22✔
2904
            }
2905

2906
            $offset += 6;
22✔
2907
        }
2908
    }
2909

2910
    /**
2911
     * Read NUMBER record
2912
     * This record represents a cell that contains a
2913
     * floating-point value.
2914
     *
2915
     * --    "OpenOffice.org's Documentation of the Microsoft
2916
     *         Excel File Format"
2917
     */
2918
    protected function readNumber(): void
60✔
2919
    {
2920
        $length = self::getUInt2d($this->data, $this->pos + 2);
60✔
2921
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
60✔
2922

2923
        // move stream pointer to next record
2924
        $this->pos += 4 + $length;
60✔
2925

2926
        // offset: 0; size: 2; index to row
2927
        $row = self::getUInt2d($recordData, 0);
60✔
2928

2929
        // offset: 2; size 2; index to column
2930
        $column = self::getUInt2d($recordData, 2);
60✔
2931
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
60✔
2932
        $cellCoordinate = $columnString . ($row + 1);
60✔
2933

2934
        // Read cell?
2935
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
60✔
2936
            // offset 4; size: 2; index to XF record
2937
            $xfIndex = self::getUInt2d($recordData, 4);
60✔
2938

2939
            $numValue = self::extractNumber(substr($recordData, 6, 8));
60✔
2940

2941
            $cell = $this->phpSheet->getCell($cellCoordinate);
60✔
2942
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
60✔
2943
                // add cell style
2944
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
58✔
2945
            }
2946

2947
            // add cell value
2948
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
60✔
2949
        }
2950
    }
2951

2952
    /**
2953
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
2954
     * This record contains the token array and the result of a
2955
     * formula cell.
2956
     *
2957
     * --    "OpenOffice.org's Documentation of the Microsoft
2958
     *         Excel File Format"
2959
     */
2960
    protected function readFormula(): void
44✔
2961
    {
2962
        $length = self::getUInt2d($this->data, $this->pos + 2);
44✔
2963
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
44✔
2964

2965
        // move stream pointer to next record
2966
        $this->pos += 4 + $length;
44✔
2967

2968
        // offset: 0; size: 2; row index
2969
        $row = self::getUInt2d($recordData, 0);
44✔
2970

2971
        // offset: 2; size: 2; col index
2972
        $column = self::getUInt2d($recordData, 2);
44✔
2973
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
44✔
2974
        $cellCoordinate = $columnString . ($row + 1);
44✔
2975

2976
        // offset: 20: size: variable; formula structure
2977
        $formulaStructure = substr($recordData, 20);
44✔
2978

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

2982
        // bit: 0; mask: 0x0001; 1 = recalculate always
2983
        // bit: 1; mask: 0x0002; 1 = calculate on open
2984
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
2985
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
44✔
2986

2987
        // WARNING:
2988
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
2989
        // the formula data may be ordinary formula data, therefore we need to check
2990
        // explicitly for the tExp token (0x01)
2991
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
44✔
2992

2993
        if ($isPartOfSharedFormula) {
44✔
2994
            // part of shared formula which means there will be a formula with a tExp token and nothing else
2995
            // get the base cell, grab tExp token
2996
            $baseRow = self::getUInt2d($formulaStructure, 3);
1✔
2997
            $baseCol = self::getUInt2d($formulaStructure, 5);
1✔
2998
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
1✔
2999
        }
3000

3001
        // Read cell?
3002
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
44✔
3003
            if ($isPartOfSharedFormula) {
44✔
3004
                // formula is added to this cell after the sheet has been read
3005
                $this->sharedFormulaParts[$cellCoordinate] = $this->baseCell;
1✔
3006
            }
3007

3008
            // offset: 16: size: 4; not used
3009

3010
            // offset: 4; size: 2; XF index
3011
            $xfIndex = self::getUInt2d($recordData, 4);
44✔
3012

3013
            // offset: 6; size: 8; result of the formula
3014
            $resultType = ord($recordData[6]);
44✔
3015
            $isSpecialResult = (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255);
44✔
3016
            if (($resultType == 0) && $isSpecialResult) {
44✔
3017
                // String formula. Result follows in appended STRING record
3018
                $dataType = DataType::TYPE_STRING;
11✔
3019

3020
                // read possible SHAREDFMLA record
3021
                $code = self::getUInt2d($this->data, $this->pos);
11✔
3022
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
11✔
UNCOV
3023
                    $this->readSharedFmla();
×
3024
                }
3025

3026
                // read STRING record
3027
                $value = $this->readString();
11✔
3028
            } elseif (($resultType == 1) && $isSpecialResult) {
38✔
3029
                // Boolean formula. Result is in +2; 0=false, 1=true
3030
                $dataType = DataType::TYPE_BOOL;
3✔
3031
                $value = (bool) ord($recordData[8]);
3✔
3032
            } elseif (($resultType == 2) && $isSpecialResult) {
36✔
3033
                // Error formula. Error code is in +2
3034
                $dataType = DataType::TYPE_ERROR;
11✔
3035
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
11✔
3036
            } elseif (($resultType == 3) && $isSpecialResult) {
36✔
3037
                // Formula result is a null string
3038
                $dataType = DataType::TYPE_NULL;
2✔
3039
                $value = '';
2✔
3040
            } else {
3041
                // forumla result is a number, first 14 bytes like _NUMBER record
3042
                $dataType = DataType::TYPE_NUMERIC;
36✔
3043
                $value = self::extractNumber(substr($recordData, 6, 8));
36✔
3044
            }
3045

3046
            $cell = $this->phpSheet->getCell($cellCoordinate);
44✔
3047
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
44✔
3048
                // add cell style
3049
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
42✔
3050
            }
3051

3052
            // store the formula
3053
            if (!$isPartOfSharedFormula) {
44✔
3054
                // not part of shared formula
3055
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
3056
                try {
3057
                    if ($this->version != self::XLS_BIFF8) {
44✔
3058
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
1✔
3059
                    }
3060
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
43✔
3061
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
43✔
3062
                } catch (PhpSpreadsheetException) {
2✔
3063
                    $cell->setValueExplicit($value, $dataType);
2✔
3064
                }
3065
            } else {
3066
                if ($this->version == self::XLS_BIFF8) {
1✔
3067
                    // do nothing at this point, formula id added later in the code
3068
                } else {
UNCOV
3069
                    $cell->setValueExplicit($value, $dataType);
×
3070
                }
3071
            }
3072

3073
            // store the cached calculated value
3074
            $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC);
44✔
3075
        }
3076
    }
3077

3078
    /**
3079
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
3080
     * which usually contains relative references.
3081
     * These will be used to construct the formula in each shared formula part after the sheet is read.
3082
     */
3083
    protected function readSharedFmla(): void
1✔
3084
    {
3085
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
3086
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
3087

3088
        // move stream pointer to next record
3089
        $this->pos += 4 + $length;
1✔
3090

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

3095
        // offset: 6, size: 1; not used
3096

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

3100
        // offset: 8, size: var; Binary token array of the shared formula
3101
        $formula = substr($recordData, 8);
1✔
3102

3103
        // at this point we only store the shared formula for later use
3104
        $this->sharedFormulas[$this->baseCell] = $formula;
1✔
3105
    }
3106

3107
    /**
3108
     * Read a STRING record from current stream position and advance the stream pointer to next record
3109
     * This record is used for storing result from FORMULA record when it is a string, and
3110
     * it occurs directly after the FORMULA record.
3111
     *
3112
     * @return string The string contents as UTF-8
3113
     */
3114
    protected function readString(): string
11✔
3115
    {
3116
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3117
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3118

3119
        // move stream pointer to next record
3120
        $this->pos += 4 + $length;
11✔
3121

3122
        if ($this->version == self::XLS_BIFF8) {
11✔
3123
            $string = self::readUnicodeStringLong($recordData);
11✔
3124
            $value = $string['value'];
11✔
3125
        } else {
3126
            $string = $this->readByteStringLong($recordData);
×
UNCOV
3127
            $value = $string['value'];
×
3128
        }
3129
        /** @var string $value */
3130

3131
        return $value;
11✔
3132
    }
3133

3134
    /**
3135
     * Read BOOLERR record
3136
     * This record represents a Boolean value or error value
3137
     * cell.
3138
     *
3139
     * --    "OpenOffice.org's Documentation of the Microsoft
3140
     *         Excel File Format"
3141
     */
3142
    protected function readBoolErr(): void
11✔
3143
    {
3144
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3145
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3146

3147
        // move stream pointer to next record
3148
        $this->pos += 4 + $length;
11✔
3149

3150
        // offset: 0; size: 2; row index
3151
        $row = self::getUInt2d($recordData, 0);
11✔
3152

3153
        // offset: 2; size: 2; column index
3154
        $column = self::getUInt2d($recordData, 2);
11✔
3155
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
11✔
3156
        $cellCoordinate = $columnString . ($row + 1);
11✔
3157

3158
        // Read cell?
3159
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
11✔
3160
            // offset: 4; size: 2; index to XF record
3161
            $xfIndex = self::getUInt2d($recordData, 4);
11✔
3162

3163
            // offset: 6; size: 1; the boolean value or error value
3164
            $boolErr = ord($recordData[6]);
11✔
3165

3166
            // offset: 7; size: 1; 0=boolean; 1=error
3167
            $isError = ord($recordData[7]);
11✔
3168

3169
            $cell = $this->phpSheet->getCell($cellCoordinate);
11✔
3170
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
11✔
3171
                // add cell style
3172
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
9✔
3173
            }
3174
            switch ($isError) {
3175
                case 0: // boolean
11✔
3176
                    $value = (bool) $boolErr;
11✔
3177

3178
                    // add cell value
3179
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
11✔
3180

3181
                    break;
11✔
3182
                case 1: // error type
×
UNCOV
3183
                    $value = Xls\ErrorCode::lookup($boolErr);
×
3184

3185
                    // add cell value
UNCOV
3186
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
×
3187

UNCOV
3188
                    break;
×
3189
            }
3190
        }
3191
    }
3192

3193
    /**
3194
     * Read MULBLANK record
3195
     * This record represents a cell range of empty cells. All
3196
     * cells are located in the same row.
3197
     *
3198
     * --    "OpenOffice.org's Documentation of the Microsoft
3199
     *         Excel File Format"
3200
     */
3201
    protected function readMulBlank(): void
26✔
3202
    {
3203
        $length = self::getUInt2d($this->data, $this->pos + 2);
26✔
3204
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
26✔
3205

3206
        // move stream pointer to next record
3207
        $this->pos += 4 + $length;
26✔
3208

3209
        // offset: 0; size: 2; index to row
3210
        $row = self::getUInt2d($recordData, 0);
26✔
3211

3212
        // offset: 2; size: 2; index to first column
3213
        $fc = self::getUInt2d($recordData, 2);
26✔
3214

3215
        // offset: 4; size: 2 x nc; list of indexes to XF records
3216
        // add style information
3217
        if (!$this->readDataOnly && $this->readEmptyCells) {
26✔
3218
            $rowIndex = $row + 1;
24✔
3219
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
24✔
3220
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
24✔
3221

3222
                // Read cell?
3223
                if ($this->cachedReadFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
24✔
3224
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
24✔
3225
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
24✔
3226
                        $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
24✔
3227
                    }
3228
                }
3229
            }
3230
        }
3231

3232
        // offset: 6; size 2; index to last column (not needed)
3233
    }
3234

3235
    /**
3236
     * Read LABEL record
3237
     * This record represents a cell that contains a string. In
3238
     * BIFF8 it is usually replaced by the LABELSST record.
3239
     * Excel still uses this record, if it copies unformatted
3240
     * text cells to the clipboard.
3241
     *
3242
     * --    "OpenOffice.org's Documentation of the Microsoft
3243
     *         Excel File Format"
3244
     */
3245
    protected function readLabel(): void
4✔
3246
    {
3247
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
3248
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
3249

3250
        // move stream pointer to next record
3251
        $this->pos += 4 + $length;
4✔
3252

3253
        // offset: 0; size: 2; index to row
3254
        $row = self::getUInt2d($recordData, 0);
4✔
3255

3256
        // offset: 2; size: 2; index to column
3257
        $column = self::getUInt2d($recordData, 2);
4✔
3258
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4✔
3259
        $cellCoordinate = $columnString . ($row + 1);
4✔
3260

3261
        // Read cell?
3262
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
4✔
3263
            // offset: 4; size: 2; XF index
3264
            $xfIndex = self::getUInt2d($recordData, 4);
4✔
3265

3266
            // add cell value
3267
            // todo: what if string is very long? continue record
3268
            if ($this->version == self::XLS_BIFF8) {
4✔
3269
                $string = self::readUnicodeStringLong(substr($recordData, 6));
2✔
3270
                $value = $string['value'];
2✔
3271
            } else {
3272
                $string = $this->readByteStringLong(substr($recordData, 6));
2✔
3273
                $value = $string['value'];
2✔
3274
            }
3275
            /** @var string $value */
3276
            if ($this->readEmptyCells || trim($value) !== '') {
4✔
3277
                $cell = $this->phpSheet->getCell($cellCoordinate);
4✔
3278
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4✔
3279
                    // add cell style
3280
                    $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
4✔
3281
                }
3282
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4✔
3283
            }
3284
        }
3285
    }
3286

3287
    /**
3288
     * Read BLANK record.
3289
     */
3290
    protected function readBlank(): void
26✔
3291
    {
3292
        $length = self::getUInt2d($this->data, $this->pos + 2);
26✔
3293
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
26✔
3294

3295
        // move stream pointer to next record
3296
        $this->pos += 4 + $length;
26✔
3297

3298
        // offset: 0; size: 2; row index
3299
        $row = self::getUInt2d($recordData, 0);
26✔
3300

3301
        // offset: 2; size: 2; col index
3302
        $col = self::getUInt2d($recordData, 2);
26✔
3303
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
26✔
3304

3305
        $rowIndex = $row + 1;
26✔
3306

3307
        // Read cell?
3308
        if ($this->cachedReadFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
26✔
3309
            // offset: 4; size: 2; XF index
3310
            $xfIndex = self::getUInt2d($recordData, 4);
26✔
3311

3312
            // add style information
3313
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
26✔
3314
                $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
26✔
3315
            }
3316
        }
3317
    }
3318

3319
    /**
3320
     * Read MSODRAWING record.
3321
     */
3322
    protected function readMsoDrawing(): void
18✔
3323
    {
3324
        //$length = self::getUInt2d($this->data, $this->pos + 2);
3325

3326
        // get spliced record data
3327
        $splicedRecordData = $this->getSplicedRecordData();
18✔
3328
        $recordData = $splicedRecordData['recordData'];
18✔
3329

3330
        $this->drawingData .= StringHelper::convertToString($recordData);
18✔
3331
    }
3332

3333
    /**
3334
     * Read OBJ record.
3335
     */
3336
    protected function readObj(): void
14✔
3337
    {
3338
        $length = self::getUInt2d($this->data, $this->pos + 2);
14✔
3339
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
14✔
3340

3341
        // move stream pointer to next record
3342
        $this->pos += 4 + $length;
14✔
3343

3344
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
14✔
3345
            return;
1✔
3346
        }
3347

3348
        // recordData consists of an array of subrecords looking like this:
3349
        //    ft: 2 bytes; ftCmo type (0x15)
3350
        //    cb: 2 bytes; size in bytes of ftCmo data
3351
        //    ot: 2 bytes; Object Type
3352
        //    id: 2 bytes; Object id number
3353
        //    grbit: 2 bytes; Option Flags
3354
        //    data: var; subrecord data
3355

3356
        // for now, we are just interested in the second subrecord containing the object type
3357
        $ftCmoType = self::getUInt2d($recordData, 0);
13✔
3358
        $cbCmoSize = self::getUInt2d($recordData, 2);
13✔
3359
        $otObjType = self::getUInt2d($recordData, 4);
13✔
3360
        $idObjID = self::getUInt2d($recordData, 6);
13✔
3361
        $grbitOpts = self::getUInt2d($recordData, 6);
13✔
3362

3363
        $this->objs[] = [
13✔
3364
            'ftCmoType' => $ftCmoType,
13✔
3365
            'cbCmoSize' => $cbCmoSize,
13✔
3366
            'otObjType' => $otObjType,
13✔
3367
            'idObjID' => $idObjID,
13✔
3368
            'grbitOpts' => $grbitOpts,
13✔
3369
        ];
13✔
3370
        $this->textObjRef = $idObjID;
13✔
3371
    }
3372

3373
    /**
3374
     * Read WINDOW2 record.
3375
     */
3376
    protected function readWindow2(): void
135✔
3377
    {
3378
        $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
3379
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
3380

3381
        // move stream pointer to next record
3382
        $this->pos += 4 + $length;
135✔
3383

3384
        // offset: 0; size: 2; option flags
3385
        $options = self::getUInt2d($recordData, 0);
135✔
3386

3387
        // offset: 2; size: 2; index to first visible row
3388
        //$firstVisibleRow = self::getUInt2d($recordData, 2);
3389

3390
        // offset: 4; size: 2; index to first visible colum
3391
        //$firstVisibleColumn = self::getUInt2d($recordData, 4);
3392
        $zoomscaleInPageBreakPreview = 0;
135✔
3393
        $zoomscaleInNormalView = 0;
135✔
3394
        if ($this->version === self::XLS_BIFF8) {
135✔
3395
            // offset:  8; size: 2; not used
3396
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
3397
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
3398
            // offset: 14; size: 4; not used
3399
            if (!isset($recordData[10])) {
133✔
UNCOV
3400
                $zoomscaleInPageBreakPreview = 0;
×
3401
            } else {
3402
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
133✔
3403
            }
3404

3405
            if ($zoomscaleInPageBreakPreview === 0) {
133✔
3406
                $zoomscaleInPageBreakPreview = 60;
129✔
3407
            }
3408

3409
            if (!isset($recordData[12])) {
133✔
UNCOV
3410
                $zoomscaleInNormalView = 0;
×
3411
            } else {
3412
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
133✔
3413
            }
3414

3415
            if ($zoomscaleInNormalView === 0) {
133✔
3416
                $zoomscaleInNormalView = 100;
54✔
3417
            }
3418
        }
3419

3420
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
3421
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
135✔
3422
        $this->phpSheet->setShowGridlines($showGridlines);
135✔
3423

3424
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
3425
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
135✔
3426
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
135✔
3427

3428
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
3429
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
135✔
3430

3431
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
3432
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
135✔
3433

3434
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
3435
        $isActive = (bool) ((0x0400 & $options) >> 10);
135✔
3436
        if ($isActive) {
135✔
3437
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
131✔
3438
            $this->activeSheetSet = true;
131✔
3439
        }
3440

3441
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
3442
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
135✔
3443

3444
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
3445

3446
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
135✔
3447
            //NOTE: this setting is inferior to page layout view(Excel2007-)
3448
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
135✔
3449
            $this->phpSheet->getSheetView()->setView($view);
135✔
3450
            if ($this->version === self::XLS_BIFF8) {
135✔
3451
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
133✔
3452
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
133✔
3453
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
133✔
3454
            }
3455
        }
3456
    }
3457

3458
    /**
3459
     * Read PLV Record(Created by Excel2007 or upper).
3460
     */
3461
    protected function readPageLayoutView(): void
120✔
3462
    {
3463
        $length = self::getUInt2d($this->data, $this->pos + 2);
120✔
3464
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
120✔
3465

3466
        // move stream pointer to next record
3467
        $this->pos += 4 + $length;
120✔
3468

3469
        // offset: 0; size: 2; rt
3470
        //->ignore
3471
        //$rt = self::getUInt2d($recordData, 0);
3472
        // offset: 2; size: 2; grbitfr
3473
        //->ignore
3474
        //$grbitFrt = self::getUInt2d($recordData, 2);
3475
        // offset: 4; size: 8; reserved
3476
        //->ignore
3477

3478
        // offset: 12; size 2; zoom scale
3479
        $wScalePLV = self::getUInt2d($recordData, 12);
120✔
3480
        // offset: 14; size 2; grbit
3481
        $grbit = self::getUInt2d($recordData, 14);
120✔
3482

3483
        // decomprise grbit
3484
        $fPageLayoutView = $grbit & 0x01;
120✔
3485
        //$fRulerVisible = ($grbit >> 1) & 0x01; //no support
3486
        //$fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
3487

3488
        if ($fPageLayoutView === 1) {
120✔
3489
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
×
UNCOV
3490
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
×
3491
        }
3492
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
3493
    }
3494

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

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

3506
        // offset: 0; size: 2; numerator of the view magnification
3507
        $numerator = self::getUInt2d($recordData, 0);
6✔
3508

3509
        // offset: 2; size: 2; numerator of the view magnification
3510
        $denumerator = self::getUInt2d($recordData, 2);
6✔
3511

3512
        // set the zoom scale (in percent)
3513
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
6✔
3514
    }
3515

3516
    /**
3517
     * Read PANE record.
3518
     */
3519
    protected function readPane(): void
8✔
3520
    {
3521
        $length = self::getUInt2d($this->data, $this->pos + 2);
8✔
3522
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
8✔
3523

3524
        // move stream pointer to next record
3525
        $this->pos += 4 + $length;
8✔
3526

3527
        if (!$this->readDataOnly) {
8✔
3528
            // offset: 0; size: 2; position of vertical split
3529
            $px = self::getUInt2d($recordData, 0);
8✔
3530

3531
            // offset: 2; size: 2; position of horizontal split
3532
            $py = self::getUInt2d($recordData, 2);
8✔
3533

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

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

3540
            if ($this->frozen) {
8✔
3541
                // frozen panes
3542
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
8✔
3543
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
8✔
3544
                $this->phpSheet->freezePane($cell, $topLeftCell);
8✔
3545
            }
3546
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
3547
        }
3548
    }
3549

3550
    private const REGEX_WHOLE_COLUMN = '/^([A-Z]+1\:[A-Z]+)'
3551
        . '(' . AddressRange::MAX_ROW_XLS_OLD . '|' . AddressRange::MAX_ROW_XLS . ')'
3552
        . '$/';
3553
    private const REGEX_WHOLE_COLUMN_REPLACE = '${1}' . AddressRange::MAX_ROW;
3554
    private const REGEX_WHOLE_ROW = '/^(A\d+\:)'
3555
        . AddressRange::MAX_COLUMN_XLS
3556
        . '(\d+)$/';
3557
    private const REGEX_WHOLE_ROW_REPLACE = '${1}'
3558
        . AddressRange::MAX_COLUMN
3559
        . '${2}';
3560

3561
    /**
3562
     * Read SELECTION record. There is one such record for each pane in the sheet.
3563
     */
3564
    protected function readSelection(): string
132✔
3565
    {
3566
        $length = self::getUInt2d($this->data, $this->pos + 2);
132✔
3567
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
132✔
3568
        $selectedCells = '';
132✔
3569

3570
        // move stream pointer to next record
3571
        $this->pos += 4 + $length;
132✔
3572

3573
        if (!$this->readDataOnly) {
132✔
3574
            // offset: 0; size: 1; pane identifier
3575
            //$paneId = ord($recordData[0]);
3576

3577
            // offset: 1; size: 2; index to row of the active cell
3578
            //$r = self::getUInt2d($recordData, 1);
3579

3580
            // offset: 3; size: 2; index to column of the active cell
3581
            //$c = self::getUInt2d($recordData, 3);
3582

3583
            // offset: 5; size: 2; index into the following cell range list to the
3584
            //  entry that contains the active cell
3585
            //$index = self::getUInt2d($recordData, 5);
3586

3587
            // offset: 7; size: var; cell range address list containing all selected cell ranges
3588
            $data = substr($recordData, 7);
130✔
3589
            $cellRangeAddressList = Xls\Biff5::readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
130✔
3590

3591
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
130✔
3592

3593
            // first row '1' + last row '16384' or '65536' indicates that full column is selected (apparently also in BIFF8!)
3594
            if (Preg::isMatch(self::REGEX_WHOLE_COLUMN, $selectedCells)) {
130✔
3595
                $selectedCells = Preg::replace(self::REGEX_WHOLE_COLUMN, self::REGEX_WHOLE_COLUMN_REPLACE, $selectedCells);
1✔
3596
            }
3597

3598
            // first column 'A' + last column 'IV' indicates that full row is selected
3599
            if (Preg::isMatch(self::REGEX_WHOLE_ROW, $selectedCells)) {
130✔
3600
                $selectedCells = Preg::replace(self::REGEX_WHOLE_ROW, self::REGEX_WHOLE_ROW_REPLACE, $selectedCells);
3✔
3601
            }
3602

3603
            $this->phpSheet->setSelectedCells($selectedCells);
130✔
3604
        }
3605

3606
        return $selectedCells;
132✔
3607
    }
3608

3609
    private function includeCellRangeFiltered(string $cellRangeAddress): bool
17✔
3610
    {
3611
        $includeCellRange = false;
17✔
3612
        $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
17✔
3613
        StringHelper::stringIncrement($rangeBoundaries[1][0]);
17✔
3614
        for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
17✔
3615
            for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) {
17✔
3616
                if ($this->cachedReadFilter->readCell($column, $row, $this->phpSheetTitle)) {
17✔
3617
                    $includeCellRange = true;
17✔
3618

3619
                    break 2;
17✔
3620
                }
3621
            }
3622
        }
3623

3624
        return $includeCellRange;
17✔
3625
    }
3626

3627
    /**
3628
     * MERGEDCELLS.
3629
     *
3630
     * This record contains the addresses of merged cell ranges
3631
     * in the current sheet.
3632
     *
3633
     * --    "OpenOffice.org's Documentation of the Microsoft
3634
     *         Excel File Format"
3635
     */
3636
    protected function readMergedCells(): void
19✔
3637
    {
3638
        $length = self::getUInt2d($this->data, $this->pos + 2);
19✔
3639
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
19✔
3640

3641
        // move stream pointer to next record
3642
        $this->pos += 4 + $length;
19✔
3643

3644
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
19✔
3645
            $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($recordData);
17✔
3646
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
17✔
3647
                /** @var string $cellRangeAddress */
3648
                if (
3649
                    (str_contains($cellRangeAddress, ':'))
17✔
3650
                    && ($this->includeCellRangeFiltered($cellRangeAddress))
17✔
3651
                ) {
3652
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
17✔
3653
                }
3654
            }
3655
        }
3656
    }
3657

3658
    /**
3659
     * Read HYPERLINK record.
3660
     */
3661
    protected function readHyperLink(): void
7✔
3662
    {
3663
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
3664
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
3665

3666
        // move stream pointer forward to next record
3667
        $this->pos += 4 + $length;
7✔
3668

3669
        if (!$this->readDataOnly) {
7✔
3670
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
3671
            try {
3672
                $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed($recordData);
7✔
3673
            } catch (PhpSpreadsheetException) {
×
UNCOV
3674
                return;
×
3675
            }
3676

3677
            // offset: 8, size: 16; GUID of StdLink
3678

3679
            // offset: 24, size: 4; unknown value
3680

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

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

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

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

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

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

3700
            // offset within record data
3701
            $offset = 32;
7✔
3702

3703
            if ($hasDesc) {
7✔
3704
                // offset: 32; size: var; character count of description text
3705
                $dl = self::getInt4d($recordData, 32);
3✔
3706
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
3707
                //$desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
3708
                $offset += 4 + 2 * $dl;
3✔
3709
            }
3710
            if ($hasFrame) {
7✔
3711
                $fl = self::getInt4d($recordData, $offset);
×
UNCOV
3712
                $offset += 4 + 2 * $fl;
×
3713
            }
3714

3715
            // detect type of hyperlink (there are 4 types)
3716
            $hyperlinkType = null;
7✔
3717

3718
            if ($isUNC) {
7✔
UNCOV
3719
                $hyperlinkType = 'UNC';
×
3720
            } elseif (!$isFileLinkOrUrl) {
7✔
3721
                $hyperlinkType = 'workbook';
4✔
3722
            } elseif (ord($recordData[$offset]) == 0x03) {
7✔
UNCOV
3723
                $hyperlinkType = 'local';
×
3724
            } elseif (ord($recordData[$offset]) == 0xE0) {
7✔
3725
                $hyperlinkType = 'URL';
7✔
3726
            }
3727

3728
            switch ($hyperlinkType) {
3729
                case 'URL':
7✔
3730
                    // section 5.58.2: Hyperlink containing a URL
3731
                    // e.g. http://example.org/index.php
3732

3733
                    // offset: var; size: 16; GUID of URL Moniker
3734
                    $offset += 16;
7✔
3735
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
3736
                    $us = self::getInt4d($recordData, $offset);
7✔
3737
                    $offset += 4;
7✔
3738
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
3739
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
7✔
3740
                    $nullOffset = strpos($url, chr(0x00));
7✔
3741
                    if ($nullOffset) {
7✔
3742
                        $url = substr($url, 0, $nullOffset);
3✔
3743
                    }
3744
                    $url .= $hasText ? '#' : '';
7✔
3745
                    $offset += $us;
7✔
3746

3747
                    break;
7✔
3748
                case 'local':
4✔
3749
                    // section 5.58.3: Hyperlink to local file
3750
                    // examples:
3751
                    //   mydoc.txt
3752
                    //   ../../somedoc.xls#Sheet!A1
3753

3754
                    // offset: var; size: 16; GUI of File Moniker
UNCOV
3755
                    $offset += 16;
×
3756

3757
                    // offset: var; size: 2; directory up-level count.
3758
                    $upLevelCount = self::getUInt2d($recordData, $offset);
×
UNCOV
3759
                    $offset += 2;
×
3760

3761
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
3762
                    $sl = self::getInt4d($recordData, $offset);
×
UNCOV
3763
                    $offset += 4;
×
3764

3765
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
3766
                    $shortenedFilePath = substr($recordData, $offset, $sl);
×
3767
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
×
UNCOV
3768
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
×
3769

UNCOV
3770
                    $offset += $sl;
×
3771

3772
                    // offset: var; size: 24; unknown sequence
UNCOV
3773
                    $offset += 24;
×
3774

3775
                    // extended file path
3776
                    // offset: var; size: 4; size of the following file link field including string lenth mark
3777
                    $sz = self::getInt4d($recordData, $offset);
×
UNCOV
3778
                    $offset += 4;
×
3779

UNCOV
3780
                    $extendedFilePath = '';
×
3781
                    // only present if $sz > 0
UNCOV
3782
                    if ($sz > 0) {
×
3783
                        // offset: var; size: 4; size of the character array of the extended file path and name
3784
                        $xl = self::getInt4d($recordData, $offset);
×
UNCOV
3785
                        $offset += 4;
×
3786

3787
                        // offset: var; size 2; unknown
UNCOV
3788
                        $offset += 2;
×
3789

3790
                        // offset: var; size $xl; character array of the extended file path and name.
3791
                        $extendedFilePath = substr($recordData, $offset, $xl);
×
3792
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
×
UNCOV
3793
                        $offset += $xl;
×
3794
                    }
3795

3796
                    // construct the path
3797
                    $url = str_repeat('..\\', $upLevelCount);
×
3798
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
×
UNCOV
3799
                    $url .= $hasText ? '#' : '';
×
3800

UNCOV
3801
                    break;
×
3802
                case 'UNC':
4✔
3803
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
3804
                    // todo: implement
UNCOV
3805
                    return;
×
3806
                case 'workbook':
4✔
3807
                    // section 5.58.5: Hyperlink to the Current Workbook
3808
                    // e.g. Sheet2!B1:C2, stored in text mark field
3809
                    $url = 'sheet://';
4✔
3810

3811
                    break;
4✔
3812
                default:
UNCOV
3813
                    return;
×
3814
            }
3815

3816
            if ($hasText) {
7✔
3817
                // offset: var; size: 4; character count of text mark including trailing zero word
3818
                $tl = self::getInt4d($recordData, $offset);
4✔
3819
                $offset += 4;
4✔
3820
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
3821
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4✔
3822
                $url .= $text;
4✔
3823
            }
3824

3825
            // apply the hyperlink to all the relevant cells
3826
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
7✔
3827
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
7✔
3828
            }
3829
        }
3830
    }
3831

3832
    /**
3833
     * Read DATAVALIDATIONS record.
3834
     */
3835
    protected function readDataValidations(): void
5✔
3836
    {
3837
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3838
        //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3839

3840
        // move stream pointer forward to next record
3841
        $this->pos += 4 + $length;
5✔
3842
    }
3843

3844
    /**
3845
     * Read DATAVALIDATION record.
3846
     */
3847
    protected function readDataValidation(): void
5✔
3848
    {
3849
        (new Xls\DataValidationHelper())->readDataValidation2($this);
5✔
3850
    }
3851

3852
    /**
3853
     * Read SHEETLAYOUT record. Stores sheet tab color information.
3854
     */
3855
    protected function readSheetLayout(): void
5✔
3856
    {
3857
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3858
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
3859

3860
        // move stream pointer to next record
3861
        $this->pos += 4 + $length;
5✔
3862

3863
        if (!$this->readDataOnly) {
5✔
3864
            // offset: 0; size: 2; repeated record identifier 0x0862
3865

3866
            // offset: 2; size: 10; not used
3867

3868
            // offset: 12; size: 4; size of record data
3869
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
3870
            $sz = self::getInt4d($recordData, 12);
5✔
3871

3872
            switch ($sz) {
3873
                case 0x14:
5✔
3874
                    // offset: 16; size: 2; color index for sheet tab
3875
                    $colorIndex = self::getUInt2d($recordData, 16);
1✔
3876
                    /** @var string[] */
3877
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
1✔
3878
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
1✔
3879

3880
                    break;
1✔
3881
                case 0x28:
4✔
3882
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
3883
                    return;
4✔
3884
            }
3885
        }
3886
    }
3887

3888
    /**
3889
     * Read SHEETPROTECTION record (FEATHEADR).
3890
     */
3891
    protected function readSheetProtection(): void
124✔
3892
    {
3893
        $length = self::getUInt2d($this->data, $this->pos + 2);
124✔
3894
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
124✔
3895

3896
        // move stream pointer to next record
3897
        $this->pos += 4 + $length;
124✔
3898

3899
        if ($this->readDataOnly) {
124✔
3900
            return;
2✔
3901
        }
3902

3903
        // offset: 0; size: 2; repeated record header
3904

3905
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
3906

3907
        // offset: 4; size: 8; Currently not used and set to 0
3908

3909
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
3910
        $isf = self::getUInt2d($recordData, 12);
122✔
3911
        if ($isf != 2) {
122✔
UNCOV
3912
            return;
×
3913
        }
3914

3915
        // offset: 14; size: 1; =1 since this is a feat header
3916

3917
        // offset: 15; size: 4; size of rgbHdrSData
3918

3919
        // rgbHdrSData, assume "Enhanced Protection"
3920
        // offset: 19; size: 2; option flags
3921
        $options = self::getUInt2d($recordData, 19);
122✔
3922

3923
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
3924
        // Note - do not negate $bool
3925
        $bool = (0x0001 & $options) >> 0;
122✔
3926
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
122✔
3927

3928
        // bit: 1; mask 0x0002; edit scenarios
3929
        // Note - do not negate $bool
3930
        $bool = (0x0002 & $options) >> 1;
122✔
3931
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
122✔
3932

3933
        // bit: 2; mask 0x0004; format cells
3934
        $bool = (0x0004 & $options) >> 2;
122✔
3935
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
122✔
3936

3937
        // bit: 3; mask 0x0008; format columns
3938
        $bool = (0x0008 & $options) >> 3;
122✔
3939
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
122✔
3940

3941
        // bit: 4; mask 0x0010; format rows
3942
        $bool = (0x0010 & $options) >> 4;
122✔
3943
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
122✔
3944

3945
        // bit: 5; mask 0x0020; insert columns
3946
        $bool = (0x0020 & $options) >> 5;
122✔
3947
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
122✔
3948

3949
        // bit: 6; mask 0x0040; insert rows
3950
        $bool = (0x0040 & $options) >> 6;
122✔
3951
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
122✔
3952

3953
        // bit: 7; mask 0x0080; insert hyperlinks
3954
        $bool = (0x0080 & $options) >> 7;
122✔
3955
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
122✔
3956

3957
        // bit: 8; mask 0x0100; delete columns
3958
        $bool = (0x0100 & $options) >> 8;
122✔
3959
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
122✔
3960

3961
        // bit: 9; mask 0x0200; delete rows
3962
        $bool = (0x0200 & $options) >> 9;
122✔
3963
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
122✔
3964

3965
        // bit: 10; mask 0x0400; select locked cells
3966
        // Note that this is opposite of most of above.
3967
        $bool = (0x0400 & $options) >> 10;
122✔
3968
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
122✔
3969

3970
        // bit: 11; mask 0x0800; sort cell range
3971
        $bool = (0x0800 & $options) >> 11;
122✔
3972
        $this->phpSheet->getProtection()->setSort(!$bool);
122✔
3973

3974
        // bit: 12; mask 0x1000; auto filter
3975
        $bool = (0x1000 & $options) >> 12;
122✔
3976
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
122✔
3977

3978
        // bit: 13; mask 0x2000; pivot tables
3979
        $bool = (0x2000 & $options) >> 13;
122✔
3980
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
122✔
3981

3982
        // bit: 14; mask 0x4000; select unlocked cells
3983
        // Note that this is opposite of most of above.
3984
        $bool = (0x4000 & $options) >> 14;
122✔
3985
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
122✔
3986

3987
        // offset: 21; size: 2; not used
3988
    }
3989

3990
    /**
3991
     * Read RANGEPROTECTION record
3992
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
3993
     * where it is referred to as FEAT record.
3994
     */
3995
    protected function readRangeProtection(): void
2✔
3996
    {
3997
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
3998
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
3999

4000
        // move stream pointer to next record
4001
        $this->pos += 4 + $length;
2✔
4002

4003
        // local pointer in record data
4004
        $offset = 0;
2✔
4005

4006
        if (!$this->readDataOnly) {
2✔
4007
            $offset += 12;
2✔
4008

4009
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
4010
            $isf = self::getUInt2d($recordData, 12);
2✔
4011
            if ($isf != 2) {
2✔
4012
                // we only read FEAT records of type 2
UNCOV
4013
                return;
×
4014
            }
4015
            $offset += 2;
2✔
4016

4017
            $offset += 5;
2✔
4018

4019
            // offset: 19; size: 2; count of ref ranges this feature is on
4020
            $cref = self::getUInt2d($recordData, 19);
2✔
4021
            $offset += 2;
2✔
4022

4023
            $offset += 6;
2✔
4024

4025
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
4026
            $cellRanges = [];
2✔
4027
            for ($i = 0; $i < $cref; ++$i) {
2✔
4028
                try {
4029
                    $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
2✔
4030
                } catch (PhpSpreadsheetException) {
×
UNCOV
4031
                    return;
×
4032
                }
4033
                $cellRanges[] = $cellRange;
2✔
4034
                $offset += 8;
2✔
4035
            }
4036

4037
            // offset: var; size: var; variable length of feature specific data
4038
            //$rgbFeat = substr($recordData, $offset);
4039
            $offset += 4;
2✔
4040

4041
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
4042
            $wPassword = self::getInt4d($recordData, $offset);
2✔
4043
            $offset += 4;
2✔
4044

4045
            // Apply range protection to sheet
4046
            if ($cellRanges) {
2✔
4047
                $this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true);
2✔
4048
            }
4049
        }
4050
    }
4051

4052
    /**
4053
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
4054
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
4055
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
4056
     */
4057
    protected function readContinue(): void
1✔
4058
    {
4059
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
4060
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
4061

4062
        // check if we are reading drawing data
4063
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
4064
        if ($this->drawingData == '') {
1✔
4065
            // move stream pointer to next record
4066
            $this->pos += 4 + $length;
1✔
4067

4068
            return;
1✔
4069
        }
4070

4071
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
UNCOV
4072
        if ($length < 4) {
×
4073
            // move stream pointer to next record
UNCOV
4074
            $this->pos += 4 + $length;
×
4075

UNCOV
4076
            return;
×
4077
        }
4078

4079
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
4080
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
4081
        // we know that Escher stream may be split at least at
4082
        //        0xF003 MsofbtSpgrContainer
4083
        //        0xF004 MsofbtSpContainer
4084
        //        0xF00D MsofbtClientTextbox
UNCOV
4085
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
×
4086

4087
        $splitPoint = self::getUInt2d($recordData, 2);
×
UNCOV
4088
        if (in_array($splitPoint, $validSplitPoints)) {
×
4089
            // get spliced record data (and move pointer to next record)
4090
            $splicedRecordData = $this->getSplicedRecordData();
×
UNCOV
4091
            $this->drawingData .= StringHelper::convertToString($splicedRecordData['recordData']);
×
4092

UNCOV
4093
            return;
×
4094
        }
4095

4096
        // move stream pointer to next record
UNCOV
4097
        $this->pos += 4 + $length;
×
4098
    }
4099

4100
    /**
4101
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
4102
     * records are found. Splices the record data pieces and returns the combined string as if record data
4103
     * is in one piece.
4104
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
4105
     *
4106
     * @return mixed[]
4107
     */
4108
    private function getSplicedRecordData(): array
134✔
4109
    {
4110
        $data = '';
134✔
4111
        $spliceOffsets = [];
134✔
4112

4113
        $i = 0;
134✔
4114
        $spliceOffsets[0] = 0;
134✔
4115

4116
        do {
4117
            ++$i;
134✔
4118

4119
            // offset: 0; size: 2; identifier
4120
            //$identifier = self::getUInt2d($this->data, $this->pos);
4121
            // offset: 2; size: 2; length
4122
            $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
4123
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
4124

4125
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
134✔
4126

4127
            $this->pos += 4 + $length;
134✔
4128
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
134✔
4129
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
134✔
4130

4131
        return [
134✔
4132
            'recordData' => $data,
134✔
4133
            'spliceOffsets' => $spliceOffsets,
134✔
4134
        ];
134✔
4135
    }
4136

4137
    /**
4138
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
4139
     *
4140
     * @param string $formulaStructure The complete binary data for the formula
4141
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4142
     *
4143
     * @return string Human readable formula
4144
     */
4145
    protected function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string
74✔
4146
    {
4147
        // offset: 0; size: 2; size of the following formula data
4148
        $sz = self::getUInt2d($formulaStructure, 0);
74✔
4149

4150
        // offset: 2; size: sz
4151
        $formulaData = substr($formulaStructure, 2, $sz);
74✔
4152

4153
        // offset: 2 + sz; size: variable (optional)
4154
        if (strlen($formulaStructure) > 2 + $sz) {
74✔
4155
            $additionalData = substr($formulaStructure, 2 + $sz);
1✔
4156
        } else {
4157
            $additionalData = '';
73✔
4158
        }
4159

4160
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
74✔
4161
    }
4162

4163
    /**
4164
     * Take formula data and additional data for formula and return human readable formula.
4165
     *
4166
     * @param string $formulaData The binary data for the formula itself
4167
     * @param string $additionalData Additional binary data going with the formula
4168
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4169
     *
4170
     * @return string Human readable formula
4171
     */
4172
    private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string
74✔
4173
    {
4174
        // start parsing the formula data
4175
        $tokens = [];
74✔
4176

4177
        while ($formulaData !== '' && $token = $this->getNextToken($formulaData, $baseCell)) {
74✔
4178
            $tokens[] = $token;
74✔
4179
            /** @var int[] $token */
4180
            $formulaData = substr($formulaData, $token['size']);
74✔
4181
        }
4182

4183
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
74✔
4184

4185
        return $formulaString;
74✔
4186
    }
4187

4188
    /**
4189
     * Take array of tokens together with additional data for formula and return human readable formula.
4190
     *
4191
     * @param mixed[][] $tokens
4192
     * @param string $additionalData Additional binary data going with the formula
4193
     *
4194
     * @return string Human readable formula
4195
     */
4196
    private function createFormulaFromTokens(array $tokens, string $additionalData): string
74✔
4197
    {
4198
        // empty formula?
4199
        if (empty($tokens)) {
74✔
4200
            return '';
5✔
4201
        }
4202

4203
        $formulaStrings = [];
74✔
4204
        foreach ($tokens as $token) {
74✔
4205
            // initialize spaces
4206
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
74✔
4207
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
74✔
4208
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
74✔
4209
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
74✔
4210
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
74✔
4211
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
74✔
4212
            /** @var string */
4213
            $tokenData = $token['data'] ?? '';
74✔
4214
            switch ($token['name']) {
74✔
4215
                case 'tAdd': // addition
74✔
4216
                case 'tConcat': // addition
74✔
4217
                case 'tDiv': // division
74✔
4218
                case 'tEQ': // equality
74✔
4219
                case 'tGE': // greater than or equal
74✔
4220
                case 'tGT': // greater than
74✔
4221
                case 'tIsect': // intersection
74✔
4222
                case 'tLE': // less than or equal
74✔
4223
                case 'tList': // less than or equal
74✔
4224
                case 'tLT': // less than
74✔
4225
                case 'tMul': // multiplication
74✔
4226
                case 'tNE': // multiplication
74✔
4227
                case 'tPower': // power
74✔
4228
                case 'tRange': // range
74✔
4229
                case 'tSub': // subtraction
74✔
4230
                    $op2 = array_pop($formulaStrings);
37✔
4231
                    $op1 = array_pop($formulaStrings);
37✔
4232
                    $formulaStrings[] = "$op1$space1$space0{$tokenData}$op2";
37✔
4233
                    unset($space0, $space1);
37✔
4234

4235
                    break;
37✔
4236
                case 'tUplus': // unary plus
74✔
4237
                case 'tUminus': // unary minus
74✔
4238
                    $op = array_pop($formulaStrings);
4✔
4239
                    $formulaStrings[] = "$space1$space0{$tokenData}$op";
4✔
4240
                    unset($space0, $space1);
4✔
4241

4242
                    break;
4✔
4243
                case 'tPercent': // percent sign
74✔
4244
                    $op = array_pop($formulaStrings);
1✔
4245
                    $formulaStrings[] = "$op$space1$space0{$tokenData}";
1✔
4246
                    unset($space0, $space1);
1✔
4247

4248
                    break;
1✔
4249
                case 'tAttrVolatile': // indicates volatile function
74✔
4250
                case 'tAttrIf':
74✔
4251
                case 'tAttrSkip':
74✔
4252
                case 'tAttrChoose':
74✔
4253
                    // token is only important for Excel formula evaluator
4254
                    // do nothing
4255
                    break;
3✔
4256
                case 'tAttrSpace': // space / carriage return
74✔
4257
                    // space will be used when next token arrives, do not alter formulaString stack
4258
                    /** @var string[][] $token */
4259
                    switch ($token['data']['spacetype']) {
×
4260
                        case 'type0':
×
UNCOV
4261
                            $space0 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4262

4263
                            break;
×
4264
                        case 'type1':
×
UNCOV
4265
                            $space1 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4266

4267
                            break;
×
4268
                        case 'type2':
×
UNCOV
4269
                            $space2 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4270

4271
                            break;
×
4272
                        case 'type3':
×
UNCOV
4273
                            $space3 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4274

4275
                            break;
×
4276
                        case 'type4':
×
UNCOV
4277
                            $space4 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4278

4279
                            break;
×
4280
                        case 'type5':
×
UNCOV
4281
                            $space5 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4282

UNCOV
4283
                            break;
×
4284
                    }
4285

UNCOV
4286
                    break;
×
4287
                case 'tAttrSum': // SUM function with one parameter
74✔
4288
                    $op = array_pop($formulaStrings);
15✔
4289
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
15✔
4290
                    unset($space0, $space1);
15✔
4291

4292
                    break;
15✔
4293
                case 'tFunc': // function with fixed number of arguments
74✔
4294
                case 'tFuncV': // function with variable number of arguments
74✔
4295
                    /** @var string[] */
4296
                    $temp1 = $token['data'];
39✔
4297
                    $temp2 = $temp1['function'];
39✔
4298
                    if ($temp2 != '') {
39✔
4299
                        // normal function
4300
                        $ops = []; // array of operators
38✔
4301
                        $temp3 = (int) $temp1['args'];
38✔
4302
                        for ($i = 0; $i < $temp3; ++$i) {
38✔
4303
                            $ops[] = array_pop($formulaStrings);
29✔
4304
                        }
4305
                        $ops = array_reverse($ops);
38✔
4306
                        $formulaStrings[] = "$space1$space0{$temp2}(" . implode(',', $ops) . ')';
38✔
4307
                        unset($space0, $space1);
38✔
4308
                    } else {
4309
                        // add-in function
4310
                        $ops = []; // array of operators
1✔
4311
                        /** @var int[] */
4312
                        $temp = $token['data'];
1✔
4313
                        for ($i = 0; $i < $temp['args'] - 1; ++$i) {
1✔
4314
                            $ops[] = array_pop($formulaStrings);
1✔
4315
                        }
4316
                        $ops = array_reverse($ops);
1✔
4317
                        $function = array_pop($formulaStrings);
1✔
4318
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
1✔
4319
                        unset($space0, $space1);
1✔
4320
                    }
4321

4322
                    break;
39✔
4323
                case 'tParen': // parenthesis
74✔
4324
                    $expression = array_pop($formulaStrings);
1✔
4325
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
1✔
4326
                    unset($space2, $space3, $space4, $space5);
1✔
4327

4328
                    break;
1✔
4329
                case 'tArray': // array constant
74✔
4330
                    $constantArray = Xls\Biff8::readBIFF8ConstantArray($additionalData);
1✔
4331
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
1✔
4332
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
1✔
4333
                    unset($space0, $space1);
1✔
4334

4335
                    break;
1✔
4336
                case 'tMemArea':
74✔
4337
                    // bite off chunk of additional data
4338
                    $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($additionalData);
×
4339
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
×
4340
                    $formulaStrings[] = "$space1$space0{$tokenData}";
×
UNCOV
4341
                    unset($space0, $space1);
×
4342

UNCOV
4343
                    break;
×
4344
                case 'tArea': // cell range address
74✔
4345
                case 'tBool': // boolean
72✔
4346
                case 'tErr': // error code
71✔
4347
                case 'tInt': // integer
70✔
4348
                case 'tMemErr':
54✔
4349
                case 'tMemFunc':
54✔
4350
                case 'tMissArg':
54✔
4351
                case 'tName':
54✔
4352
                case 'tNameX':
54✔
4353
                case 'tNum': // number
53✔
4354
                case 'tRef': // single cell reference
51✔
4355
                case 'tRef3d': // 3d cell reference
41✔
4356
                case 'tArea3d': // 3d cell range reference
38✔
4357
                case 'tRefN':
27✔
4358
                case 'tAreaN':
26✔
4359
                case 'tStr': // string
25✔
4360
                    $formulaStrings[] = "$space1$space0{$tokenData}";
74✔
4361
                    unset($space0, $space1);
74✔
4362

4363
                    break;
74✔
4364
            }
4365
        }
4366
        $formulaString = $formulaStrings[0];
74✔
4367

4368
        return $formulaString;
74✔
4369
    }
4370

4371
    /**
4372
     * Fetch next token from binary formula data.
4373
     *
4374
     * @param string $formulaData Formula data
4375
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4376
     *
4377
     * @return mixed[]
4378
     */
4379
    private function getNextToken(string $formulaData, string $baseCell = 'A1'): array
74✔
4380
    {
4381
        // offset: 0; size: 1; token id
4382
        $id = ord($formulaData[0]); // token id
74✔
4383
        $name = false; // initialize token name
74✔
4384

4385
        switch ($id) {
4386
            case 0x03:
74✔
4387
                $name = 'tAdd';
16✔
4388
                $size = 1;
16✔
4389
                $data = '+';
16✔
4390

4391
                break;
16✔
4392
            case 0x04:
74✔
4393
                $name = 'tSub';
14✔
4394
                $size = 1;
14✔
4395
                $data = '-';
14✔
4396

4397
                break;
14✔
4398
            case 0x05:
74✔
4399
                $name = 'tMul';
4✔
4400
                $size = 1;
4✔
4401
                $data = '*';
4✔
4402

4403
                break;
4✔
4404
            case 0x06:
74✔
4405
                $name = 'tDiv';
13✔
4406
                $size = 1;
13✔
4407
                $data = '/';
13✔
4408

4409
                break;
13✔
4410
            case 0x07:
74✔
4411
                $name = 'tPower';
1✔
4412
                $size = 1;
1✔
4413
                $data = '^';
1✔
4414

4415
                break;
1✔
4416
            case 0x08:
74✔
4417
                $name = 'tConcat';
4✔
4418
                $size = 1;
4✔
4419
                $data = '&';
4✔
4420

4421
                break;
4✔
4422
            case 0x09:
74✔
4423
                $name = 'tLT';
1✔
4424
                $size = 1;
1✔
4425
                $data = '<';
1✔
4426

4427
                break;
1✔
4428
            case 0x0A:
74✔
4429
                $name = 'tLE';
1✔
4430
                $size = 1;
1✔
4431
                $data = '<=';
1✔
4432

4433
                break;
1✔
4434
            case 0x0B:
74✔
4435
                $name = 'tEQ';
4✔
4436
                $size = 1;
4✔
4437
                $data = '=';
4✔
4438

4439
                break;
4✔
4440
            case 0x0C:
74✔
4441
                $name = 'tGE';
1✔
4442
                $size = 1;
1✔
4443
                $data = '>=';
1✔
4444

4445
                break;
1✔
4446
            case 0x0D:
74✔
4447
                $name = 'tGT';
2✔
4448
                $size = 1;
2✔
4449
                $data = '>';
2✔
4450

4451
                break;
2✔
4452
            case 0x0E:
74✔
4453
                $name = 'tNE';
2✔
4454
                $size = 1;
2✔
4455
                $data = '<>';
2✔
4456

4457
                break;
2✔
4458
            case 0x0F:
74✔
4459
                $name = 'tIsect';
×
4460
                $size = 1;
×
UNCOV
4461
                $data = ' ';
×
4462

UNCOV
4463
                break;
×
4464
            case 0x10:
74✔
4465
                $name = 'tList';
2✔
4466
                $size = 1;
2✔
4467
                $data = ',';
2✔
4468

4469
                break;
2✔
4470
            case 0x11:
74✔
4471
                $name = 'tRange';
×
4472
                $size = 1;
×
UNCOV
4473
                $data = ':';
×
4474

UNCOV
4475
                break;
×
4476
            case 0x12:
74✔
4477
                $name = 'tUplus';
1✔
4478
                $size = 1;
1✔
4479
                $data = '+';
1✔
4480

4481
                break;
1✔
4482
            case 0x13:
74✔
4483
                $name = 'tUminus';
4✔
4484
                $size = 1;
4✔
4485
                $data = '-';
4✔
4486

4487
                break;
4✔
4488
            case 0x14:
74✔
4489
                $name = 'tPercent';
1✔
4490
                $size = 1;
1✔
4491
                $data = '%';
1✔
4492

4493
                break;
1✔
4494
            case 0x15:    //    parenthesis
74✔
4495
                $name = 'tParen';
1✔
4496
                $size = 1;
1✔
4497
                $data = null;
1✔
4498

4499
                break;
1✔
4500
            case 0x16:    //    missing argument
74✔
4501
                $name = 'tMissArg';
×
4502
                $size = 1;
×
UNCOV
4503
                $data = '';
×
4504

UNCOV
4505
                break;
×
4506
            case 0x17:    //    string
74✔
4507
                $name = 'tStr';
25✔
4508
                // offset: 1; size: var; Unicode string, 8-bit string length
4509
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
25✔
4510
                $size = 1 + $string['size'];
25✔
4511
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
25✔
4512

4513
                break;
25✔
4514
            case 0x19:    //    Special attribute
73✔
4515
                // offset: 1; size: 1; attribute type flags:
4516
                switch (ord($formulaData[1])) {
17✔
4517
                    case 0x01:
17✔
4518
                        $name = 'tAttrVolatile';
3✔
4519
                        $size = 4;
3✔
4520
                        $data = null;
3✔
4521

4522
                        break;
3✔
4523
                    case 0x02:
15✔
4524
                        $name = 'tAttrIf';
1✔
4525
                        $size = 4;
1✔
4526
                        $data = null;
1✔
4527

4528
                        break;
1✔
4529
                    case 0x04:
15✔
4530
                        $name = 'tAttrChoose';
1✔
4531
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
4532
                        $nc = self::getUInt2d($formulaData, 2);
1✔
4533
                        // offset: 4; size: 2 * $nc
4534
                        // offset: 4 + 2 * $nc; size: 2
4535
                        $size = 2 * $nc + 6;
1✔
4536
                        $data = null;
1✔
4537

4538
                        break;
1✔
4539
                    case 0x08:
15✔
4540
                        $name = 'tAttrSkip';
1✔
4541
                        $size = 4;
1✔
4542
                        $data = null;
1✔
4543

4544
                        break;
1✔
4545
                    case 0x10:
15✔
4546
                        $name = 'tAttrSum';
15✔
4547
                        $size = 4;
15✔
4548
                        $data = null;
15✔
4549

4550
                        break;
15✔
4551
                    case 0x40:
×
4552
                    case 0x41:
×
4553
                        $name = 'tAttrSpace';
×
UNCOV
4554
                        $size = 4;
×
4555
                        // offset: 2; size: 2; space type and position
4556
                        $spacetype = match (ord($formulaData[2])) {
×
4557
                            0x00 => 'type0',
×
4558
                            0x01 => 'type1',
×
4559
                            0x02 => 'type2',
×
4560
                            0x03 => 'type3',
×
4561
                            0x04 => 'type4',
×
4562
                            0x05 => 'type5',
×
4563
                            default => throw new Exception('Unrecognized space type in tAttrSpace token'),
×
UNCOV
4564
                        };
×
4565
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
UNCOV
4566
                        $spacecount = ord($formulaData[3]);
×
4567

UNCOV
4568
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
×
4569

UNCOV
4570
                        break;
×
4571
                    default:
UNCOV
4572
                        throw new Exception('Unrecognized attribute flag in tAttr token');
×
4573
                }
4574

4575
                break;
17✔
4576
            case 0x1C:    //    error code
73✔
4577
                // offset: 1; size: 1; error code
4578
                $name = 'tErr';
5✔
4579
                $size = 2;
5✔
4580
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5✔
4581

4582
                break;
5✔
4583
            case 0x1D:    //    boolean
72✔
4584
                // offset: 1; size: 1; 0 = false, 1 = true;
4585
                $name = 'tBool';
3✔
4586
                $size = 2;
3✔
4587
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
3✔
4588

4589
                break;
3✔
4590
            case 0x1E:    //    integer
72✔
4591
                // offset: 1; size: 2; unsigned 16-bit integer
4592
                $name = 'tInt';
46✔
4593
                $size = 3;
46✔
4594
                $data = self::getUInt2d($formulaData, 1);
46✔
4595

4596
                break;
46✔
4597
            case 0x1F:    //    number
65✔
4598
                // offset: 1; size: 8;
4599
                $name = 'tNum';
9✔
4600
                $size = 9;
9✔
4601
                $data = self::extractNumber(substr($formulaData, 1));
9✔
4602
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
9✔
4603

4604
                break;
9✔
4605
            case 0x20:    //    array constant
64✔
4606
            case 0x40:
64✔
4607
            case 0x60:
64✔
4608
                // offset: 1; size: 7; not used
4609
                $name = 'tArray';
1✔
4610
                $size = 8;
1✔
4611
                $data = null;
1✔
4612

4613
                break;
1✔
4614
            case 0x21:    //    function with fixed number of arguments
64✔
4615
            case 0x41:
64✔
4616
            case 0x61:
60✔
4617
                $name = 'tFunc';
22✔
4618
                $size = 3;
22✔
4619
                // offset: 1; size: 2; index to built-in sheet function
4620
                $mapping = Xls\Mappings::TFUNC_MAPPINGS[self::getUInt2d($formulaData, 1)] ?? null;
22✔
4621
                if ($mapping === null) {
22✔
4622
                    throw new Exception('Unrecognized function in formula');
1✔
4623
                }
4624
                $data = ['function' => $mapping[0], 'args' => $mapping[1]];
22✔
4625

4626
                break;
22✔
4627
            case 0x22:    //    function with variable number of arguments
60✔
4628
            case 0x42:
60✔
4629
            case 0x62:
56✔
4630
                $name = 'tFuncV';
23✔
4631
                $size = 4;
23✔
4632
                // offset: 1; size: 1; number of arguments
4633
                $args = ord($formulaData[1]);
23✔
4634
                // offset: 2: size: 2; index to built-in sheet function
4635
                $index = self::getUInt2d($formulaData, 2);
23✔
4636
                $function = Xls\Mappings::TFUNCV_MAPPINGS[$index] ?? null;
23✔
4637
                if ($function === null) {
23✔
UNCOV
4638
                    throw new Exception('Unrecognized function in formula');
×
4639
                }
4640
                $data = ['function' => $function, 'args' => $args];
23✔
4641

4642
                break;
23✔
4643
            case 0x23:    //    index to defined name
56✔
4644
            case 0x43:
56✔
4645
            case 0x63:
56✔
4646
                $name = 'tName';
1✔
4647
                $size = 5;
1✔
4648
                // offset: 1; size: 2; one-based index to definedname record
4649
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
1✔
4650
                // offset: 2; size: 2; not used
4651
                /** @var string[] */
4652
                $data = $this->definedname[$definedNameIndex]['name'] ?? ''; //* @phpstan-ignore-line
1✔
4653

4654
                break;
1✔
4655
            case 0x24:    //    single cell reference e.g. A5
55✔
4656
            case 0x44:
55✔
4657
            case 0x64:
46✔
4658
                $name = 'tRef';
25✔
4659
                $size = 5;
25✔
4660
                $data = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 1, 4));
25✔
4661

4662
                break;
25✔
4663
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
44✔
4664
            case 0x45:
22✔
4665
            case 0x65:
22✔
4666
                $name = 'tArea';
29✔
4667
                $size = 9;
29✔
4668
                $data = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
29✔
4669

4670
                break;
29✔
4671
            case 0x26:    //    Constant reference sub-expression
21✔
4672
            case 0x46:
21✔
4673
            case 0x66:
21✔
UNCOV
4674
                $name = 'tMemArea';
×
4675
                // offset: 1; size: 4; not used
4676
                // offset: 5; size: 2; size of the following subexpression
4677
                $subSize = self::getUInt2d($formulaData, 5);
×
4678
                $size = 7 + $subSize;
×
UNCOV
4679
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4680

UNCOV
4681
                break;
×
4682
            case 0x27:    //    Deleted constant reference sub-expression
21✔
4683
            case 0x47:
21✔
4684
            case 0x67:
21✔
UNCOV
4685
                $name = 'tMemErr';
×
4686
                // offset: 1; size: 4; not used
4687
                // offset: 5; size: 2; size of the following subexpression
4688
                $subSize = self::getUInt2d($formulaData, 5);
×
4689
                $size = 7 + $subSize;
×
UNCOV
4690
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4691

UNCOV
4692
                break;
×
4693
            case 0x29:    //    Variable reference sub-expression
21✔
4694
            case 0x49:
21✔
4695
            case 0x69:
21✔
4696
                $name = 'tMemFunc';
1✔
4697
                // offset: 1; size: 2; size of the following sub-expression
4698
                $subSize = self::getUInt2d($formulaData, 1);
1✔
4699
                $size = 3 + $subSize;
1✔
4700
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
1✔
4701

4702
                break;
1✔
4703
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
21✔
4704
            case 0x4C:
21✔
4705
            case 0x6C:
20✔
4706
                $name = 'tRefN';
4✔
4707
                $size = 5;
4✔
4708
                $data = Xls\Biff8::readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
4✔
4709

4710
                break;
4✔
4711
            case 0x2D:    //    Relative 2d range reference
18✔
4712
            case 0x4D:
17✔
4713
            case 0x6D:
17✔
4714
                $name = 'tAreaN';
1✔
4715
                $size = 9;
1✔
4716
                $data = Xls\Biff8::readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
1✔
4717

4718
                break;
1✔
4719
            case 0x39:    //    External name
17✔
4720
            case 0x59:
16✔
4721
            case 0x79:
16✔
4722
                $name = 'tNameX';
1✔
4723
                $size = 7;
1✔
4724
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
4725
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
4726
                $index = self::getUInt2d($formulaData, 3);
1✔
4727
                // assume index is to EXTERNNAME record
4728
                $data = $this->externalNames[$index - 1]['name'] ?? '';
1✔
4729

4730
                // offset: 5; size: 2; not used
4731
                break;
1✔
4732
            case 0x3A:    //    3d reference to cell
16✔
4733
            case 0x5A:
13✔
4734
            case 0x7A:
13✔
4735
                $name = 'tRef3d';
4✔
4736
                $size = 7;
4✔
4737

4738
                try {
4739
                    // offset: 1; size: 2; index to REF entry
4740
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
4✔
4741
                    // offset: 3; size: 4; cell address
4742
                    $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 3, 4));
4✔
4743

4744
                    $data = "$sheetRange!$cellAddress";
4✔
UNCOV
4745
                } catch (PhpSpreadsheetException) {
×
4746
                    // deleted sheet reference
UNCOV
4747
                    $data = '#REF!';
×
4748
                }
4749

4750
                break;
4✔
4751
            case 0x3B:    //    3d reference to cell range
12✔
4752
            case 0x5B:
1✔
4753
            case 0x7B:
1✔
4754
                $name = 'tArea3d';
11✔
4755
                $size = 11;
11✔
4756

4757
                try {
4758
                    // offset: 1; size: 2; index to REF entry
4759
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
11✔
4760
                    // offset: 3; size: 8; cell address
4761
                    $cellRangeAddress = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
11✔
4762

4763
                    $data = "$sheetRange!$cellRangeAddress";
11✔
UNCOV
4764
                } catch (PhpSpreadsheetException) {
×
4765
                    // deleted sheet reference
UNCOV
4766
                    $data = '#REF!';
×
4767
                }
4768

4769
                break;
11✔
4770
                // Unknown cases    // don't know how to deal with
4771
            default:
4772
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
1✔
4773
        }
4774

4775
        return [
74✔
4776
            'id' => $id,
74✔
4777
            'name' => $name,
74✔
4778
            'size' => $size,
74✔
4779
            'data' => $data,
74✔
4780
        ];
74✔
4781
    }
4782

4783
    /**
4784
     * Get a sheet range like Sheet1:Sheet3 from REF index
4785
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
4786
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
4787
     * in which case an Exception is thrown.
4788
     */
4789
    protected function readSheetRangeByRefIndex(int $index): string|false
15✔
4790
    {
4791
        if (isset($this->ref[$index])) {
15✔
4792
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
15✔
4793

4794
            switch ($type) {
4795
                case 'internal':
15✔
4796
                    // check if we have a deleted 3d reference
4797
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
15✔
UNCOV
4798
                        throw new Exception('Deleted sheet reference');
×
4799
                    }
4800

4801
                    // we have normal sheet range (collapsed or uncollapsed)
4802
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
15✔
4803
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
15✔
4804

4805
                    if ($firstSheetName == $lastSheetName) {
15✔
4806
                        // collapsed sheet range
4807
                        $sheetRange = $firstSheetName;
15✔
4808
                    } else {
UNCOV
4809
                        $sheetRange = "$firstSheetName:$lastSheetName";
×
4810
                    }
4811

4812
                    // escape the single-quotes
4813
                    $sheetRange = str_replace("'", "''", $sheetRange);
15✔
4814

4815
                    // if there are special characters, we need to enclose the range in single-quotes
4816
                    // todo: check if we have identified the whole set of special characters
4817
                    // it seems that the following characters are not accepted for sheet names
4818
                    // and we may assume that they are not present: []*/:\?
4819
                    // 'u' qualifier makes it risky to use Preg::isMatch here
4820
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
15✔
4821
                        $sheetRange = "'$sheetRange'";
4✔
4822
                    }
4823

4824
                    return $sheetRange;
15✔
4825
                default:
4826
                    // TODO: external sheet support
UNCOV
4827
                    throw new Exception('Xls reader only supports internal sheets in formulas');
×
4828
            }
4829
        }
4830

UNCOV
4831
        return false;
×
4832
    }
4833

4834
    /**
4835
     * Read byte string (8-bit string length)
4836
     * OpenOffice documentation: 2.5.2.
4837
     *
4838
     * @return array{value: mixed, size: int}
4839
     */
4840
    protected function readByteStringShort(string $subData): array
7✔
4841
    {
4842
        // offset: 0; size: 1; length of the string (character count)
4843
        $ln = ord($subData[0]);
7✔
4844

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

4848
        return [
7✔
4849
            'value' => $value,
7✔
4850
            'size' => 1 + $ln, // size in bytes of data structure
7✔
4851
        ];
7✔
4852
    }
4853

4854
    /**
4855
     * Read byte string (16-bit string length)
4856
     * OpenOffice documentation: 2.5.2.
4857
     *
4858
     * @return array{value: mixed, size: int}
4859
     */
4860
    protected function readByteStringLong(string $subData): array
2✔
4861
    {
4862
        // offset: 0; size: 2; length of the string (character count)
4863
        $ln = self::getUInt2d($subData, 0);
2✔
4864

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

4868
        //return $string;
4869
        return [
2✔
4870
            'value' => $value,
2✔
4871
            'size' => 2 + $ln, // size in bytes of data structure
2✔
4872
        ];
2✔
4873
    }
4874

4875
    protected function parseRichText(string $is): RichText
3✔
4876
    {
4877
        $value = new RichText();
3✔
4878
        $value->createText($is);
3✔
4879

4880
        return $value;
3✔
4881
    }
4882

4883
    /**
4884
     * Phpstan 1.4.4 complains that this property is never read.
4885
     * So, we might be able to get rid of it altogether.
4886
     * For now, however, this function makes it readable,
4887
     * which satisfies Phpstan.
4888
     *
4889
     * @return mixed[]
4890
     *
4891
     * @codeCoverageIgnore
4892
     */
4893
    public function getMapCellStyleXfIndex(): array
4894
    {
4895
        return $this->mapCellStyleXfIndex;
4896
    }
4897

4898
    /**
4899
     * Parse conditional formatting blocks.
4900
     *
4901
     * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
4902
     *
4903
     * @return mixed[]
4904
     */
4905
    protected function readCFHeader(): array
24✔
4906
    {
4907
        return (new Xls\ConditionalFormatting())->readCFHeader2($this);
24✔
4908
    }
4909

4910
    /** @param string[] $cellRangeAddresses */
4911
    protected function readCFRule(array $cellRangeAddresses): void
24✔
4912
    {
4913
        (new Xls\ConditionalFormatting())->readCFRule2($cellRangeAddresses, $this);
24✔
4914
    }
4915

4916
    public function getVersion(): int
5✔
4917
    {
4918
        return $this->version;
5✔
4919
    }
4920
}
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