Passed
Push — master ( f9b1b7...3922d9 )
by
unknown
18:56 queued 11:14
created

Worksheet::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 44
rs 9.488
ccs 28
cts 28
cp 1
cc 3
nc 4
nop 8
crap 3

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5
use Composer\Pcre\Preg;
6
use GdImage;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\RichText\RichText;
12
use PhpOffice\PhpSpreadsheet\RichText\Run;
13
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
14
use PhpOffice\PhpSpreadsheet\Shared\Xls;
15
use PhpOffice\PhpSpreadsheet\Style\Border;
16
use PhpOffice\PhpSpreadsheet\Style\Borders;
17
use PhpOffice\PhpSpreadsheet\Style\Conditional;
18
use PhpOffice\PhpSpreadsheet\Style\Protection;
19
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
20
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
21
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
22
23
// Original file header of PEAR::Spreadsheet_Excel_Writer_Worksheet (used as the base for this class):
24
// -----------------------------------------------------------------------------------------
25
// /*
26
// *  Module written/ported by Xavier Noguer <[email protected]>
27
// *
28
// *  The majority of this is _NOT_ my code.  I simply ported it from the
29
// *  PERL Spreadsheet::WriteExcel module.
30
// *
31
// *  The author of the Spreadsheet::WriteExcel module is John McNamara
32
// *  <[email protected]>
33
// *
34
// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
35
// *  porting of this code to PHP.  Any questions directly related to this
36
// *  class library should be directed to me.
37
// *
38
// *  License Information:
39
// *
40
// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
41
// *    Copyright (c) 2002-2003 Xavier Noguer [email protected]
42
// *
43
// *    This library is free software; you can redistribute it and/or
44
// *    modify it under the terms of the GNU Lesser General Public
45
// *    License as published by the Free Software Foundation; either
46
// *    version 2.1 of the License, or (at your option) any later version.
47
// *
48
// *    This library is distributed in the hope that it will be useful,
49
// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
50
// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
51
// *    Lesser General Public License for more details.
52
// *
53
// *    You should have received a copy of the GNU Lesser General Public
54
// *    License along with this library; if not, write to the Free Software
55
// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
56
// */
57
class Worksheet extends BIFFwriter
58
{
59
    private static int $always0 = 0;
60
61
    private static int $always1 = 1;
62
63
    /**
64
     * Formula parser.
65
     */
66
    private Parser $parser;
67
68
    /**
69
     * Array containing format information for columns.
70
     *
71
     * @var array<array{int, int, float, int, int, int}>
72
     */
73
    private array $columnInfo;
74
75
    /**
76
     * The active pane for the worksheet.
77
     */
78
    private int $activePane;
79
80
    /**
81
     * Whether to use outline.
82
     */
83
    private bool $outlineOn;
84
85
    /**
86
     * Auto outline styles.
87
     */
88
    private bool $outlineStyle;
89
90
    /**
91
     * Whether to have outline summary below.
92
     * Not currently used.
93
     */
94
    private bool $outlineBelow; //* @phpstan-ignore-line
95
96
    /**
97
     * Whether to have outline summary at the right.
98
     * Not currently used.
99
     */
100
    private bool $outlineRight; //* @phpstan-ignore-line
101
102
    /**
103
     * Reference to the total number of strings in the workbook.
104
     */
105
    private int $stringTotal;
106
107
    /**
108
     * Reference to the number of unique strings in the workbook.
109
     */
110
    private int $stringUnique;
111
112
    /**
113
     * Reference to the array containing all the unique strings in the workbook.
114
     *
115
     * @var array<string, int>
116
     */
117
    private array $stringTable;
118
119
    /**
120
     * Color cache.
121
     *
122
     * @var mixed[]
123
     */
124
    private array $colors;
125
126
    /**
127
     * Index of first used row (at least 0).
128
     */
129
    private int $firstRowIndex;
130
131
    /**
132
     * Index of last used row. (no used rows means -1).
133
     */
134
    private int $lastRowIndex;
135
136
    /**
137
     * Index of first used column (at least 0).
138
     */
139
    private int $firstColumnIndex;
140
141
    /**
142
     * Index of last used column (no used columns means -1).
143
     */
144
    private int $lastColumnIndex;
145
146
    /**
147
     * Sheet object.
148
     */
149
    public \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet;
150
151
    /**
152
     * Escher object corresponding to MSODRAWING.
153
     */
154
    private ?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher = null;
155
156
    /**
157
     * Array of font hashes associated to FONT records index.
158
     *
159
     * @var array<int|string>
160
     */
161
    public array $fontHashIndex;
162
163
    private bool $preCalculateFormulas;
164
165
    private int $printHeaders;
166
167
    private ?Workbook $writerWorkbook;
168
169
    /**
170
     * Constructor.
171
     *
172
     * @param int $str_total Total number of strings
173
     * @param int $str_unique Total number of unique strings
174
     * @param array<string, int> $str_table String Table
175
     * @param mixed[] $colors Colour Table
176
     * @param Parser $parser The formula parser created for the Workbook
177
     * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written
178
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write
179
     */
180 115
    public function __construct(int &$str_total, int &$str_unique, array &$str_table, array &$colors, Parser $parser, bool $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet, ?Workbook $writerWorkbook = null)
181
    {
182
        // It needs to call its parent's constructor explicitly
183 115
        parent::__construct();
184
185 115
        $this->preCalculateFormulas = $preCalculateFormulas;
186 115
        $this->stringTotal = &$str_total;
187 115
        $this->stringUnique = &$str_unique;
188 115
        $this->stringTable = &$str_table;
189 115
        $this->colors = &$colors;
190 115
        $this->parser = $parser;
191
192 115
        $this->phpSheet = $phpSheet;
193
194 115
        $this->columnInfo = [];
195 115
        $this->activePane = 3;
196
197 115
        $this->printHeaders = 0;
198
199 115
        $this->outlineStyle = false;
200 115
        $this->outlineBelow = true;
201 115
        $this->outlineRight = true;
202 115
        $this->outlineOn = true;
203
204 115
        $this->fontHashIndex = [];
205
206
        // calculate values for DIMENSIONS record
207 115
        $minR = 1;
208 115
        $minC = 'A';
209
210 115
        $maxR = $this->phpSheet->getHighestRow();
211 115
        $maxC = $this->phpSheet->getHighestColumn();
212
213
        // Determine lowest and highest column and row
214 115
        $this->firstRowIndex = $minR;
215 115
        $this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR;
216
217 115
        $this->firstColumnIndex = Coordinate::columnIndexFromString($minC);
218 115
        $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC);
219
220 115
        if ($this->lastColumnIndex > 255) {
221 1
            $this->lastColumnIndex = 255;
222
        }
223 115
        $this->writerWorkbook = $writerWorkbook;
224
    }
225
226
    /**
227
     * Add data to the beginning of the workbook (note the reverse order)
228
     * and to the end of the workbook.
229
     *
230
     * @see Workbook::storeWorkbook
231
     */
232 115
    public function close(): void
233
    {
234 115
        $phpSheet = $this->phpSheet;
235
236
        // Storing selected cells and active sheet because it changes while parsing cells with formulas.
237 115
        $selectedCells = $this->phpSheet->getSelectedCells();
238 115
        $activeSheetIndex = $this->phpSheet->getParentOrThrow()->getActiveSheetIndex();
239
240
        // Write BOF record
241 115
        $this->storeBof(0x0010);
242
243
        // Write PRINTHEADERS
244 115
        $this->writePrintHeaders();
245
246
        // Write PRINTGRIDLINES
247 115
        $this->writePrintGridlines();
248
249
        // Write GRIDSET
250 115
        $this->writeGridset();
251
252
        // Calculate column widths
253 115
        $phpSheet->calculateColumnWidths();
254
255
        // Column dimensions
256 115
        if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) {
257 110
            $defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParentOrThrow()->getDefaultStyle()->getFont());
258
        }
259
260 115
        $columnDimensions = $phpSheet->getColumnDimensions();
261 115
        $maxCol = $this->lastColumnIndex - 1;
262 115
        for ($i = 0; $i <= $maxCol; ++$i) {
263 115
            $hidden = 0;
264 115
            $level = 0;
265 115
            $xfIndex = 15; // there are 15 cell style Xfs
266
267 115
            $width = $defaultWidth;
268
269 115
            $columnLetter = Coordinate::stringFromColumnIndex($i + 1);
270 115
            if (isset($columnDimensions[$columnLetter])) {
271 39
                $columnDimension = $columnDimensions[$columnLetter];
272 39
                if ($columnDimension->getWidth() >= 0) {
273 38
                    $width = $columnDimension->getWidth();
274
                }
275 39
                $hidden = $columnDimension->getVisible() ? 0 : 1;
276 39
                $level = $columnDimension->getOutlineLevel();
277 39
                $xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs
278
            }
279
280
            // Components of columnInfo:
281
            // $firstcol first column on the range
282
            // $lastcol  last column on the range
283
            // $width    width to set
284
            // $xfIndex  The optional cell style Xf index to apply to the columns
285
            // $hidden   The optional hidden atribute
286
            // $level    The optional outline level
287 115
            $this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level];
288
        }
289
290
        // Write GUTS
291 115
        $this->writeGuts();
292
293
        // Write DEFAULTROWHEIGHT
294 115
        $this->writeDefaultRowHeight();
295
        // Write WSBOOL
296 115
        $this->writeWsbool();
297
        // Write horizontal and vertical page breaks
298 115
        $this->writeBreaks();
299
        // Write page header
300 115
        $this->writeHeader();
301
        // Write page footer
302 115
        $this->writeFooter();
303
        // Write page horizontal centering
304 115
        $this->writeHcenter();
305
        // Write page vertical centering
306 115
        $this->writeVcenter();
307
        // Write left margin
308 115
        $this->writeMarginLeft();
309
        // Write right margin
310 115
        $this->writeMarginRight();
311
        // Write top margin
312 115
        $this->writeMarginTop();
313
        // Write bottom margin
314 115
        $this->writeMarginBottom();
315
        // Write page setup
316 115
        $this->writeSetup();
317
        // Write sheet protection
318 115
        $this->writeProtect();
319
        // Write SCENPROTECT
320 115
        $this->writeScenProtect();
321
        // Write OBJECTPROTECT
322 115
        $this->writeObjectProtect();
323
        // Write sheet password
324 115
        $this->writePassword();
325
        // Write DEFCOLWIDTH record
326 115
        $this->writeDefcol();
327
328
        // Write the COLINFO records if they exist
329 115
        if (!empty($this->columnInfo)) {
330 115
            $colcount = count($this->columnInfo);
331 115
            for ($i = 0; $i < $colcount; ++$i) {
332 115
                $this->writeColinfo($this->columnInfo[$i]);
333
            }
334
        }
335 115
        $autoFilterRange = $phpSheet->getAutoFilter()->getRange();
336 115
        if (!empty($autoFilterRange)) {
337
            // Write AUTOFILTERINFO
338 3
            $this->writeAutoFilterInfo();
339
        }
340
341
        // Write sheet dimensions
342 115
        $this->writeDimensions();
343
344
        // Row dimensions
345 115
        foreach ($phpSheet->getRowDimensions() as $rowDimension) {
346 26
            $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs
347 26
            $this->writeRow(
348 26
                $rowDimension->getRowIndex() - 1,
349 26
                (int) $rowDimension->getRowHeight(),
350 26
                $xfIndex,
351 26
                !$rowDimension->getVisible(),
352 26
                $rowDimension->getOutlineLevel()
353 26
            );
354
        }
355
356
        // Write Cells
357 115
        foreach ($phpSheet->getCellCollection()->getSortedCoordinates() as $coordinate) {
358
            /** @var Cell $cell */
359 109
            $cell = $phpSheet->getCellCollection()->get($coordinate);
360 109
            $row = $cell->getRow() - 1;
361 109
            $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
362
363
            // Don't break Excel break the code!
364 109
            if ($row > 65535 || $column > 255) {
365
                throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.');
366
            }
367
368
            // Write cell value
369 109
            $xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs
370
371 109
            $cVal = $cell->getValue();
372 109
            if ($cVal instanceof RichText) {
373 11
                $arrcRun = [];
374 11
                $str_pos = 0;
375 11
                $elements = $cVal->getRichTextElements();
376 11
                foreach ($elements as $element) {
377
                    // FONT Index
378 11
                    $str_fontidx = 0;
379 11
                    if ($element instanceof Run) {
380 11
                        $getFont = $element->getFont();
381 11
                        if ($getFont !== null) {
382 11
                            $str_fontidx = $this->fontHashIndex[$getFont->getHashCode()];
383
                        }
384
                    }
385 11
                    $arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx];
386
                    // Position FROM
387 11
                    $str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8');
388
                }
389
                /** @var array<int, array{strlen: int, fontidx: int}> $arrcRun */
390 11
                $this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun);
391
            } else {
392 108
                switch ($cell->getDatatype()) {
393
                    case DataType::TYPE_STRING:
394
                    case DataType::TYPE_INLINE:
395
                    case DataType::TYPE_NULL:
396 80
                        if ($cVal === '' || $cVal === null) {
397 39
                            $this->writeBlank($row, $column, $xfIndex);
398
                        } else {
399 74
                            $this->writeString($row, $column, $cell->getValueString(), $xfIndex);
400
                        }
401
402 80
                        break;
403
                    case DataType::TYPE_NUMERIC:
404 60
                        $this->writeNumber($row, $column, is_numeric($cVal) ? ($cVal + 0) : 0, $xfIndex);
405
406 60
                        break;
407
                    case DataType::TYPE_FORMULA:
408 42
                        $calculatedValue = $this->preCalculateFormulas ? $cell->getCalculatedValue() : null;
409 42
                        $calculatedValueString = $this->preCalculateFormulas ? $cell->getCalculatedValueString() : '';
410 42
                        if (self::WRITE_FORMULA_EXCEPTION == $this->writeFormula($row, $column, $cell->getValueString(), $xfIndex, $calculatedValue)) {
411 8
                            if ($calculatedValue === null) {
412
                                $calculatedValue = $cell->getCalculatedValue();
413
                            }
414 8
                            $calctype = gettype($calculatedValue);
415 8
                            match ($calctype) {
416 5
                                'integer', 'double' => $this->writeNumber($row, $column, is_numeric($calculatedValue) ? ((float) $calculatedValue) : 0.0, $xfIndex),
417 5
                                'string' => $this->writeString($row, $column, $calculatedValueString, $xfIndex),
418 1
                                'boolean' => $this->writeBoolErr($row, $column, (int) $calculatedValueString, 0, $xfIndex),
419
                                default => $this->writeString($row, $column, $cell->getValueString(), $xfIndex),
420 8
                            };
421
                        }
422
423 41
                        break;
424
                    case DataType::TYPE_BOOL:
425 10
                        $this->writeBoolErr($row, $column, (int) $cell->getValueString(), 0, $xfIndex);
426
427 10
                        break;
428
                    case DataType::TYPE_ERROR:
429 1
                        $this->writeBoolErr($row, $column, ErrorCode::error($cell->getValueString()), 1, $xfIndex);
430
431 1
                        break;
432
                }
433
            }
434
        }
435
436
        // Append
437 114
        $this->writeMsoDrawing();
438
439
        // Restoring active sheet.
440 114
        $this->phpSheet->getParentOrThrow()->setActiveSheetIndex($activeSheetIndex);
441
442
        // Write WINDOW2 record
443 114
        $this->writeWindow2();
444
445
        // Write PLV record
446 114
        $this->writePageLayoutView();
447
448
        // Write ZOOM record
449 114
        $this->writeZoom();
450 114
        if ($phpSheet->getFreezePane()) {
451 9
            $this->writePanes();
452
        }
453
454
        // Restoring selected cells.
455 114
        $this->phpSheet->setSelectedCells($selectedCells);
456
457
        // Write SELECTION record
458 114
        $this->writeSelection();
459
460
        // Write MergedCellsTable Record
461 114
        $this->writeMergedCells();
462
463
        // Hyperlinks
464 114
        $phpParent = $phpSheet->getParent();
465 114
        $hyperlinkbase = ($phpParent === null) ? '' : $phpParent->getProperties()->getHyperlinkBase();
466 114
        foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) {
467 13
            [$column, $row] = Coordinate::indexesFromString($coordinate);
468
469 13
            $url = $hyperlink->getUrl();
470 13
            if ($url[0] === '#') {
471 1
                $url = "internal:$url";
472 13
            } elseif (str_starts_with($url, 'sheet://')) {
473
                // internal to current workbook
474 7
                $url = str_replace('sheet://', 'internal:', $url);
475 13
            } elseif (Preg::isMatch('/^(http:|https:|ftp:|mailto:)/', $url)) {
476
                // URL
477 1
            } elseif (!empty($hyperlinkbase) && !Preg::isMatch('~^([A-Za-z]:)?[/\\\]~', $url)) {
478 1
                $url = "$hyperlinkbase$url";
479 1
                if (!Preg::isMatch('/^(http:|https:|ftp:|mailto:)/', $url)) {
480
                    $url = 'external:' . $url;
481
                }
482
            } else {
483
                // external (local file)
484
                $url = 'external:' . $url;
485
            }
486
487 13
            $this->writeUrl($row - 1, $column - 1, $url);
488
        }
489
490 114
        $this->writeDataValidity();
491 114
        $this->writeSheetLayout();
492
493
        // Write SHEETPROTECTION record
494 114
        $this->writeSheetProtection();
495 114
        $this->writeRangeProtection();
496
497
        // Write Conditional Formatting Rules and Styles
498 114
        $this->writeConditionalFormatting();
499
500 114
        $this->storeEof();
501
    }
502
503
    public const MAX_XLS_COLUMN = 256;
504
    public const MAX_XLS_COLUMN_STRING = 'IV';
505
    public const MAX_XLS_ROW = 65536;
506
507 15
    private static function limitRange(string $exploded): string
508
    {
509 15
        $retVal = '';
510 15
        $ranges = Coordinate::getRangeBoundaries($exploded);
511 15
        $firstCol = Coordinate::columnIndexFromString($ranges[0][0]);
512 15
        $firstRow = (int) $ranges[0][1];
513 15
        if ($firstCol <= self::MAX_XLS_COLUMN && $firstRow <= self::MAX_XLS_ROW) {
514 15
            $retVal = $exploded;
515 15
            if (str_contains($exploded, ':')) {
516 12
                $lastCol = Coordinate::columnIndexFromString($ranges[1][0]);
517 12
                $ranges[1][1] = min(self::MAX_XLS_ROW, (int) $ranges[1][1]);
518 12
                if ($lastCol > self::MAX_XLS_COLUMN) {
519 1
                    $ranges[1][0] = self::MAX_XLS_COLUMN_STRING;
520
                }
521 12
                $retVal = "{$ranges[0][0]}{$ranges[0][1]}:{$ranges[1][0]}{$ranges[1][1]}";
522
            }
523
        }
524
525 15
        return $retVal;
526
    }
527
528 114
    private function writeConditionalFormatting(): void
529
    {
530 114
        $conditionalFormulaHelper = new ConditionalHelper($this->parser);
531
532 114
        $arrConditionalStyles = [];
533 114
        foreach ($this->phpSheet->getConditionalStylesCollection() as $key => $value) {
534 15
            $keyExplode = explode(',', Coordinate::resolveUnionAndIntersection($key));
535 15
            foreach ($keyExplode as $exploded) {
536 15
                $range = self::limitRange($exploded);
537 15
                if ($range !== '') {
538 15
                    $arrConditionalStyles[$range] = $value;
539
                }
540
            }
541
        }
542 114
        if (!empty($arrConditionalStyles)) {
543
            // Write ConditionalFormattingTable records
544 15
            foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) {
545 15
                $cfHeaderWritten = false;
546 15
                foreach ($conditionalStyles as $conditional) {
547
                    /** @var Conditional $conditional */
548
                    if (
549 15
                        $conditional->getConditionType() === Conditional::CONDITION_EXPRESSION
550 15
                        || $conditional->getConditionType() === Conditional::CONDITION_CELLIS
551
                    ) {
552
                        // Write CFHEADER record (only if there are Conditional Styles that we are able to write)
553 11
                        if ($cfHeaderWritten === false) {
554 11
                            $cfHeaderWritten = $this->writeCFHeader($cellCoordinate, $conditionalStyles);
555
                        }
556 11
                        if ($cfHeaderWritten === true) {
557
                            // Write CFRULE record
558 11
                            $this->writeCFRule($conditionalFormulaHelper, $conditional, $cellCoordinate);
559
                        }
560
                    }
561
                }
562
            }
563
        }
564
    }
565
566
    /**
567
     * Write a cell range address in BIFF8
568
     * always fixed range
569
     * See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format.
570
     *
571
     * @param string $range E.g. 'A1' or 'A1:B6'
572
     *
573
     * @return string Binary data
574
     */
575 13
    private function writeBIFF8CellRangeAddressFixed(string $range): string
576
    {
577 13
        $explodes = explode(':', $range);
578
579
        // extract first cell, e.g. 'A1'
580 13
        $firstCell = $explodes[0];
581 13
        if (ctype_alpha($firstCell)) {
582 1
            $firstCell .= '1';
583 13
        } elseif (ctype_digit($firstCell)) {
584 1
            $firstCell = "A$firstCell";
585
        }
586
587
        // extract last cell, e.g. 'B6'
588 13
        if (count($explodes) == 1) {
589 5
            $lastCell = $firstCell;
590
        } else {
591 11
            $lastCell = $explodes[1];
592
        }
593 13
        if (ctype_alpha($lastCell)) {
594 1
            $lastCell .= (string) self::MAX_XLS_ROW;
595 13
        } elseif (ctype_digit($lastCell)) {
596 1
            $lastCell = self::MAX_XLS_COLUMN_STRING . $lastCell;
597
        }
598
599 13
        $firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1]
600 13
        $lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6]
601
602 13
        return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, $firstCellCoordinates[0] - 1, $lastCellCoordinates[0] - 1);
603
    }
604
605
    /**
606
     * Retrieves data from memory in one chunk, or from disk
607
     * sized chunks.
608
     *
609
     * @return string The data
610
     */
611 114
    public function getData(): string
612
    {
613
        // Return data stored in memory
614 114
        if (isset($this->_data)) {
615 114
            $tmp = $this->_data;
616 114
            $this->_data = null;
617
618 114
            return $tmp;
619
        }
620
621
        // No data to return
622
        return '';
623
    }
624
625
    /**
626
     * Set the option to print the row and column headers on the printed page.
627
     *
628
     * @param int $print Whether to print the headers or not. Defaults to 1 (print).
629
     */
630
    public function printRowColHeaders(int $print = 1): void
631
    {
632
        $this->printHeaders = $print;
633
    }
634
635
    /**
636
     * This method sets the properties for outlining and grouping. The defaults
637
     * correspond to Excel's defaults.
638
     */
639
    public function setOutline(bool $visible = true, bool $symbols_below = true, bool $symbols_right = true, bool $auto_style = false): void
640
    {
641
        $this->outlineOn = $visible;
642
        $this->outlineBelow = $symbols_below;
643
        $this->outlineRight = $symbols_right;
644
        $this->outlineStyle = $auto_style;
645
    }
646
647
    /**
648
     * Write a double to the specified row and column (zero indexed).
649
     * An integer can be written as a double. Excel will display an
650
     * integer. $format is optional.
651
     *
652
     * Returns  0 : normal termination
653
     *         -2 : row or column out of range
654
     *
655
     * @param int $row Zero indexed row
656
     * @param int $col Zero indexed column
657
     * @param float $num The number to write
658
     * @param int $xfIndex The optional XF format
659
     */
660 61
    private function writeNumber(int $row, int $col, float $num, int $xfIndex): int
661
    {
662 61
        $record = 0x0203; // Record identifier
663 61
        $length = 0x000E; // Number of bytes to follow
664
665 61
        $header = pack('vv', $record, $length);
666 61
        $data = pack('vvv', $row, $col, $xfIndex);
667 61
        $xl_double = pack('d', $num);
668 61
        if (self::getByteOrder()) { // if it's Big Endian
669
            $xl_double = strrev($xl_double);
670
        }
671
672 61
        $this->append($header . $data . $xl_double);
673
674 61
        return 0;
675
    }
676
677
    /**
678
     * Write a LABELSST record or a LABEL record. Which one depends on BIFF version.
679
     *
680
     * @param int $row Row index (0-based)
681
     * @param int $col Column index (0-based)
682
     * @param string $str The string
683
     * @param int $xfIndex Index to XF record
684
     */
685 75
    private function writeString(int $row, int $col, string $str, int $xfIndex): void
686
    {
687 75
        $this->writeLabelSst($row, $col, $str, $xfIndex);
688
    }
689
690
    /**
691
     * Write a LABELSST record or a LABEL record. Which one depends on BIFF version
692
     * It differs from writeString by the writing of rich text strings.
693
     *
694
     * @param int $row Row index (0-based)
695
     * @param int $col Column index (0-based)
696
     * @param string $str The string
697
     * @param int $xfIndex The XF format index for the cell
698
     * @param array<int, array{strlen: int, fontidx: int}> $arrcRun Index to Font record and characters beginning
699
     */
700 11
    private function writeRichTextString(int $row, int $col, string $str, int $xfIndex, array $arrcRun): void
701
    {
702 11
        $record = 0x00FD; // Record identifier
703 11
        $length = 0x000A; // Bytes to follow
704 11
        $str = StringHelper::UTF8toBIFF8UnicodeShort($str, $arrcRun);
705
706
        // check if string is already present
707 11
        if (!isset($this->stringTable[$str])) {
708 11
            $this->stringTable[$str] = $this->stringUnique++;
709
        }
710 11
        ++$this->stringTotal;
711
712 11
        $header = pack('vv', $record, $length);
713 11
        $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]);
714 11
        $this->append($header . $data);
715
    }
716
717
    /**
718
     * Write a string to the specified row and column (zero indexed).
719
     * This is the BIFF8 version (no 255 chars limit).
720
     * $format is optional.
721
     *
722
     * @param int $row Zero indexed row
723
     * @param int $col Zero indexed column
724
     * @param string $str The string to write
725
     * @param int $xfIndex The XF format index for the cell
726
     */
727 75
    private function writeLabelSst(int $row, int $col, string $str, int $xfIndex): void
728
    {
729 75
        $record = 0x00FD; // Record identifier
730 75
        $length = 0x000A; // Bytes to follow
731
732 75
        $str = StringHelper::UTF8toBIFF8UnicodeLong($str);
733
734
        // check if string is already present
735 75
        if (!isset($this->stringTable[$str])) {
736 75
            $this->stringTable[$str] = $this->stringUnique++;
737
        }
738 75
        ++$this->stringTotal;
739
740 75
        $header = pack('vv', $record, $length);
741 75
        $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]);
742 75
        $this->append($header . $data);
743
    }
744
745
    /**
746
     * Write a blank cell to the specified row and column (zero indexed).
747
     * A blank cell is used to specify formatting without adding a string
748
     * or a number.
749
     *
750
     * A blank cell without a format serves no purpose. Therefore, we don't write
751
     * a BLANK record unless a format is specified.
752
     *
753
     * Returns  0 : normal termination (including no format)
754
     *         -1 : insufficient number of arguments
755
     *         -2 : row or column out of range
756
     *
757
     * @param int $row Zero indexed row
758
     * @param int $col Zero indexed column
759
     * @param int $xfIndex The XF format index
760
     */
761 39
    public function writeBlank(int $row, int $col, int $xfIndex): int
762
    {
763 39
        $record = 0x0201; // Record identifier
764 39
        $length = 0x0006; // Number of bytes to follow
765
766 39
        $header = pack('vv', $record, $length);
767 39
        $data = pack('vvv', $row, $col, $xfIndex);
768 39
        $this->append($header . $data);
769
770 39
        return 0;
771
    }
772
773
    /**
774
     * Write a boolean or an error type to the specified row and column (zero indexed).
775
     *
776
     * @param int $row Row index (0-based)
777
     * @param int $col Column index (0-based)
778
     * @param int $isError Error or Boolean?
779
     */
780 11
    private function writeBoolErr(int $row, int $col, int $value, int $isError, int $xfIndex): int
781
    {
782 11
        $record = 0x0205;
783 11
        $length = 8;
784
785 11
        $header = pack('vv', $record, $length);
786 11
        $data = pack('vvvCC', $row, $col, $xfIndex, $value, $isError);
787 11
        $this->append($header . $data);
788
789 11
        return 0;
790
    }
791
792
    const WRITE_FORMULA_NORMAL = 0;
793
    const WRITE_FORMULA_ERRORS = -1;
794
    const WRITE_FORMULA_RANGE = -2;
795
    const WRITE_FORMULA_EXCEPTION = -3;
796
797
    private static bool $allowThrow = false;
798
799 2
    public static function setAllowThrow(bool $allowThrow): void
800
    {
801 2
        self::$allowThrow = $allowThrow;
802
    }
803
804 2
    public static function getAllowThrow(): bool
805
    {
806 2
        return self::$allowThrow;
807
    }
808
809
    /**
810
     * Write a formula to the specified row and column (zero indexed).
811
     * The textual representation of the formula is passed to the parser in
812
     * Parser.php which returns a packed binary string.
813
     *
814
     * Returns  0 : WRITE_FORMULA_NORMAL  normal termination
815
     *         -1 : WRITE_FORMULA_ERRORS formula errors (bad formula)
816
     *         -2 : WRITE_FORMULA_RANGE  row or column out of range
817
     *         -3 : WRITE_FORMULA_EXCEPTION parse raised exception, probably due to definedname
818
     *
819
     * @param int $row Zero indexed row
820
     * @param int $col Zero indexed column
821
     * @param string $formula The formula text string
822
     * @param int $xfIndex The XF format index
823
     * @param mixed $calculatedValue Calculated value
824
     */
825 42
    private function writeFormula(int $row, int $col, string $formula, int $xfIndex, mixed $calculatedValue): int
826
    {
827 42
        $record = 0x0006; // Record identifier
828
        // Initialize possible additional value for STRING record that should be written after the FORMULA record?
829 42
        $stringValue = null;
830
831
        // calculated value
832 42
        if (isset($calculatedValue)) {
833
            // Since we can't yet get the data type of the calculated value,
834
            // we use best effort to determine data type
835 40
            if (is_bool($calculatedValue)) {
836
                // Boolean value
837 6
                $num = pack('CCCvCv', 0x01, 0x00, (int) $calculatedValue, 0x00, 0x00, 0xFFFF);
838 39
            } elseif (is_int($calculatedValue) || is_float($calculatedValue)) {
839
                // Numeric value
840 32
                $num = pack('d', $calculatedValue);
841 24
            } elseif (is_string($calculatedValue)) {
842 24
                $errorCodes = DataType::getErrorCodes();
843 24
                if (isset($errorCodes[$calculatedValue])) {
844
                    // Error value
845 6
                    $num = pack('CCCvCv', 0x02, 0x00, ErrorCode::error($calculatedValue), 0x00, 0x00, 0xFFFF);
846 22
                } elseif ($calculatedValue === '') {
847
                    // Empty string (and BIFF8)
848 7
                    $num = pack('CCCvCv', 0x03, 0x00, 0x00, 0x00, 0x00, 0xFFFF);
849
                } else {
850
                    // Non-empty string value (or empty string BIFF5)
851 16
                    $stringValue = $calculatedValue;
852 16
                    $num = pack('CCCvCv', 0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFF);
853
                }
854
            } else {
855
                // We are really not supposed to reach here
856
                $num = pack('d', 0x00);
857
            }
858
        } else {
859 2
            $num = pack('d', 0x00);
860
        }
861
862 42
        $grbit = 0x03; // Option flags
863 42
        $unknown = 0x0000; // Must be zero
864
865
        // Strip the '=' or '@' sign at the beginning of the formula string
866 42
        if ($formula[0] == '=') {
867 42
            $formula = substr($formula, 1);
868
        } else {
869
            // Error handling
870
            $this->writeString($row, $col, 'Unrecognised character for formula', 0);
871
872
            return self::WRITE_FORMULA_ERRORS;
873
        }
874
875
        // Parse the formula using the parser in Parser.php
876
        try {
877 42
            $this->parser->parse($formula);
878 42
            $formula = $this->parser->toReversePolish();
879
880 40
            $formlen = strlen($formula); // Length of the binary string
881 40
            $length = 0x16 + $formlen; // Length of the record data
882
883 40
            $header = pack('vv', $record, $length);
884
885 40
            $data = pack('vvv', $row, $col, $xfIndex)
886 40
                . $num
887 40
                . pack('vVv', $grbit, $unknown, $formlen);
888 40
            $this->append($header . $data . $formula);
889
890
            // Append also a STRING record if necessary
891 40
            if ($stringValue !== null) {
892 15
                $this->writeStringRecord($stringValue);
893
            }
894
895 40
            return self::WRITE_FORMULA_NORMAL;
896 9
        } catch (PhpSpreadsheetException $e) {
897 9
            if (self::$allowThrow) {
898 1
                throw $e;
899
            }
900
901 8
            return self::WRITE_FORMULA_EXCEPTION;
902
        }
903
    }
904
905
    /**
906
     * Write a STRING record. This.
907
     */
908 15
    private function writeStringRecord(string $stringValue): void
909
    {
910 15
        $record = 0x0207; // Record identifier
911 15
        $data = StringHelper::UTF8toBIFF8UnicodeLong($stringValue);
912
913 15
        $length = strlen($data);
914 15
        $header = pack('vv', $record, $length);
915
916 15
        $this->append($header . $data);
917
    }
918
919
    /**
920
     * Write a hyperlink.
921
     * This is comprised of two elements: the visible label and
922
     * the invisible link. The visible label is the same as the link unless an
923
     * alternative string is specified. The label is written using the
924
     * writeString() method. Therefore the 255 characters string limit applies.
925
     * $string and $format are optional.
926
     *
927
     * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external
928
     * directory url.
929
     *
930
     * @param int $row Row
931
     * @param int $col Column
932
     * @param string $url URL string
933
     */
934 13
    private function writeUrl(int $row, int $col, string $url): void
935
    {
936
        // Add start row and col to arg list
937 13
        $this->writeUrlRange($row, $col, $row, $col, $url);
938
    }
939
940
    /**
941
     * This is the more general form of writeUrl(). It allows a hyperlink to be
942
     * written to a range of cells. This function also decides the type of hyperlink
943
     * to be written. These are either, Web (http, ftp, mailto), Internal
944
     * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1').
945
     *
946
     * @param int $row1 Start row
947
     * @param int $col1 Start column
948
     * @param int $row2 End row
949
     * @param int $col2 End column
950
     * @param string $url URL string
951
     *
952
     * @see writeUrl()
953
     */
954 13
    private function writeUrlRange(int $row1, int $col1, int $row2, int $col2, string $url): void
955
    {
956
        // Check for internal/external sheet links or default to web link
957 13
        if (Preg::isMatch('[^internal:]', $url)) {
958 8
            $this->writeUrlInternal($row1, $col1, $row2, $col2, $url);
959 13
        } elseif (Preg::isMatch('[^external:]', $url)) {
960
            $this->writeUrlExternal($row1, $col1, $row2, $col2, $url);
961
        } else {
962 13
            $this->writeUrlWeb($row1, $col1, $row2, $col2, $url);
963
        }
964
    }
965
966
    /**
967
     * Used to write http, ftp and mailto hyperlinks.
968
     * The link type ($options) is 0x03 is the same as absolute dir ref without
969
     * sheet. However it is differentiated by the $unknown2 data stream.
970
     *
971
     * @param int $row1 Start row
972
     * @param int $col1 Start column
973
     * @param int $row2 End row
974
     * @param int $col2 End column
975
     * @param string $url URL string
976
     *
977
     * @see writeUrl()
978
     */
979 13
    public function writeUrlWeb(int $row1, int $col1, int $row2, int $col2, string $url): void
980
    {
981 13
        $record = 0x01B8; // Record identifier
982
983
        // Pack the undocumented parts of the hyperlink stream
984 13
        $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
985 13
        $unknown2 = pack('H*', 'E0C9EA79F9BACE118C8200AA004BA90B');
986
987
        // Pack the option flags
988 13
        $options = pack('V', 0x03);
989
990
        // Convert URL to a null terminated wchar string
991
992 13
        $url = implode("\0", Preg::split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
993 13
        $url = $url . "\0\0\0";
994
995
        // Pack the length of the URL
996 13
        $url_len = pack('V', strlen($url));
997
998
        // Calculate the data length
999 13
        $length = 0x34 + strlen($url);
1000
1001
        // Pack the header data
1002 13
        $header = pack('vv', $record, $length);
1003 13
        $data = pack('vvvv', $row1, $row2, $col1, $col2);
1004
1005
        // Write the packed data
1006 13
        $this->append($header . $data . $unknown1 . $options . $unknown2 . $url_len . $url);
1007
    }
1008
1009
    /**
1010
     * Used to write internal reference hyperlinks such as "Sheet1!A1".
1011
     *
1012
     * @param int $row1 Start row
1013
     * @param int $col1 Start column
1014
     * @param int $row2 End row
1015
     * @param int $col2 End column
1016
     * @param string $url URL string
1017
     *
1018
     * @see writeUrl()
1019
     */
1020 8
    private function writeUrlInternal(int $row1, int $col1, int $row2, int $col2, string $url): void
1021
    {
1022 8
        $record = 0x01B8; // Record identifier
1023
1024
        // Strip URL type
1025 8
        $url = Preg::replace('/^internal:/', '', $url);
1026
1027
        // Pack the undocumented parts of the hyperlink stream
1028 8
        $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
1029
1030
        // Pack the option flags
1031 8
        $options = pack('V', 0x08);
1032
1033
        // Convert the URL type and to a null terminated wchar string
1034 8
        $url .= "\0";
1035
1036
        // character count
1037 8
        $url_len = StringHelper::countCharacters($url);
1038 8
        $url_len = pack('V', $url_len);
1039
1040 8
        $url = StringHelper::convertEncoding($url, 'UTF-16LE', 'UTF-8');
1041
1042
        // Calculate the data length
1043 8
        $length = 0x24 + strlen($url);
1044
1045
        // Pack the header data
1046 8
        $header = pack('vv', $record, $length);
1047 8
        $data = pack('vvvv', $row1, $row2, $col1, $col2);
1048
1049
        // Write the packed data
1050 8
        $this->append($header . $data . $unknown1 . $options . $url_len . $url);
1051
    }
1052
1053
    /**
1054
     * Write links to external directory names such as 'c:\foo.xls',
1055
     * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'.
1056
     *
1057
     * Note: Excel writes some relative links with the $dir_long string. We ignore
1058
     * these cases for the sake of simpler code.
1059
     *
1060
     * @param int $row1 Start row
1061
     * @param int $col1 Start column
1062
     * @param int $row2 End row
1063
     * @param int $col2 End column
1064
     * @param string $url URL string
1065
     *
1066
     * @see writeUrl()
1067
     */
1068
    private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, string $url): void
1069
    {
1070
        // Network drives are different. We will handle them separately
1071
        // MS/Novell network drives and shares start with \\
1072
        if (Preg::isMatch('[^external:\\\]', $url)) {
1073
            return;
1074
        }
1075
1076
        $record = 0x01B8; // Record identifier
1077
1078
        // Strip URL type and change Unix dir separator to Dos style (if needed)
1079
        //
1080
        $url = Preg::replace(['/^external:/', '/\//'], ['', '\\'], $url);
1081
1082
        // Determine if the link is relative or absolute:
1083
        //   relative if link contains no dir separator, "somefile.xls"
1084
        //   relative if link starts with up-dir, "..\..\somefile.xls"
1085
        //   otherwise, absolute
1086
1087
        $absolute = 0x00; // relative path
1088
        if (Preg::isMatch('/^[A-Z]:/', $url)) {
1089
            $absolute = 0x02; // absolute path on Windows, e.g. C:\...
1090
        }
1091
        $link_type = 0x01 | $absolute;
1092
1093
        // Determine if the link contains a sheet reference and change some of the
1094
        // parameters accordingly.
1095
        // Split the dir name and sheet name (if it exists)
1096
        $dir_long = $url;
1097
        if (Preg::isMatch('/\#/', $url)) {
1098
            $link_type |= 0x08;
1099
        }
1100
1101
        // Pack the link type
1102
        $link_type = pack('V', $link_type);
1103
1104
        // Calculate the up-level dir count e.g.. (..\..\..\ == 3)
1105
        $up_count = Preg::isMatchAll('/\.\.\\\/', $dir_long, $useless);
1106
        $up_count = pack('v', $up_count);
1107
1108
        // Store the short dos dir name (null terminated)
1109
        $dir_short = Preg::replace('/\.\.\\\/', '', $dir_long) . "\0";
1110
1111
        // Store the long dir name as a wchar string (non-null terminated)
1112
        //$dir_long = $dir_long . "\0";
1113
1114
        // Pack the lengths of the dir strings
1115
        $dir_short_len = pack('V', strlen($dir_short));
1116
        //$dir_long_len = pack('V', strlen($dir_long));
1117
        $stream_len = pack('V', 0); //strlen($dir_long) + 0x06);
1118
1119
        // Pack the undocumented parts of the hyperlink stream
1120
        $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
1121
        $unknown2 = pack('H*', '0303000000000000C000000000000046');
1122
        $unknown3 = pack('H*', 'FFFFADDE000000000000000000000000000000000000000');
1123
        //$unknown4 = pack('v', 0x03);
1124
1125
        // Pack the main data stream
1126
        $data = pack('vvvv', $row1, $row2, $col1, $col2)
1127
            . $unknown1
1128
            . $link_type
1129
            . $unknown2
1130
            . $up_count
1131
            . $dir_short_len
1132
            . $dir_short
1133
            . $unknown3
1134
            . $stream_len; /*.
1135
                          $dir_long_len .
1136
                          $unknown4     .
1137
                          $dir_long     .
1138
                          $sheet_len    .
1139
                          $sheet        ;*/
1140
1141
        // Pack the header data
1142
        $length = strlen($data);
1143
        $header = pack('vv', $record, $length);
1144
1145
        // Write the packed data
1146
        $this->append($header . $data);
1147
    }
1148
1149
    /**
1150
     * This method is used to set the height and format for a row.
1151
     *
1152
     * @param int $row The row to set
1153
     * @param int $height Height we are giving to the row.
1154
     *                        Use null to set XF without setting height
1155
     * @param int $xfIndex The optional cell style Xf index to apply to the columns
1156
     * @param bool $hidden The optional hidden attribute
1157
     * @param int $level The optional outline level for row, in range [0,7]
1158
     */
1159 26
    private function writeRow(int $row, int $height, int $xfIndex, bool $hidden = false, int $level = 0): void
1160
    {
1161 26
        $record = 0x0208; // Record identifier
1162 26
        $length = 0x0010; // Number of bytes to follow
1163
1164 26
        $colMic = 0x0000; // First defined column
1165 26
        $colMac = 0x0000; // Last defined column
1166 26
        $irwMac = 0x0000; // Used by Excel to optimise loading
1167 26
        $reserved = 0x0000; // Reserved
1168 26
        $grbit = 0x0000; // Option flags
1169 26
        $ixfe = $xfIndex;
1170
1171 26
        if ($height < 0) {
1172 8
            $height = null;
1173
        }
1174
1175
        // Use writeRow($row, null, $XF) to set XF format without setting height
1176 26
        if ($height !== null) {
1177 18
            $miyRw = $height * 20; // row height
1178
        } else {
1179 8
            $miyRw = 0xFF; // default row height is 256
1180
        }
1181
1182
        // Set the options flags. fUnsynced is used to show that the font and row
1183
        // heights are not compatible. This is usually the case for WriteExcel.
1184
        // The collapsed flag 0x10 doesn't seem to be used to indicate that a row
1185
        // is collapsed. Instead it is used to indicate that the previous row is
1186
        // collapsed. The zero height flag, 0x20, is used to collapse a row.
1187
1188 26
        $grbit |= $level;
1189 26
        if ($hidden === true) {
1190 7
            $grbit |= 0x0030;
1191
        }
1192 26
        if ($height !== null) {
1193 18
            $grbit |= 0x0040; // fUnsynced
1194
        }
1195 26
        if ($xfIndex !== 0xF) {
1196 3
            $grbit |= 0x0080;
1197
        }
1198 26
        $grbit |= 0x0100;
1199
1200 26
        $header = pack('vv', $record, $length);
1201 26
        $data = pack('vvvvvvvv', $row, $colMic, $colMac, $miyRw, $irwMac, $reserved, $grbit, $ixfe);
1202 26
        $this->append($header . $data);
1203
    }
1204
1205
    /**
1206
     * Writes Excel DIMENSIONS to define the area in which there is data.
1207
     */
1208 115
    private function writeDimensions(): void
1209
    {
1210 115
        $record = 0x0200; // Record identifier
1211
1212 115
        $length = 0x000E;
1213 115
        $data = pack('VVvvv', $this->firstRowIndex, $this->lastRowIndex + 1, $this->firstColumnIndex, $this->lastColumnIndex + 1, 0x0000); // reserved
1214
1215 115
        $header = pack('vv', $record, $length);
1216 115
        $this->append($header . $data);
1217
    }
1218
1219
    /**
1220
     * Write BIFF record Window2.
1221
     */
1222 114
    private function writeWindow2(): void
1223
    {
1224 114
        $record = 0x023E; // Record identifier
1225 114
        $length = 0x0012;
1226
1227 114
        $rwTop = 0x0000; // Top row visible in window
1228 114
        $colLeft = 0x0000; // Leftmost column visible in window
1229
1230
        // The options flags that comprise $grbit
1231 114
        $fDspFmla = 0; // 0 - bit
1232 114
        $fDspGrid = $this->phpSheet->getShowGridlines() ? 1 : 0; // 1
1233 114
        $fDspRwCol = $this->phpSheet->getShowRowColHeaders() ? 1 : 0; // 2
1234 114
        $fFrozen = $this->phpSheet->getFreezePane() ? 1 : 0; // 3
1235 114
        $fDspZeros = 1; // 4
1236 114
        $fDefaultHdr = 1; // 5
1237 114
        $fArabic = $this->phpSheet->getRightToLeft() ? 1 : 0; // 6
1238 114
        $fDspGuts = $this->outlineOn; // 7
1239 114
        $fFrozenNoSplit = 0; // 0 - bit
1240
        // no support in PhpSpreadsheet for selected sheet, therefore sheet is only selected if it is the active sheet
1241 114
        $fSelected = ($this->phpSheet === $this->phpSheet->getParentOrThrow()->getActiveSheet()) ? 1 : 0;
1242 114
        $fPageBreakPreview = $this->phpSheet->getSheetView()->getView() === SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW;
1243
1244 114
        $grbit = $fDspFmla;
1245 114
        $grbit |= $fDspGrid << 1;
1246 114
        $grbit |= $fDspRwCol << 2;
1247 114
        $grbit |= $fFrozen << 3;
1248 114
        $grbit |= $fDspZeros << 4;
1249 114
        $grbit |= $fDefaultHdr << 5;
1250 114
        $grbit |= $fArabic << 6;
1251 114
        $grbit |= $fDspGuts << 7;
1252 114
        $grbit |= $fFrozenNoSplit << 8;
1253 114
        $grbit |= $fSelected << 9; // Selected sheets.
1254 114
        $grbit |= $fSelected << 10; // Active sheet.
1255 114
        $grbit |= $fPageBreakPreview << 11;
1256
1257 114
        $header = pack('vv', $record, $length);
1258 114
        $data = pack('vvv', $grbit, $rwTop, $colLeft);
1259
1260
        // FIXME !!!
1261 114
        $rgbHdr = 0x0040; // Row/column heading and gridline color index
1262 114
        $zoom_factor_page_break = ($fPageBreakPreview ? $this->phpSheet->getSheetView()->getZoomScale() : 0x0000);
1263 114
        $zoom_factor_normal = $this->phpSheet->getSheetView()->getZoomScaleNormal();
1264
1265 114
        $data .= pack('vvvvV', $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000);
1266
1267 114
        $this->append($header . $data);
1268
    }
1269
1270
    /**
1271
     * Write BIFF record DEFAULTROWHEIGHT.
1272
     */
1273 115
    private function writeDefaultRowHeight(): void
1274
    {
1275 115
        $defaultRowHeight = $this->phpSheet->getDefaultRowDimension()->getRowHeight();
1276
1277 115
        if ($defaultRowHeight < 0) {
1278 103
            return;
1279
        }
1280
1281
        // convert to twips
1282 12
        $defaultRowHeight = (int) 20 * $defaultRowHeight;
1283
1284 12
        $record = 0x0225; // Record identifier
1285 12
        $length = 0x0004; // Number of bytes to follow
1286
1287 12
        $header = pack('vv', $record, $length);
1288 12
        $data = pack('vv', 1, $defaultRowHeight);
1289 12
        $this->append($header . $data);
1290
    }
1291
1292
    /**
1293
     * Write BIFF record DEFCOLWIDTH if COLINFO records are in use.
1294
     */
1295 115
    private function writeDefcol(): void
1296
    {
1297 115
        $defaultColWidth = 8;
1298
1299 115
        $record = 0x0055; // Record identifier
1300 115
        $length = 0x0002; // Number of bytes to follow
1301
1302 115
        $header = pack('vv', $record, $length);
1303 115
        $data = pack('v', $defaultColWidth);
1304 115
        $this->append($header . $data);
1305
    }
1306
1307
    /**
1308
     * Write BIFF record COLINFO to define column widths.
1309
     *
1310
     * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C
1311
     * length record.
1312
     *
1313
     * @param array{?int, ?int, ?float, ?int, ?int, ?int} $col_array This is the only parameter received and is composed of the following:
1314
     *                0 => First formatted column,
1315
     *                1 => Last formatted column,
1316
     *                2 => Col width (8.43 is Excel default),
1317
     *                3 => The optional XF format of the column,
1318
     *                4 => Option flags.
1319
     *                5 => Optional outline level
1320
     */
1321 115
    private function writeColinfo(array $col_array): void
1322
    {
1323 115
        $colFirst = $col_array[0] ?? null;
1324 115
        $colLast = $col_array[1] ?? null;
1325 115
        $coldx = $col_array[2] ?? 8.43;
1326 115
        $xfIndex = $col_array[3] ?? 15;
1327 115
        $grbit = $col_array[4] ?? 0;
1328 115
        $level = $col_array[5] ?? 0;
1329
1330 115
        $record = 0x007D; // Record identifier
1331 115
        $length = 0x000C; // Number of bytes to follow
1332
1333 115
        $coldx *= 256; // Convert to units of 1/256 of a char
1334
1335 115
        $ixfe = $xfIndex;
1336 115
        $reserved = 0x0000; // Reserved
1337
1338 115
        $level = max(0, min($level, 7));
1339 115
        $grbit |= $level << 8;
1340
1341 115
        $header = pack('vv', $record, $length);
1342 115
        $data = pack('vvvvvv', $colFirst, $colLast, $coldx, $ixfe, $grbit, $reserved);
1343 115
        $this->append($header . $data);
1344
    }
1345
1346
    /**
1347
     * Write BIFF record SELECTION.
1348
     */
1349 114
    private function writeSelection(): void
1350
    {
1351
        // look up the selected cell range
1352 114
        $selectedCells = Coordinate::splitRange($this->phpSheet->getSelectedCells());
1353 114
        $selectedCells = $selectedCells[0];
1354 114
        if (count($selectedCells) == 2) {
1355 22
            [$first, $last] = $selectedCells;
1356
        } else {
1357 101
            $first = $selectedCells[0];
1358 101
            $last = $selectedCells[0];
1359
        }
1360
1361 114
        [$colFirst, $rwFirst] = Coordinate::coordinateFromString($first);
1362 114
        $colFirst = Coordinate::columnIndexFromString($colFirst) - 1; // base 0 column index
1363 114
        --$rwFirst; // base 0 row index
1364
1365 114
        [$colLast, $rwLast] = Coordinate::coordinateFromString($last);
1366 114
        $colLast = Coordinate::columnIndexFromString($colLast) - 1; // base 0 column index
1367 114
        --$rwLast; // base 0 row index
1368
1369
        // make sure we are not out of bounds
1370 114
        $colFirst = min($colFirst, 255);
1371 114
        $colLast = min($colLast, 255);
1372
1373 114
        $rwFirst = min($rwFirst, 65535);
1374 114
        $rwLast = min($rwLast, 65535);
1375
1376 114
        $record = 0x001D; // Record identifier
1377 114
        $length = 0x000F; // Number of bytes to follow
1378
1379 114
        $pnn = $this->activePane; // Pane position
1380 114
        $rwAct = $rwFirst; // Active row
1381 114
        $colAct = $colFirst; // Active column
1382 114
        $irefAct = 0; // Active cell ref
1383 114
        $cref = 1; // Number of refs
1384
1385
        // Swap last row/col for first row/col as necessary
1386 114
        if ($rwFirst > $rwLast) {
1387
            [$rwFirst, $rwLast] = [$rwLast, $rwFirst];
1388
        }
1389
1390 114
        if ($colFirst > $colLast) {
1391
            [$colFirst, $colLast] = [$colLast, $colFirst];
1392
        }
1393
1394 114
        $header = pack('vv', $record, $length);
1395 114
        $data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast);
1396 114
        $this->append($header . $data);
1397
    }
1398
1399
    /**
1400
     * Store the MERGEDCELLS records for all ranges of merged cells.
1401
     */
1402 114
    private function writeMergedCells(): void
1403
    {
1404 114
        $mergeCells = $this->phpSheet->getMergeCells();
1405 114
        $countMergeCells = count($mergeCells);
1406
1407 114
        if ($countMergeCells == 0) {
1408 111
            return;
1409
        }
1410
1411
        // maximum allowed number of merged cells per record
1412 13
        $maxCountMergeCellsPerRecord = 1027;
1413
1414
        // record identifier
1415 13
        $record = 0x00E5;
1416
1417
        // counter for total number of merged cells treated so far by the writer
1418 13
        $i = 0;
1419
1420
        // counter for number of merged cells written in record currently being written
1421 13
        $j = 0;
1422
1423
        // initialize record data
1424 13
        $recordData = '';
1425
1426
        // loop through the merged cells
1427 13
        foreach ($mergeCells as $mergeCell) {
1428 13
            ++$i;
1429 13
            ++$j;
1430
1431
            // extract the row and column indexes
1432 13
            $range = Coordinate::splitRange($mergeCell);
1433 13
            [$first, $last] = $range[0];
1434 13
            [$firstColumn, $firstRow] = Coordinate::indexesFromString($first);
1435 13
            [$lastColumn, $lastRow] = Coordinate::indexesFromString($last);
1436
1437 13
            $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, $firstColumn - 1, $lastColumn - 1);
1438
1439
            // flush record if we have reached limit for number of merged cells, or reached final merged cell
1440 13
            if ($j == $maxCountMergeCellsPerRecord || $i == $countMergeCells) {
1441 13
                $recordData = pack('v', $j) . $recordData;
1442 13
                $length = strlen($recordData);
1443 13
                $header = pack('vv', $record, $length);
1444 13
                $this->append($header . $recordData);
1445
1446
                // initialize for next record, if any
1447 13
                $recordData = '';
1448 13
                $j = 0;
1449
            }
1450
        }
1451
    }
1452
1453
    /**
1454
     * Write SHEETLAYOUT record.
1455
     */
1456 114
    private function writeSheetLayout(): void
1457
    {
1458 114
        if (!$this->phpSheet->isTabColorSet()) {
1459 114
            return;
1460
        }
1461
1462 6
        $recordData = pack(
1463 6
            'vvVVVvv',
1464 6
            0x0862,
1465 6
            0x0000, // unused
1466 6
            0x00000000, // unused
1467 6
            0x00000000, // unused
1468 6
            0x00000014, // size of record data
1469 6
            $this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index
1470 6
            0x0000        // unused
1471 6
        );
1472
1473 6
        $length = strlen($recordData);
1474
1475 6
        $record = 0x0862; // Record identifier
1476 6
        $header = pack('vv', $record, $length);
1477 6
        $this->append($header . $recordData);
1478
    }
1479
1480 114
    private static function protectionBitsDefaultFalse(?bool $value, int $shift): int
1481
    {
1482 114
        if ($value === false) {
1483 6
            return 1 << $shift;
1484
        }
1485
1486 112
        return 0;
1487
    }
1488
1489 114
    private static function protectionBitsDefaultTrue(?bool $value, int $shift): int
1490
    {
1491 114
        if ($value !== false) {
1492 114
            return 1 << $shift;
1493
        }
1494
1495 2
        return 0;
1496
    }
1497
1498
    /**
1499
     * Write SHEETPROTECTION.
1500
     */
1501 114
    private function writeSheetProtection(): void
1502
    {
1503
        // record identifier
1504 114
        $record = 0x0867;
1505
1506
        // prepare options
1507 114
        $protection = $this->phpSheet->getProtection();
1508 114
        $options = self::protectionBitsDefaultTrue($protection->getObjects(), 0)
1509 114
            | self::protectionBitsDefaultTrue($protection->getScenarios(), 1)
1510 114
            | self::protectionBitsDefaultFalse($protection->getFormatCells(), 2)
1511 114
            | self::protectionBitsDefaultFalse($protection->getFormatColumns(), 3)
1512 114
            | self::protectionBitsDefaultFalse($protection->getFormatRows(), 4)
1513 114
            | self::protectionBitsDefaultFalse($protection->getInsertColumns(), 5)
1514 114
            | self::protectionBitsDefaultFalse($protection->getInsertRows(), 6)
1515 114
            | self::protectionBitsDefaultFalse($protection->getInsertHyperlinks(), 7)
1516 114
            | self::protectionBitsDefaultFalse($protection->getDeleteColumns(), 8)
1517 114
            | self::protectionBitsDefaultFalse($protection->getDeleteRows(), 9)
1518 114
            | self::protectionBitsDefaultTrue($protection->getSelectLockedCells(), 10)
1519 114
            | self::protectionBitsDefaultFalse($protection->getSort(), 11)
1520 114
            | self::protectionBitsDefaultFalse($protection->getAutoFilter(), 12)
1521 114
            | self::protectionBitsDefaultFalse($protection->getPivotTables(), 13)
1522 114
            | self::protectionBitsDefaultTrue($protection->getSelectUnlockedCells(), 14);
1523
1524
        // record data
1525 114
        $recordData = pack(
1526 114
            'vVVCVVvv',
1527 114
            0x0867, // repeated record identifier
1528 114
            0x0000, // not used
1529 114
            0x0000, // not used
1530 114
            0x00, // not used
1531 114
            0x01000200, // unknown data
1532 114
            0xFFFFFFFF, // unknown data
1533 114
            $options, // options
1534 114
            0x0000 // not used
1535 114
        );
1536
1537 114
        $length = strlen($recordData);
1538 114
        $header = pack('vv', $record, $length);
1539
1540 114
        $this->append($header . $recordData);
1541
    }
1542
1543
    /**
1544
     * Write BIFF record RANGEPROTECTION.
1545
     *
1546
     * Openoffice.org's Documentation of the Microsoft Excel File Format uses term RANGEPROTECTION for these records
1547
     * Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records
1548
     */
1549 114
    private function writeRangeProtection(): void
1550
    {
1551 114
        foreach ($this->phpSheet->getProtectedCellRanges() as $range => $protectedCells) {
1552 8
            $password = $protectedCells->getPassword();
1553
            // number of ranges, e.g. 'A1:B3 C20:D25'
1554 8
            $cellRanges = explode(' ', $range);
1555 8
            $cref = count($cellRanges);
1556
1557 8
            $recordData = pack(
1558 8
                'vvVVvCVvVv',
1559 8
                0x0868,
1560 8
                0x00,
1561 8
                0x0000,
1562 8
                0x0000,
1563 8
                0x02,
1564 8
                0x0,
1565 8
                0x0000,
1566 8
                $cref,
1567 8
                0x0000,
1568 8
                0x00
1569 8
            );
1570
1571 8
            foreach ($cellRanges as $cellRange) {
1572 8
                $recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange);
1573
            }
1574
1575
            // the rgbFeat structure
1576 8
            $recordData .= pack(
1577 8
                'VV',
1578 8
                0x0000,
1579 8
                hexdec($password)
1580 8
            );
1581
1582 8
            $recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData));
1583
1584 8
            $length = strlen($recordData);
1585
1586 8
            $record = 0x0868; // Record identifier
1587 8
            $header = pack('vv', $record, $length);
1588 8
            $this->append($header . $recordData);
1589
        }
1590
    }
1591
1592
    /**
1593
     * Writes the Excel BIFF PANE record.
1594
     * The panes can either be frozen or thawed (unfrozen).
1595
     * Frozen panes are specified in terms of an integer number of rows and columns.
1596
     * Thawed panes are specified in terms of Excel's units for rows and columns.
1597
     */
1598 9
    private function writePanes(): void
1599
    {
1600 9
        if (!$this->phpSheet->getFreezePane()) {
1601
            // thaw panes
1602
            return;
1603
        }
1604
1605 9
        [$column, $row] = Coordinate::indexesFromString($this->phpSheet->getFreezePane());
1606 9
        $x = $column - 1;
1607 9
        $y = $row - 1;
1608
1609 9
        [$leftMostColumn, $topRow] = Coordinate::indexesFromString($this->phpSheet->getTopLeftCell() ?? '');
1610
        //Coordinates are zero-based in xls files
1611 9
        $rwTop = $topRow - 1;
1612 9
        $colLeft = $leftMostColumn - 1;
1613
1614 9
        $record = 0x0041; // Record identifier
1615 9
        $length = 0x000A; // Number of bytes to follow
1616
1617
        // Determine which pane should be active. There is also the undocumented
1618
        // option to override this should it be necessary: may be removed later.
1619 9
        $pnnAct = 0;
1620 9
        if ($x != 0 && $y != 0) {
1621 3
            $pnnAct = 0; // Bottom right
1622
        }
1623 9
        if ($x != 0 && $y == 0) {
1624
            $pnnAct = 1; // Top right
1625
        }
1626 9
        if ($x == 0 && $y != 0) {
1627 6
            $pnnAct = 2; // Bottom left
1628
        }
1629 9
        if ($x == 0 && $y == 0) {
1630
            $pnnAct = 3; // Top left
1631
        }
1632
1633 9
        $this->activePane = $pnnAct; // Used in writeSelection
1634
1635 9
        $header = pack('vv', $record, $length);
1636 9
        $data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct);
1637 9
        $this->append($header . $data);
1638
    }
1639
1640
    /**
1641
     * Store the page setup SETUP BIFF record.
1642
     */
1643 115
    private function writeSetup(): void
1644
    {
1645 115
        $record = 0x00A1; // Record identifier
1646 115
        $length = 0x0022; // Number of bytes to follow
1647
1648 115
        $iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size
1649 115
        $iScale = $this->phpSheet->getPageSetup()->getScale() ?: 100; // Print scaling factor
1650
1651 115
        $iPageStart = 0x01; // Starting page number
1652 115
        $iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide
1653 115
        $iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high
1654 115
        $iRes = 0x0258; // Print resolution
1655 115
        $iVRes = 0x0258; // Vertical print resolution
1656
1657 115
        $numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin
1658
1659 115
        $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin
1660 115
        $iCopies = 0x01; // Number of copies
1661
1662
        // Order of printing pages
1663 115
        $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() === PageSetup::PAGEORDER_DOWN_THEN_OVER
1664 115
            ? 0x0 : 0x1;
1665
        // Page orientation
1666 115
        $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE)
1667 115
            ? 0x0 : 0x1;
1668
1669 115
        $fNoPls = 0x0; // Setup not read from printer
1670 115
        $fNoColor = 0x0; // Print black and white
1671 115
        $fDraft = 0x0; // Print draft quality
1672 115
        $fNotes = 0x0; // Print notes
1673 115
        $fNoOrient = 0x0; // Orientation not set
1674 115
        $fUsePage = 0x0; // Use custom starting page
1675
1676 115
        $grbit = $fLeftToRight;
1677 115
        $grbit |= $fLandscape << 1;
1678 115
        $grbit |= $fNoPls << 2;
1679 115
        $grbit |= $fNoColor << 3;
1680 115
        $grbit |= $fDraft << 4;
1681 115
        $grbit |= $fNotes << 5;
1682 115
        $grbit |= $fNoOrient << 6;
1683 115
        $grbit |= $fUsePage << 7;
1684
1685 115
        $numHdr = pack('d', $numHdr);
1686 115
        $numFtr = pack('d', $numFtr);
1687 115
        if (self::getByteOrder()) { // if it's Big Endian
1688
            $numHdr = strrev($numHdr);
1689
            $numFtr = strrev($numFtr);
1690
        }
1691
1692 115
        $header = pack('vv', $record, $length);
1693 115
        $data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes);
1694 115
        $data2 = $numHdr . $numFtr;
1695 115
        $data3 = pack('v', $iCopies);
1696 115
        $this->append($header . $data1 . $data2 . $data3);
1697
    }
1698
1699
    /**
1700
     * Store the header caption BIFF record.
1701
     */
1702 115
    private function writeHeader(): void
1703
    {
1704 115
        $record = 0x0014; // Record identifier
1705
1706
        /* removing for now
1707
        // need to fix character count (multibyte!)
1708
        if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) {
1709
            $str      = $this->phpSheet->getHeaderFooter()->getOddHeader();       // header string
1710
        } else {
1711
            $str = '';
1712
        }
1713
        */
1714
1715 115
        $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader());
1716 115
        $length = strlen($recordData);
1717
1718 115
        $header = pack('vv', $record, $length);
1719
1720 115
        $this->append($header . $recordData);
1721
    }
1722
1723
    /**
1724
     * Store the footer caption BIFF record.
1725
     */
1726 115
    private function writeFooter(): void
1727
    {
1728 115
        $record = 0x0015; // Record identifier
1729
1730
        /* removing for now
1731
        // need to fix character count (multibyte!)
1732
        if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) {
1733
            $str = $this->phpSheet->getHeaderFooter()->getOddFooter();
1734
        } else {
1735
            $str = '';
1736
        }
1737
        */
1738
1739 115
        $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter());
1740 115
        $length = strlen($recordData);
1741
1742 115
        $header = pack('vv', $record, $length);
1743
1744 115
        $this->append($header . $recordData);
1745
    }
1746
1747
    /**
1748
     * Store the horizontal centering HCENTER BIFF record.
1749
     */
1750 115
    private function writeHcenter(): void
1751
    {
1752 115
        $record = 0x0083; // Record identifier
1753 115
        $length = 0x0002; // Bytes to follow
1754
1755 115
        $fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering
1756
1757 115
        $header = pack('vv', $record, $length);
1758 115
        $data = pack('v', $fHCenter);
1759
1760 115
        $this->append($header . $data);
1761
    }
1762
1763
    /**
1764
     * Store the vertical centering VCENTER BIFF record.
1765
     */
1766 115
    private function writeVcenter(): void
1767
    {
1768 115
        $record = 0x0084; // Record identifier
1769 115
        $length = 0x0002; // Bytes to follow
1770
1771 115
        $fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering
1772
1773 115
        $header = pack('vv', $record, $length);
1774 115
        $data = pack('v', $fVCenter);
1775 115
        $this->append($header . $data);
1776
    }
1777
1778
    /**
1779
     * Store the LEFTMARGIN BIFF record.
1780
     */
1781 115
    private function writeMarginLeft(): void
1782
    {
1783 115
        $record = 0x0026; // Record identifier
1784 115
        $length = 0x0008; // Bytes to follow
1785
1786 115
        $margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches
1787
1788 115
        $header = pack('vv', $record, $length);
1789 115
        $data = pack('d', $margin);
1790 115
        if (self::getByteOrder()) { // if it's Big Endian
1791
            $data = strrev($data);
1792
        }
1793
1794 115
        $this->append($header . $data);
1795
    }
1796
1797
    /**
1798
     * Store the RIGHTMARGIN BIFF record.
1799
     */
1800 115
    private function writeMarginRight(): void
1801
    {
1802 115
        $record = 0x0027; // Record identifier
1803 115
        $length = 0x0008; // Bytes to follow
1804
1805 115
        $margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches
1806
1807 115
        $header = pack('vv', $record, $length);
1808 115
        $data = pack('d', $margin);
1809 115
        if (self::getByteOrder()) { // if it's Big Endian
1810
            $data = strrev($data);
1811
        }
1812
1813 115
        $this->append($header . $data);
1814
    }
1815
1816
    /**
1817
     * Store the TOPMARGIN BIFF record.
1818
     */
1819 115
    private function writeMarginTop(): void
1820
    {
1821 115
        $record = 0x0028; // Record identifier
1822 115
        $length = 0x0008; // Bytes to follow
1823
1824 115
        $margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches
1825
1826 115
        $header = pack('vv', $record, $length);
1827 115
        $data = pack('d', $margin);
1828 115
        if (self::getByteOrder()) { // if it's Big Endian
1829
            $data = strrev($data);
1830
        }
1831
1832 115
        $this->append($header . $data);
1833
    }
1834
1835
    /**
1836
     * Store the BOTTOMMARGIN BIFF record.
1837
     */
1838 115
    private function writeMarginBottom(): void
1839
    {
1840 115
        $record = 0x0029; // Record identifier
1841 115
        $length = 0x0008; // Bytes to follow
1842
1843 115
        $margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches
1844
1845 115
        $header = pack('vv', $record, $length);
1846 115
        $data = pack('d', $margin);
1847 115
        if (self::getByteOrder()) { // if it's Big Endian
1848
            $data = strrev($data);
1849
        }
1850
1851 115
        $this->append($header . $data);
1852
    }
1853
1854
    /**
1855
     * Write the PRINTHEADERS BIFF record.
1856
     */
1857 115
    private function writePrintHeaders(): void
1858
    {
1859 115
        $record = 0x002A; // Record identifier
1860 115
        $length = 0x0002; // Bytes to follow
1861
1862 115
        $fPrintRwCol = $this->printHeaders; // Boolean flag
1863
1864 115
        $header = pack('vv', $record, $length);
1865 115
        $data = pack('v', $fPrintRwCol);
1866 115
        $this->append($header . $data);
1867
    }
1868
1869
    /**
1870
     * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the
1871
     * GRIDSET record.
1872
     */
1873 115
    private function writePrintGridlines(): void
1874
    {
1875 115
        $record = 0x002B; // Record identifier
1876 115
        $length = 0x0002; // Bytes to follow
1877
1878 115
        $fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag
1879
1880 115
        $header = pack('vv', $record, $length);
1881 115
        $data = pack('v', $fPrintGrid);
1882 115
        $this->append($header . $data);
1883
    }
1884
1885
    /**
1886
     * Write the GRIDSET BIFF record. Must be used in conjunction with the
1887
     * PRINTGRIDLINES record.
1888
     */
1889 115
    private function writeGridset(): void
1890
    {
1891 115
        $record = 0x0082; // Record identifier
1892 115
        $length = 0x0002; // Bytes to follow
1893
1894 115
        $fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag
1895
1896 115
        $header = pack('vv', $record, $length);
1897 115
        $data = pack('v', $fGridSet);
1898 115
        $this->append($header . $data);
1899
    }
1900
1901
    /**
1902
     * Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet.
1903
     */
1904 3
    private function writeAutoFilterInfo(): void
1905
    {
1906 3
        $record = 0x009D; // Record identifier
1907 3
        $length = 0x0002; // Bytes to follow
1908
1909 3
        $rangeBounds = Coordinate::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange());
1910 3
        $iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0];
1911
1912 3
        $header = pack('vv', $record, $length);
1913 3
        $data = pack('v', $iNumFilters);
1914 3
        $this->append($header . $data);
1915
    }
1916
1917
    /**
1918
     * Write the GUTS BIFF record. This is used to configure the gutter margins
1919
     * where Excel outline symbols are displayed. The visibility of the gutters is
1920
     * controlled by a flag in WSBOOL.
1921
     *
1922
     * @see writeWsbool()
1923
     */
1924 115
    private function writeGuts(): void
1925
    {
1926 115
        $record = 0x0080; // Record identifier
1927 115
        $length = 0x0008; // Bytes to follow
1928
1929 115
        $dxRwGut = 0x0000; // Size of row gutter
1930 115
        $dxColGut = 0x0000; // Size of col gutter
1931
1932
        // determine maximum row outline level
1933 115
        $maxRowOutlineLevel = 0;
1934 115
        foreach ($this->phpSheet->getRowDimensions() as $rowDimension) {
1935 26
            $maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel());
1936
        }
1937
1938 115
        $col_level = 0;
1939
1940
        // Calculate the maximum column outline level. The equivalent calculation
1941
        // for the row outline level is carried out in writeRow().
1942 115
        $colcount = count($this->columnInfo);
1943 115
        for ($i = 0; $i < $colcount; ++$i) {
1944 115
            $col_level = max($this->columnInfo[$i][5], $col_level);
1945
        }
1946
1947
        // Set the limits for the outline levels (0 <= x <= 7).
1948 115
        $col_level = max(0, min($col_level, 7));
1949
1950
        // The displayed level is one greater than the max outline levels
1951 115
        if ($maxRowOutlineLevel) {
1952
            ++$maxRowOutlineLevel;
1953
        }
1954 115
        if ($col_level) {
1955 1
            ++$col_level;
1956
        }
1957
1958 115
        $header = pack('vv', $record, $length);
1959 115
        $data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level);
1960
1961 115
        $this->append($header . $data);
1962
    }
1963
1964
    /**
1965
     * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction
1966
     * with the SETUP record.
1967
     */
1968 115
    private function writeWsbool(): void
1969
    {
1970 115
        $record = 0x0081; // Record identifier
1971 115
        $length = 0x0002; // Bytes to follow
1972 115
        $grbit = 0x0000;
1973
1974
        // The only option that is of interest is the flag for fit to page. So we
1975
        // set all the options in one go.
1976
        //
1977
        // Set the option flags
1978 115
        $grbit |= 0x0001; // Auto page breaks visible
1979 115
        if ($this->outlineStyle) {
1980
            $grbit |= 0x0020; // Auto outline styles
1981
        }
1982 115
        if ($this->phpSheet->getShowSummaryBelow()) {
1983 115
            $grbit |= 0x0040; // Outline summary below
1984
        }
1985 115
        if ($this->phpSheet->getShowSummaryRight()) {
1986 115
            $grbit |= 0x0080; // Outline summary right
1987
        }
1988 115
        if ($this->phpSheet->getPageSetup()->getFitToPage()) {
1989
            $grbit |= 0x0100; // Page setup fit to page
1990
        }
1991 115
        if ($this->outlineOn) {
1992 115
            $grbit |= 0x0400; // Outline symbols displayed
1993
        }
1994
1995 115
        $header = pack('vv', $record, $length);
1996 115
        $data = pack('v', $grbit);
1997 115
        $this->append($header . $data);
1998
    }
1999
2000
    /**
2001
     * Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records.
2002
     */
2003 115
    private function writeBreaks(): void
2004
    {
2005
        // initialize
2006 115
        $vbreaks = [];
2007 115
        $hbreaks = [];
2008
2009 115
        foreach ($this->phpSheet->getRowBreaks() as $cell => $break) {
2010
            // Fetch coordinates
2011 5
            $coordinates = Coordinate::coordinateFromString($cell);
2012 5
            $hbreaks[] = $coordinates[1];
2013
        }
2014 115
        foreach ($this->phpSheet->getColumnBreaks() as $cell => $break) {
2015
            // Fetch coordinates
2016 3
            $coordinates = Coordinate::indexesFromString($cell);
2017 3
            $vbreaks[] = $coordinates[0] - 1;
2018
        }
2019
2020
        //horizontal page breaks
2021 115
        if (!empty($hbreaks)) {
2022
            // Sort and filter array of page breaks
2023 5
            sort($hbreaks, SORT_NUMERIC);
2024 5
            if ($hbreaks[0] == 0) { // don't use first break if it's 0
2025
                array_shift($hbreaks);
2026
            }
2027
2028 5
            $record = 0x001B; // Record identifier
2029 5
            $cbrk = count($hbreaks); // Number of page breaks
2030 5
            $length = 2 + 6 * $cbrk; // Bytes to follow
2031
2032 5
            $header = pack('vv', $record, $length);
2033 5
            $data = pack('v', $cbrk);
2034
2035
            // Append each page break
2036 5
            foreach ($hbreaks as $hbreak) {
2037 5
                $data .= pack('vvv', $hbreak, 0x0000, 0x00FF);
2038
            }
2039
2040 5
            $this->append($header . $data);
2041
        }
2042
2043
        // vertical page breaks
2044 115
        if (!empty($vbreaks)) {
2045
            // 1000 vertical pagebreaks appears to be an internal Excel 5 limit.
2046
            // It is slightly higher in Excel 97/200, approx. 1026
2047 3
            $vbreaks = array_slice($vbreaks, 0, 1000);
2048
2049
            // Sort and filter array of page breaks
2050 3
            sort($vbreaks, SORT_NUMERIC);
2051 3
            if ($vbreaks[0] == 0) { // don't use first break if it's 0
2052
                array_shift($vbreaks);
2053
            }
2054
2055 3
            $record = 0x001A; // Record identifier
2056 3
            $cbrk = count($vbreaks); // Number of page breaks
2057 3
            $length = 2 + 6 * $cbrk; // Bytes to follow
2058
2059 3
            $header = pack('vv', $record, $length);
2060 3
            $data = pack('v', $cbrk);
2061
2062
            // Append each page break
2063 3
            foreach ($vbreaks as $vbreak) {
2064 3
                $data .= pack('vvv', $vbreak, 0x0000, 0xFFFF);
2065
            }
2066
2067 3
            $this->append($header . $data);
2068
        }
2069
    }
2070
2071
    /**
2072
     * Set the Biff PROTECT record to indicate that the worksheet is protected.
2073
     */
2074 115
    private function writeProtect(): void
2075
    {
2076
        // Exit unless sheet protection has been specified
2077 115
        if ($this->phpSheet->getProtection()->getSheet() !== true) {
2078 108
            return;
2079
        }
2080
2081 13
        $record = 0x0012; // Record identifier
2082 13
        $length = 0x0002; // Bytes to follow
2083
2084 13
        $fLock = 1; // Worksheet is protected
2085
2086 13
        $header = pack('vv', $record, $length);
2087 13
        $data = pack('v', $fLock);
2088
2089 13
        $this->append($header . $data);
2090
    }
2091
2092
    /**
2093
     * Write SCENPROTECT.
2094
     */
2095 115
    private function writeScenProtect(): void
2096
    {
2097
        // Exit if sheet protection is not active
2098 115
        if ($this->phpSheet->getProtection()->getSheet() !== true) {
2099 108
            return;
2100
        }
2101
2102
        // Exit if scenarios are not protected
2103 13
        if ($this->phpSheet->getProtection()->getScenarios() !== true) {
2104 13
            return;
2105
        }
2106
2107 1
        $record = 0x00DD; // Record identifier
2108 1
        $length = 0x0002; // Bytes to follow
2109
2110 1
        $header = pack('vv', $record, $length);
2111 1
        $data = pack('v', 1);
2112
2113 1
        $this->append($header . $data);
2114
    }
2115
2116
    /**
2117
     * Write OBJECTPROTECT.
2118
     */
2119 115
    private function writeObjectProtect(): void
2120
    {
2121
        // Exit if sheet protection is not active
2122 115
        if ($this->phpSheet->getProtection()->getSheet() !== true) {
2123 108
            return;
2124
        }
2125
2126
        // Exit if objects are not protected
2127 13
        if ($this->phpSheet->getProtection()->getObjects() !== true) {
2128 10
            return;
2129
        }
2130
2131 4
        $record = 0x0063; // Record identifier
2132 4
        $length = 0x0002; // Bytes to follow
2133
2134 4
        $header = pack('vv', $record, $length);
2135 4
        $data = pack('v', 1);
2136
2137 4
        $this->append($header . $data);
2138
    }
2139
2140
    /**
2141
     * Write the worksheet PASSWORD record.
2142
     */
2143 115
    private function writePassword(): void
2144
    {
2145
        // Exit unless sheet protection and password have been specified
2146 115
        if ($this->phpSheet->getProtection()->getSheet() !== true || !$this->phpSheet->getProtection()->getPassword() || $this->phpSheet->getProtection()->getAlgorithm() !== '') {
2147 110
            return;
2148
        }
2149
2150 5
        $record = 0x0013; // Record identifier
2151 5
        $length = 0x0002; // Bytes to follow
2152
2153 5
        $wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password
2154
2155 5
        $header = pack('vv', $record, $length);
2156 5
        $data = pack('v', $wPassword);
2157
2158 5
        $this->append($header . $data);
2159
    }
2160
2161
    /**
2162
     * Insert a 24bit bitmap image in a worksheet.
2163
     *
2164
     * @param int $row The row we are going to insert the bitmap into
2165
     * @param int $col The column we are going to insert the bitmap into
2166
     * @param GdImage|string $bitmap The bitmap filename or GD-image resource
2167
     * @param int $x the horizontal position (offset) of the image inside the cell
2168
     * @param int $y the vertical position (offset) of the image inside the cell
2169
     * @param float $scale_x The horizontal scale
2170
     * @param float $scale_y The vertical scale
2171
     */
2172
    public function insertBitmap(int $row, int $col, GdImage|string $bitmap, int $x = 0, int $y = 0, float $scale_x = 1, float $scale_y = 1): void
2173
    {
2174
        $bitmap_array = $bitmap instanceof GdImage
2175
            ? $this->processBitmapGd($bitmap)
2176
            : $this->processBitmap($bitmap);
2177
        [$width, $height, $size, $data] = $bitmap_array;
2178
        /** @var int $width */
2179
        /** @var int $height */
2180
        /** @var int $size */
2181
        /** @var string $data */
2182
2183
        // Scale the frame of the image.
2184
        $width *= $scale_x;
2185
        $height *= $scale_y;
2186
2187
        // Calculate the vertices of the image and write the OBJ record
2188
        $this->positionImage($col, $row, $x, $y, (int) $width, (int) $height);
2189
2190
        // Write the IMDATA record to store the bitmap data
2191
        $record = 0x007F;
2192
        $length = 8 + $size;
2193
        $cf = 0x09;
2194
        $env = 0x01;
2195
        $lcb = $size;
2196
2197
        $header = pack('vvvvV', $record, $length, $cf, $env, $lcb);
2198
        $this->append($header . $data);
2199
    }
2200
2201
    /**
2202
     * Calculate the vertices that define the position of the image as required by
2203
     * the OBJ record.
2204
     *
2205
     *         +------------+------------+
2206
     *         |     A      |      B     |
2207
     *   +-----+------------+------------+
2208
     *   |     |(x1,y1)     |            |
2209
     *   |  1  |(A1)._______|______      |
2210
     *   |     |    |              |     |
2211
     *   |     |    |              |     |
2212
     *   +-----+----|    BITMAP    |-----+
2213
     *   |     |    |              |     |
2214
     *   |  2  |    |______________.     |
2215
     *   |     |            |        (B2)|
2216
     *   |     |            |     (x2,y2)|
2217
     *   +---- +------------+------------+
2218
     *
2219
     * Example of a bitmap that covers some of the area from cell A1 to cell B2.
2220
     *
2221
     * Based on the width and height of the bitmap we need to calculate 8 vars:
2222
     *     $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
2223
     * The width and height of the cells are also variable and have to be taken into
2224
     * account.
2225
     * The values of $col_start and $row_start are passed in from the calling
2226
     * function. The values of $col_end and $row_end are calculated by subtracting
2227
     * the width and height of the bitmap from the width and height of the
2228
     * underlying cells.
2229
     * The vertices are expressed as a percentage of the underlying cell width as
2230
     * follows (rhs values are in pixels):
2231
     *
2232
     *       x1 = X / W *1024
2233
     *       y1 = Y / H *256
2234
     *       x2 = (X-1) / W *1024
2235
     *       y2 = (Y-1) / H *256
2236
     *
2237
     *       Where:  X is distance from the left side of the underlying cell
2238
     *               Y is distance from the top of the underlying cell
2239
     *               W is the width of the cell
2240
     *               H is the height of the cell
2241
     * The SDK incorrectly states that the height should be expressed as a
2242
     *        percentage of 1024.
2243
     *
2244
     * @param int $col_start Col containing upper left corner of object
2245
     * @param int $row_start Row containing top left corner of object
2246
     * @param int $x1 Distance to left side of object
2247
     * @param int $y1 Distance to top of object
2248
     * @param int $width Width of image frame
2249
     * @param int $height Height of image frame
2250
     */
2251
    public function positionImage(int $col_start, int $row_start, int $x1, int $y1, int $width, int $height): void
2252
    {
2253
        // Initialise end cell to the same as the start cell
2254
        $col_end = $col_start; // Col containing lower right corner of object
2255
        $row_end = $row_start; // Row containing bottom right corner of object
2256
2257
        // Zero the specified offset if greater than the cell dimensions
2258
        if ($x1 >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1))) {
2259
            $x1 = 0;
2260
        }
2261
        if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) {
2262
            $y1 = 0;
2263
        }
2264
2265
        $width = $width + $x1 - 1;
2266
        $height = $height + $y1 - 1;
2267
2268
        // Subtract the underlying cell widths to find the end cell of the image
2269
        while ($width >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1))) {
2270
            $width -= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1));
2271
            ++$col_end;
2272
        }
2273
2274
        // Subtract the underlying cell heights to find the end cell of the image
2275
        while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) {
2276
            $height -= Xls::sizeRow($this->phpSheet, $row_end + 1);
2277
            ++$row_end;
2278
        }
2279
2280
        // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
2281
        // with zero eight or width.
2282
        //
2283
        if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) == 0) {
2284
            return;
2285
        }
2286
        if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) == 0) {
2287
            return;
2288
        }
2289
        if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) {
2290
            return;
2291
        }
2292
        if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) {
2293
            return;
2294
        }
2295
2296
        // Convert the pixel values to the percentage value expected by Excel
2297
        $x1 = $x1 / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) * 1024;
2298
        $y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256;
2299
        $x2 = $width / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) * 1024; // Distance to right side of object
2300
        $y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object
2301
2302
        $this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2);
2303
    }
2304
2305
    /**
2306
     * Store the OBJ record that precedes an IMDATA record. This could be generalise
2307
     * to support other Excel objects.
2308
     *
2309
     * @param int $colL Column containing upper left corner of object
2310
     * @param int $dxL Distance from left side of cell
2311
     * @param int $rwT Row containing top left corner of object
2312
     * @param float|int $dyT Distance from top of cell
2313
     * @param int $colR Column containing lower right corner of object
2314
     * @param int $dxR Distance from right of cell
2315
     * @param int $rwB Row containing bottom right corner of object
2316
     * @param int $dyB Distance from bottom of cell
2317
     */
2318
    private function writeObjPicture(int $colL, int $dxL, int $rwT, int|float $dyT, int $colR, int $dxR, int $rwB, int $dyB): void
2319
    {
2320
        $record = 0x005D; // Record identifier
2321
        $length = 0x003C; // Bytes to follow
2322
2323
        $cObj = 0x0001; // Count of objects in file (set to 1)
2324
        $OT = 0x0008; // Object type. 8 = Picture
2325
        $id = 0x0001; // Object ID
2326
        $grbit = 0x0614; // Option flags
2327
2328
        $cbMacro = 0x0000; // Length of FMLA structure
2329
        $Reserved1 = 0x0000; // Reserved
2330
        $Reserved2 = 0x0000; // Reserved
2331
2332
        $icvBack = 0x09; // Background colour
2333
        $icvFore = 0x09; // Foreground colour
2334
        $fls = 0x00; // Fill pattern
2335
        $fAuto = 0x00; // Automatic fill
2336
        $icv = 0x08; // Line colour
2337
        $lns = 0xFF; // Line style
2338
        $lnw = 0x01; // Line weight
2339
        $fAutoB = 0x00; // Automatic border
2340
        $frs = 0x0000; // Frame style
2341
        $cf = 0x0009; // Image format, 9 = bitmap
2342
        $Reserved3 = 0x0000; // Reserved
2343
        $cbPictFmla = 0x0000; // Length of FMLA structure
2344
        $Reserved4 = 0x0000; // Reserved
2345
        $grbit2 = 0x0001; // Option flags
2346
        $Reserved5 = 0x0000; // Reserved
2347
2348
        $header = pack('vv', $record, $length);
2349
        $data = pack('V', $cObj);
2350
        $data .= pack('v', $OT);
2351
        $data .= pack('v', $id);
2352
        $data .= pack('v', $grbit);
2353
        $data .= pack('v', $colL);
2354
        $data .= pack('v', $dxL);
2355
        $data .= pack('v', $rwT);
2356
        $data .= pack('v', $dyT);
2357
        $data .= pack('v', $colR);
2358
        $data .= pack('v', $dxR);
2359
        $data .= pack('v', $rwB);
2360
        $data .= pack('v', $dyB);
2361
        $data .= pack('v', $cbMacro);
2362
        $data .= pack('V', $Reserved1);
2363
        $data .= pack('v', $Reserved2);
2364
        $data .= pack('C', $icvBack);
2365
        $data .= pack('C', $icvFore);
2366
        $data .= pack('C', $fls);
2367
        $data .= pack('C', $fAuto);
2368
        $data .= pack('C', $icv);
2369
        $data .= pack('C', $lns);
2370
        $data .= pack('C', $lnw);
2371
        $data .= pack('C', $fAutoB);
2372
        $data .= pack('v', $frs);
2373
        $data .= pack('V', $cf);
2374
        $data .= pack('v', $Reserved3);
2375
        $data .= pack('v', $cbPictFmla);
2376
        $data .= pack('v', $Reserved4);
2377
        $data .= pack('v', $grbit2);
2378
        $data .= pack('V', $Reserved5);
2379
2380
        $this->append($header . $data);
2381
    }
2382
2383
    /**
2384
     * Convert a GD-image into the internal format.
2385
     *
2386
     * @param GdImage $image The image to process
2387
     *
2388
     * @return array{0: float, 1: float, 2: int, 3: string} Data and properties of the bitmap
2389
     */
2390
    public function processBitmapGd(GdImage $image): array
2391
    {
2392
        $width = imagesx($image);
2393
        $height = imagesy($image);
2394
2395
        $data = pack('Vvvvv', 0x000C, $width, $height, 0x01, 0x18);
2396
        for ($j = $height; --$j;) {
2397
            for ($i = 0; $i < $width; ++$i) {
2398
                $colorAt = imagecolorat($image, $i, $j);
2399
                if ($colorAt !== false) {
2400
                    $color = imagecolorsforindex($image, $colorAt);
2401
                    foreach (['red', 'green', 'blue'] as $key) {
2402
                        $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127);
2403
                    }
2404
                    $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']);
2405
                }
2406
            }
2407
            if (3 * $width % 4) {
2408
                $data .= str_repeat("\x00", 4 - 3 * $width % 4);
2409
            }
2410
        }
2411
2412
        return [$width, $height, strlen($data), $data];
2413
    }
2414
2415
    /**
2416
     * Convert a 24 bit bitmap into the modified internal format used by Windows.
2417
     * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
2418
     * MSDN library.
2419
     *
2420
     * @param string $bitmap The bitmap to process
2421
     *
2422
     * @return mixed[] Array with data and properties of the bitmap
2423
     */
2424
    public function processBitmap(string $bitmap): array
2425
    {
2426
        // Open file.
2427
        $bmp_fd = @fopen($bitmap, 'rb');
2428
        if ($bmp_fd === false || 0 === (int) filesize($bitmap)) {
2429
            throw new WriterException("Couldn't import $bitmap");
2430
        }
2431
2432
        // Slurp the file into a string.
2433
        $data = (string) fread($bmp_fd, (int) filesize($bitmap));
2434
2435
        // Check that the file is big enough to be a bitmap.
2436
        if (strlen($data) <= 0x36) {
2437
            throw new WriterException("$bitmap doesn't contain enough data.\n");
2438
        }
2439
2440
        // The first 2 bytes are used to identify the bitmap.
2441
2442
        $identity = unpack('A2ident', $data);
2443
        if ($identity === false || $identity['ident'] != 'BM') {
2444
            throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n");
2445
        }
2446
2447
        // Remove bitmap data: ID.
2448
        $data = substr($data, 2);
2449
2450
        // Read and remove the bitmap size. This is more reliable than reading
2451
        // the data size at offset 0x22.
2452
        //
2453
        $size_array = unpack('Vsa', substr($data, 0, 4)) ?: [];
2454
        /** @var int */
2455
        $size = $size_array['sa'];
2456
        $data = substr($data, 4);
2457
        $size -= 0x36; // Subtract size of bitmap header.
2458
        $size += 0x0C; // Add size of BIFF header.
2459
2460
        // Remove bitmap data: reserved, offset, header length.
2461
        $data = substr($data, 12);
2462
2463
        // Read and remove the bitmap width and height. Verify the sizes.
2464
        $width_and_height = unpack('V2', substr($data, 0, 8)) ?: [];
2465
        $width = $width_and_height[1];
2466
        $height = $width_and_height[2];
2467
        $data = substr($data, 8);
2468
        if ($width > 0xFFFF) {
2469
            throw new WriterException("$bitmap: largest image width supported is 65k.\n");
2470
        }
2471
        if ($height > 0xFFFF) {
2472
            throw new WriterException("$bitmap: largest image height supported is 65k.\n");
2473
        }
2474
2475
        // Read and remove the bitmap planes and bpp data. Verify them.
2476
        $planes_and_bitcount = unpack('v2', substr($data, 0, 4));
2477
        $data = substr($data, 4);
2478
        if ($planes_and_bitcount === false || $planes_and_bitcount[2] != 24) { // Bitcount
2479
            throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n");
2480
        }
2481
        if ($planes_and_bitcount[1] != 1) {
2482
            throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n");
2483
        }
2484
2485
        // Read and remove the bitmap compression. Verify compression.
2486
        $compression = unpack('Vcomp', substr($data, 0, 4));
2487
        $data = substr($data, 4);
2488
2489
        if ($compression === false || $compression['comp'] != 0) {
2490
            throw new WriterException("$bitmap: compression not supported in bitmap image.\n");
2491
        }
2492
2493
        // Remove bitmap data: data size, hres, vres, colours, imp. colours.
2494
        $data = substr($data, 20);
2495
2496
        // Add the BITMAPCOREHEADER data
2497
        $header = pack('Vvvvv', 0x000C, $width, $height, 0x01, 0x18);
2498
        $data = $header . $data;
2499
2500
        return [$width, $height, $size, $data];
2501
    }
2502
2503
    /**
2504
     * Store the window zoom factor. This should be a reduced fraction but for
2505
     * simplicity we will store all fractions with a numerator of 100.
2506
     */
2507 114
    private function writeZoom(): void
2508
    {
2509
        // If scale is 100 we don't need to write a record
2510 114
        if ($this->phpSheet->getSheetView()->getZoomScale() == 100) {
2511 113
            return;
2512
        }
2513
2514 1
        $record = 0x00A0; // Record identifier
2515 1
        $length = 0x0004; // Bytes to follow
2516
2517 1
        $header = pack('vv', $record, $length);
2518 1
        $data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100);
2519 1
        $this->append($header . $data);
2520
    }
2521
2522
    /**
2523
     * Get Escher object.
2524
     */
2525
    public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher
2526
    {
2527
        return $this->escher;
2528
    }
2529
2530
    /**
2531
     * Set Escher object.
2532
     */
2533 15
    public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void
2534
    {
2535 15
        $this->escher = $escher;
2536
    }
2537
2538
    /**
2539
     * Write MSODRAWING record.
2540
     */
2541 114
    private function writeMsoDrawing(): void
2542
    {
2543
        // write the Escher stream if necessary
2544 114
        if (isset($this->escher)) {
2545 15
            $writer = new Escher($this->escher);
2546 15
            $data = $writer->close();
2547 15
            $spOffsets = $writer->getSpOffsets();
2548 15
            $spTypes = $writer->getSpTypes();
2549
            // write the neccesary MSODRAWING, OBJ records
2550
2551
            // split the Escher stream
2552 15
            $spOffsets[0] = 0;
2553 15
            $nm = count($spOffsets) - 1; // number of shapes excluding first shape
2554 15
            for ($i = 1; $i <= $nm; ++$i) {
2555
                // MSODRAWING record
2556 15
                $record = 0x00EC; // Record identifier
2557
2558
                // chunk of Escher stream for one shape
2559 15
                $dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]);
2560
2561 15
                $length = strlen($dataChunk);
2562 15
                $header = pack('vv', $record, $length);
2563
2564 15
                $this->append($header . $dataChunk);
2565
2566
                // OBJ record
2567 15
                $record = 0x005D; // record identifier
2568 15
                $objData = '';
2569
2570
                // ftCmo
2571 15
                if ($spTypes[$i] == 0x00C9) {
2572
                    // Add ftCmo (common object data) subobject
2573 3
                    $objData
2574 3
                        .= pack(
2575 3
                            'vvvvvVVV',
2576 3
                            0x0015, // 0x0015 = ftCmo
2577 3
                            0x0012, // length of ftCmo data
2578 3
                            0x0014, // object type, 0x0014 = filter
2579 3
                            $i, // object id number, Excel seems to use 1-based index, local for the sheet
2580 3
                            0x2101, // option flags, 0x2001 is what OpenOffice.org uses
2581 3
                            0, // reserved
2582 3
                            0, // reserved
2583 3
                            0  // reserved
2584 3
                        );
2585
2586
                    // Add ftSbs Scroll bar subobject
2587 3
                    $objData .= pack('vv', 0x00C, 0x0014);
2588 3
                    $objData .= pack('H*', '0000000000000000640001000A00000010000100');
2589
                    // Add ftLbsData (List box data) subobject
2590 3
                    $objData .= pack('vv', 0x0013, 0x1FEE);
2591 3
                    $objData .= pack('H*', '00000000010001030000020008005700');
2592
                } else {
2593
                    // Add ftCmo (common object data) subobject
2594 12
                    $objData
2595 12
                        .= pack(
2596 12
                            'vvvvvVVV',
2597 12
                            0x0015, // 0x0015 = ftCmo
2598 12
                            0x0012, // length of ftCmo data
2599 12
                            0x0008, // object type, 0x0008 = picture
2600 12
                            $i, // object id number, Excel seems to use 1-based index, local for the sheet
2601 12
                            0x6011, // option flags, 0x6011 is what OpenOffice.org uses
2602 12
                            0, // reserved
2603 12
                            0, // reserved
2604 12
                            0  // reserved
2605 12
                        );
2606
                }
2607
2608
                // ftEnd
2609 15
                $objData
2610 15
                    .= pack(
2611 15
                        'vv',
2612 15
                        0x0000, // 0x0000 = ftEnd
2613 15
                        0x0000  // length of ftEnd data
2614 15
                    );
2615
2616 15
                $length = strlen($objData);
2617 15
                $header = pack('vv', $record, $length);
2618 15
                $this->append($header . $objData);
2619
            }
2620
        }
2621
    }
2622
2623
    /**
2624
     * Store the DATAVALIDATIONS and DATAVALIDATION records.
2625
     */
2626 114
    private function writeDataValidity(): void
2627
    {
2628
        // Datavalidation collection
2629 114
        $dataValidationCollection1 = $this->phpSheet->getDataValidationCollection();
2630 114
        $dataValidationCollection = [];
2631 114
        foreach ($dataValidationCollection1 as $key => $dataValidation) {
2632 5
            $keyParts = explode(' ', $key);
2633 5
            foreach ($keyParts as $keyPart) {
2634 5
                $dataValidationCollection[$keyPart] = $dataValidation;
2635
            }
2636
        }
2637
2638
        // Write data validations?
2639 114
        if (!empty($dataValidationCollection)) {
2640
            // DATAVALIDATIONS record
2641 5
            $record = 0x01B2; // Record identifier
2642 5
            $length = 0x0012; // Bytes to follow
2643
2644 5
            $grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records
2645 5
            $horPos = 0x00000000; // Horizontal position of prompt box, if fixed position
2646 5
            $verPos = 0x00000000; // Vertical position of prompt box, if fixed position
2647 5
            $objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible
2648
2649 5
            $header = pack('vv', $record, $length);
2650 5
            $data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection));
2651 5
            $this->append($header . $data);
2652
2653
            // DATAVALIDATION records
2654 5
            $record = 0x01BE; // Record identifier
2655
2656 5
            foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) {
2657
                // options
2658 5
                $options = 0x00000000;
2659
2660
                // data type
2661 5
                $type = CellDataValidation::type($dataValidation);
2662
2663 5
                $options |= $type << 0;
2664
2665
                // error style
2666 5
                $errorStyle = CellDataValidation::errorStyle($dataValidation);
2667
2668 5
                $options |= $errorStyle << 4;
2669
2670
                // explicit formula?
2671 5
                if ($type == 0x03 && Preg::isMatch('/^\".*\"$/', $dataValidation->getFormula1())) {
2672 4
                    $options |= 0x01 << 7;
2673
                }
2674
2675
                // empty cells allowed
2676 5
                $options |= $dataValidation->getAllowBlank() << 8;
2677
2678
                // show drop down
2679 5
                $options |= (!$dataValidation->getShowDropDown()) << 9;
2680
2681
                // show input message
2682 5
                $options |= $dataValidation->getShowInputMessage() << 18;
2683
2684
                // show error message
2685 5
                $options |= $dataValidation->getShowErrorMessage() << 19;
2686
2687
                // condition operator
2688 5
                $operator = CellDataValidation::operator($dataValidation);
2689
2690 5
                $options |= $operator << 20;
2691
2692 5
                $data = pack('V', $options);
2693
2694
                // prompt title
2695 5
                $promptTitle = $dataValidation->getPromptTitle() !== ''
2696 5
                    ? $dataValidation->getPromptTitle() : chr(0);
2697 5
                $data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle);
2698
2699
                // error title
2700 5
                $errorTitle = $dataValidation->getErrorTitle() !== ''
2701 5
                    ? $dataValidation->getErrorTitle() : chr(0);
2702 5
                $data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle);
2703
2704
                // prompt text
2705 5
                $prompt = $dataValidation->getPrompt() !== ''
2706 5
                    ? $dataValidation->getPrompt() : chr(0);
2707 5
                $data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt);
2708
2709
                // error text
2710 5
                $error = $dataValidation->getError() !== ''
2711 5
                    ? $dataValidation->getError() : chr(0);
2712 5
                $data .= StringHelper::UTF8toBIFF8UnicodeLong($error);
2713
2714
                // formula 1
2715
                try {
2716 5
                    $formula1 = $dataValidation->getFormula1();
2717 5
                    if ($type == 0x03) { // list type
2718 5
                        $formula1 = str_replace(',', chr(0), $formula1);
2719
                    }
2720 5
                    $this->parser->parse($formula1);
2721 5
                    $formula1 = $this->parser->toReversePolish();
2722 5
                    $sz1 = strlen($formula1);
2723 3
                } catch (PhpSpreadsheetException $e) {
2724 3
                    $sz1 = 0;
2725 3
                    $formula1 = '';
2726
                }
2727 5
                $data .= pack('vv', $sz1, 0x0000);
2728 5
                $data .= $formula1;
2729
2730
                // formula 2
2731
                try {
2732 5
                    $formula2 = $dataValidation->getFormula2();
2733 5
                    if ($formula2 === '') {
2734 5
                        throw new WriterException('No formula2');
2735
                    }
2736 3
                    $this->parser->parse($formula2);
2737 3
                    $formula2 = $this->parser->toReversePolish();
2738 3
                    $sz2 = strlen($formula2);
2739 5
                } catch (PhpSpreadsheetException) {
2740 5
                    $sz2 = 0;
2741 5
                    $formula2 = '';
2742
                }
2743 5
                $data .= pack('vv', $sz2, 0x0000);
2744 5
                $data .= $formula2;
2745
2746
                // cell range address list
2747 5
                $data .= pack('v', 0x0001);
2748 5
                $data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate);
2749
2750 5
                $length = strlen($data);
2751 5
                $header = pack('vv', $record, $length);
2752
2753 5
                $this->append($header . $data);
2754
            }
2755
        }
2756
    }
2757
2758
    /**
2759
     * Write PLV Record.
2760
     */
2761 114
    private function writePageLayoutView(): void
2762
    {
2763 114
        $record = 0x088B; // Record identifier
2764 114
        $length = 0x0010; // Bytes to follow
2765
2766 114
        $rt = 0x088B; // 2
2767 114
        $grbitFrt = 0x0000; // 2
2768
        //$reserved = 0x0000000000000000; // 8
2769 114
        $wScalvePLV = $this->phpSheet->getSheetView()->getZoomScale(); // 2
2770
2771
        // The options flags that comprise $grbit
2772 114
        if ($this->phpSheet->getSheetView()->getView() == SheetView::SHEETVIEW_PAGE_LAYOUT) {
2773 1
            $fPageLayoutView = 1;
2774
        } else {
2775 113
            $fPageLayoutView = 0;
2776
        }
2777 114
        $fRulerVisible = 0;
2778 114
        $fWhitespaceHidden = 0;
2779
2780 114
        $grbit = $fPageLayoutView; // 2
2781 114
        $grbit |= $fRulerVisible << 1;
2782 114
        $grbit |= $fWhitespaceHidden << 3;
2783
2784 114
        $header = pack('vv', $record, $length);
2785 114
        $data = pack('vvVVvv', $rt, $grbitFrt, 0x00000000, 0x00000000, $wScalvePLV, $grbit);
2786 114
        $this->append($header . $data);
2787
    }
2788
2789
    /**
2790
     * Write CFRule Record.
2791
     *
2792
     * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
2793
     */
2794 11
    private function writeCFRule(
2795
        ConditionalHelper $conditionalFormulaHelper,
2796
        Conditional $conditional,
2797
        string $cellRange
2798
    ): void {
2799 11
        $record = 0x01B1; // Record identifier
2800 11
        $type = null; // Type of the CF
2801 11
        $operatorType = null; // Comparison operator
2802
2803 11
        if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) {
2804 1
            $type = 0x02;
2805 1
            $operatorType = 0x00;
2806 10
        } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS) {
2807 10
            $type = 0x01;
2808
2809 10
            switch ($conditional->getOperatorType()) {
2810
                case Conditional::OPERATOR_NONE:
2811
                    $operatorType = 0x00;
2812
2813
                    break;
2814
                case Conditional::OPERATOR_EQUAL:
2815 2
                    $operatorType = 0x03;
2816
2817 2
                    break;
2818
                case Conditional::OPERATOR_GREATERTHAN:
2819 3
                    $operatorType = 0x05;
2820
2821 3
                    break;
2822
                case Conditional::OPERATOR_GREATERTHANOREQUAL:
2823 2
                    $operatorType = 0x07;
2824
2825 2
                    break;
2826
                case Conditional::OPERATOR_LESSTHAN:
2827 6
                    $operatorType = 0x06;
2828
2829 6
                    break;
2830
                case Conditional::OPERATOR_LESSTHANOREQUAL:
2831
                    $operatorType = 0x08;
2832
2833
                    break;
2834
                case Conditional::OPERATOR_NOTEQUAL:
2835
                    $operatorType = 0x04;
2836
2837
                    break;
2838
                case Conditional::OPERATOR_BETWEEN:
2839 5
                    $operatorType = 0x01;
2840
2841 5
                    break;
2842
                    // not OPERATOR_NOTBETWEEN 0x02
2843
            }
2844
        }
2845
2846
        // $szValue1 : size of the formula data for first value or formula
2847
        // $szValue2 : size of the formula data for second value or formula
2848 11
        $arrConditions = $conditional->getConditions();
2849 11
        $numConditions = count($arrConditions);
2850
2851 11
        $szValue1 = 0x0000;
2852 11
        $szValue2 = 0x0000;
2853 11
        $operand1 = null;
2854 11
        $operand2 = null;
2855
2856 11
        if ($numConditions === 1) {
2857 8
            $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange);
2858 8
            $szValue1 = $conditionalFormulaHelper->size();
2859 8
            $operand1 = $conditionalFormulaHelper->tokens();
2860 5
        } elseif ($numConditions === 2 && ($conditional->getOperatorType() === Conditional::OPERATOR_BETWEEN)) {
2861 5
            $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange);
2862 5
            $szValue1 = $conditionalFormulaHelper->size();
2863 5
            $operand1 = $conditionalFormulaHelper->tokens();
2864 5
            $conditionalFormulaHelper->processCondition($arrConditions[1], $cellRange);
2865 5
            $szValue2 = $conditionalFormulaHelper->size();
2866 5
            $operand2 = $conditionalFormulaHelper->tokens();
2867
        }
2868
2869
        // $flags : Option flags
2870
        // Alignment
2871
        /*$bAlignHz = ($conditional->getStyle()->getAlignment()->getHorizontal() === null ? 1 : 0);
2872
        $bAlignVt = ($conditional->getStyle()->getAlignment()->getVertical() === null ? 1 : 0);
2873
        $bAlignWrapTx = ($conditional->getStyle()->getAlignment()->getWrapText() === false ? 1 : 0);
2874
        $bTxRotation = ($conditional->getStyle()->getAlignment()->getTextRotation() === null ? 1 : 0);
2875
        $bIndent = ($conditional->getStyle()->getAlignment()->getIndent() === 0 ? 1 : 0);
2876
        $bShrinkToFit = ($conditional->getStyle()->getAlignment()->getShrinkToFit() === false ? 1 : 0);
2877
        if ($bAlignHz == 0 || $bAlignVt == 0 || $bAlignWrapTx == 0 || $bTxRotation == 0 || $bIndent == 0 || $bShrinkToFit == 0) {
2878
            $bFormatAlign = 1;
2879
        } else {
2880
            $bFormatAlign = 0;
2881
        }*/
2882
        // Protection
2883
        /*$bProtLocked = ($conditional->getStyle()->getProtection()->getLocked() === null ? 1 : 0);
2884
        $bProtHidden = ($conditional->getStyle()->getProtection()->getHidden() === null ? 1 : 0);
2885
        if ($bProtLocked == 0 || $bProtHidden == 0) {
2886
            $bFormatProt = 1;
2887
        } else {
2888
            $bFormatProt = 0;
2889
        }*/
2890
        // Border
2891 11
        $bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
2892 11
        $bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
2893 11
        $bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
2894 11
        $bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
2895
        //$diagonalDirection = $conditional->getStyle()->getBorders()->getDiagonalDirection();
2896
        // Excel does not support conditional diagonal border even for xlsx
2897 11
        $bBorderDiagTop = self::$always0; //$diagonalDirection === Borders::DIAGONAL_DOWN || $diagonalDirection === Borders::DIAGONAL_BOTH;
2898 11
        $bBorderDiagBottom = self::$always0; //$diagonalDirection === Borders::DIAGONAL_UP || $diagonalDirection === Borders::DIAGONAL_BOTH;
2899
2900 11
        if ($bBorderLeft === 1 || $bBorderRight === 1 || $bBorderTop === 1 || $bBorderBottom === 1 || $bBorderDiagTop === 1 || $bBorderDiagBottom === 1) {
2901 1
            $bFormatBorder = 1;
2902
        } else {
2903 10
            $bFormatBorder = 0;
2904
        }
2905
        // Pattern
2906 11
        $bFillStyle = $conditional->getStyle()->getFill()->getFillType() ? 1 : 0;
2907 11
        $bFillColor = $conditional->getStyle()->getFill()->getStartColor()->getARGB() ? 1 : 0;
2908 11
        $bFillColorBg = $conditional->getStyle()->getFill()->getEndColor()->getARGB() ? 1 : 0;
2909 11
        if ($bFillStyle == 1 || $bFillColor == 1 || $bFillColorBg == 1) {
2910 4
            $bFormatFill = 1;
2911
        } else {
2912 7
            $bFormatFill = 0;
2913
        }
2914
        // Font
2915
        if (
2916 11
            $conditional->getStyle()->getFont()->getName() !== null
2917 11
            || $conditional->getStyle()->getFont()->getSize() !== null
2918 11
            || $conditional->getStyle()->getFont()->getBold() !== null
2919 11
            || $conditional->getStyle()->getFont()->getItalic() !== null
2920 11
            || $conditional->getStyle()->getFont()->getSuperscript() !== null
2921 11
            || $conditional->getStyle()->getFont()->getSubscript() !== null
2922 11
            || $conditional->getStyle()->getFont()->getUnderline() !== null
2923 11
            || $conditional->getStyle()->getFont()->getStrikethrough() !== null
2924 11
            || $conditional->getStyle()->getFont()->getColor()->getARGB() !== null
2925
        ) {
2926 10
            $bFormatFont = 1;
2927
        } else {
2928 1
            $bFormatFont = 0;
2929
        }
2930
        // Alignment
2931 11
        $flags = 0;
2932
        //$flags |= (1 == $bAlignHz ? 0x00000001 : 0);
2933
        //$flags |= (1 == $bAlignVt ? 0x00000002 : 0);
2934
        //$flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0);
2935
        //$flags |= (1 == $bTxRotation ? 0x00000008 : 0);
2936
        // Justify last line flag
2937 11
        $flags |= (1 == self::$always1 ? 0x00000010 : 0);
2938
        //$flags |= (1 == $bIndent ? 0x00000020 : 0);
2939
        //$flags |= (1 == $bShrinkToFit ? 0x00000040 : 0);
2940
        // Default
2941 11
        $flags |= (1 == self::$always1 ? 0x00000080 : 0);
2942
        // Protection
2943
        //$flags |= (1 == $bProtLocked ? 0x00000100 : 0);
2944
        //$flags |= (1 == $bProtHidden ? 0x00000200 : 0);
2945
        // Border, note that flags are opposite of what you might expect
2946 11
        $flags |= (0 == $bBorderLeft ? 0x00000400 : 0);
2947 11
        $flags |= (0 == $bBorderRight ? 0x00000800 : 0);
2948 11
        $flags |= (0 == $bBorderTop ? 0x00001000 : 0);
2949 11
        $flags |= (0 == $bBorderBottom ? 0x00002000 : 0);
2950 11
        $flags |= (0 === $bBorderDiagTop ? 0x00004000 : 0); // Top left to Bottom right border
2951 11
        $flags |= (0 === $bBorderDiagBottom ? 0x00008000 : 0); // Bottom left to Top right border
2952
        // Pattern
2953 11
        $flags |= (1 == $bFillStyle ? 0x00010000 : 0);
2954 11
        $flags |= (1 == $bFillColor ? 0x00020000 : 0);
2955 11
        $flags |= (1 == $bFillColorBg ? 0x00040000 : 0);
2956 11
        $flags |= (1 == self::$always1 ? 0x00380000 : 0);
2957
        // Font
2958 11
        $flags |= (1 == $bFormatFont ? 0x04000000 : 0);
2959
        // Alignment:
2960
        //$flags |= (1 == $bFormatAlign ? 0x08000000 : 0);
2961
        // Border
2962 11
        $flags |= (1 == $bFormatBorder ? 0x10000000 : 0);
2963
        // Pattern
2964 11
        $flags |= (1 == $bFormatFill ? 0x20000000 : 0);
2965
        // Protection
2966
        //$flags |= (1 == $bFormatProt ? 0x40000000 : 0);
2967
        // Text direction
2968 11
        $flags |= (1 == self::$always0 ? 0x80000000 : 0);
2969
2970 11
        $dataBlockFont = null;
2971
        //$dataBlockAlign = null;
2972 11
        $dataBlockBorder = null;
2973 11
        $dataBlockFill = null;
2974
2975
        // Data Blocks
2976 11
        if ($bFormatFont == 1) {
2977
            // Font Name
2978 10
            if ($conditional->getStyle()->getFont()->getName() === null) {
2979 10
                $dataBlockFont = pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000);
2980 10
                $dataBlockFont .= pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000);
2981
            } else {
2982
                $dataBlockFont = StringHelper::UTF8toBIFF8UnicodeLong($conditional->getStyle()->getFont()->getName());
2983
            }
2984
            // Font Size
2985 10
            if ($conditional->getStyle()->getFont()->getSize() === null) {
2986 10
                $dataBlockFont .= pack('V', 20 * 11);
2987
            } else {
2988
                $dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize());
2989
            }
2990
            // Font Options
2991 10
            $italicStrike = 0;
2992 10
            if ($conditional->getStyle()->getFont()->getItalic() === true) {
2993 2
                $italicStrike |= 2;
2994
            }
2995 10
            if ($conditional->getStyle()->getFont()->getStrikethrough() === true) {
2996 1
                $italicStrike |= 0x80;
2997
            }
2998 10
            $dataBlockFont .= pack('V', $italicStrike);
2999
            // Font weight
3000 10
            if ($conditional->getStyle()->getFont()->getBold() === true) {
3001 5
                $dataBlockFont .= pack('v', 0x02BC);
3002 7
            } elseif ($conditional->getStyle()->getFont()->getBold() === null) {
3003 7
                $dataBlockFont .= pack('v', 0x0000);
3004
            } else {
3005
                $dataBlockFont .= pack('v', 0x0190);
3006
            }
3007
            // Escapement type
3008 10
            if ($conditional->getStyle()->getFont()->getSubscript() === true) {
3009
                $dataBlockFont .= pack('v', 0x02);
3010
                $fontEscapement = 0;
3011 10
            } elseif ($conditional->getStyle()->getFont()->getSuperscript() === true) {
3012
                $dataBlockFont .= pack('v', 0x01);
3013
                $fontEscapement = 0;
3014
            } else {
3015 10
                $dataBlockFont .= pack('v', 0x00);
3016 10
                $fontEscapement = 1;
3017
            }
3018
            // Underline type
3019 10
            switch ($conditional->getStyle()->getFont()->getUnderline()) {
3020
                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE:
3021
                    $dataBlockFont .= pack('C', 0x00);
3022
                    $fontUnderline = 0;
3023
3024
                    break;
3025
                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE:
3026
                    $dataBlockFont .= pack('C', 0x02);
3027
                    $fontUnderline = 0;
3028
3029
                    break;
3030
                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING:
3031
                    $dataBlockFont .= pack('C', 0x22);
3032
                    $fontUnderline = 0;
3033
3034
                    break;
3035
                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE:
3036
                    $dataBlockFont .= pack('C', 0x01);
3037
                    $fontUnderline = 0;
3038
3039
                    break;
3040
                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING:
3041
                    $dataBlockFont .= pack('C', 0x21);
3042
                    $fontUnderline = 0;
3043
3044
                    break;
3045
                default:
3046 10
                    $dataBlockFont .= pack('C', 0x00);
3047 10
                    $fontUnderline = 1;
3048
3049 10
                    break;
3050
            }
3051
            // Not used (3)
3052 10
            $dataBlockFont .= pack('vC', 0x0000, 0x00);
3053
            // Font color index
3054 10
            $colorIdx = $this->workbookColorIndex($conditional->getStyle()->getFont()->getColor()->getRgb(), 0);
3055 10
            $dataBlockFont .= pack('V', $colorIdx);
3056
            // Not used (4)
3057 10
            $dataBlockFont .= pack('V', 0x00000000);
3058
            // Options flags for modified font attributes
3059 10
            $optionsFlags = 0;
3060 10
            $optionsFlags |= ($conditional->getStyle()->getFont()->getBold() === null && $conditional->getStyle()->getFont()->getItalic() === null) ? 2 : 0;
3061 10
            $optionsFlags |= (1 == self::$always1 ? 0x00000008 : 0);
3062 10
            $optionsFlags |= (1 == self::$always1 ? 0x00000010 : 0);
3063 10
            $optionsFlags |= (1 == self::$always0 ? 0x00000020 : 0);
3064 10
            $optionsFlags |= ($conditional->getStyle()->getFont()->getStrikethrough() === null) ? 0x80 : 0;
3065 10
            $dataBlockFont .= pack('V', $optionsFlags);
3066
            // Escapement type
3067 10
            $dataBlockFont .= pack('V', $fontEscapement);
3068
            // Underline type
3069 10
            $dataBlockFont .= pack('V', $fontUnderline);
3070
            // Always
3071 10
            $dataBlockFont .= pack('V', 0x00000000);
3072
            // Always
3073 10
            $dataBlockFont .= pack('V', 0x00000000);
3074
            // Not used (8)
3075 10
            $dataBlockFont .= pack('VV', 0x00000000, 0x00000000);
3076
            // Always
3077 10
            $dataBlockFont .= pack('v', 0x0001);
3078
        }
3079
        /*if ($bFormatAlign === 1) {
3080
            // Alignment and text break
3081
            $blockAlign = Style\CellAlignment::horizontal($conditional->getStyle()->getAlignment());
3082
            $blockAlign |= Style\CellAlignment::wrap($conditional->getStyle()->getAlignment()) << 3;
3083
            $blockAlign |= Style\CellAlignment::vertical($conditional->getStyle()->getAlignment()) << 4;
3084
            $blockAlign |= 0 << 7;
3085
3086
            // Text rotation angle
3087
            $blockRotation = $conditional->getStyle()->getAlignment()->getTextRotation();
3088
3089
            // Indentation
3090
            $blockIndent = $conditional->getStyle()->getAlignment()->getIndent();
3091
            if ($conditional->getStyle()->getAlignment()->getShrinkToFit() === true) {
3092
                $blockIndent |= 1 << 4;
3093
            } else {
3094
                $blockIndent |= 0 << 4;
3095
            }
3096
            $blockIndent |= 0 << 6;
3097
3098
            // Relative indentation
3099
            $blockIndentRelative = 255;
3100
3101
            $dataBlockAlign = pack('CCvvv', $blockAlign, $blockRotation, $blockIndent, $blockIndentRelative, 0x0000);
3102
        }*/
3103 11
        if ($bFormatBorder === 1) {
3104 1
            $blockLineStyle = Style\CellBorder::style($conditional->getStyle()->getBorders()->getLeft());
3105 1
            $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getRight()) << 4;
3106 1
            $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getTop()) << 8;
3107 1
            $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getBottom()) << 12;
3108
3109 1
            if ($bBorderLeft !== 0) {
3110 1
                $colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getLeft()->getColor()->getRgb(), 0);
3111 1
                $blockLineStyle |= $colorIdx << 16;
3112
            }
3113 1
            if ($bBorderRight !== 0) {
3114 1
                $colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getRight()->getColor()->getRgb(), 0);
3115 1
                $blockLineStyle |= $colorIdx << 23;
3116
            }
3117 1
            $blockColor = 0;
3118 1
            if ($bBorderTop !== 0) {
3119 1
                $colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getTop()->getColor()->getRgb(), 0);
3120 1
                $blockColor |= $colorIdx;
3121
            }
3122 1
            if ($bBorderBottom !== 0) {
3123 1
                $colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getBottom()->getColor()->getRgb(), 0);
3124 1
                $blockColor |= $colorIdx << 7;
3125
            }
3126
            /* Excel does not support condtional diagonal borders even for xlsx
3127
            if ($bBorderDiagTop !== 0 || $bBorderDiagBottom !== 0) {
3128
                $colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getDiagonal()->getColor()->getRgb(), 0);
3129
                $blockColor |= $colorIdx << 14;
3130
                $blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21;
3131
                if ($bBorderDiagTop !== 0) {
3132
                    $blockLineStyle |= 1 << 30;
3133
                }
3134
                if ($bBorderDiagBottom !== 0) {
3135
                    $blockLineStyle |= 1 << 31;
3136
                }
3137
            }
3138
            */
3139 1
            $dataBlockBorder = pack('VV', $blockLineStyle, $blockColor);
3140
        }
3141 11
        if ($bFormatFill === 1) {
3142
            // Fill Pattern Style
3143 4
            $blockFillPatternStyle = Style\CellFill::style($conditional->getStyle()->getFill());
3144
            // Background Color
3145 4
            $colorIdxBg = $this->workbookColorIndex($conditional->getStyle()->getFill()->getStartColor()->getRgb(), 0x41);
3146
            // Foreground Color
3147 4
            $colorIdxFg = $this->workbookColorIndex($conditional->getStyle()->getFill()->getEndColor()->getRgb(), 0x40);
3148
3149 4
            $dataBlockFill = pack('v', $blockFillPatternStyle);
3150 4
            $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7));
3151
        }
3152
3153 11
        $data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000);
3154 11
        if ($bFormatFont === 1) { // Block Formatting : OK
3155 10
            $data .= $dataBlockFont;
3156
        }
3157
        //if ($bFormatAlign === 1) {
3158
        //    $data .= $dataBlockAlign;
3159
        //}
3160 11
        if ($bFormatBorder === 1) {
3161 1
            $data .= $dataBlockBorder;
3162
        }
3163 11
        if ($bFormatFill === 1) { // Block Formatting : OK
3164 4
            $data .= $dataBlockFill;
3165
        }
3166
        //if ($bFormatProt == 1) {
3167
        //    $data .= $this->getDataBlockProtection($conditional);
3168
        //}
3169 11
        if ($operand1 !== null) {
3170 11
            $data .= $operand1;
3171
        }
3172 11
        if ($operand2 !== null) {
3173 5
            $data .= $operand2;
3174
        }
3175 11
        $header = pack('vv', $record, strlen($data));
3176 11
        $this->append($header . $data);
3177
    }
3178
3179
    /**
3180
     * Write CFHeader record.
3181
     *
3182
     * @param Conditional[] $conditionalStyles
3183
     */
3184 11
    private function writeCFHeader(string $cellCoordinate, array $conditionalStyles): bool
3185
    {
3186 11
        $record = 0x01B0; // Record identifier
3187 11
        $length = 0x0016; // Bytes to follow
3188
3189 11
        $numColumnMin = null;
3190 11
        $numColumnMax = null;
3191 11
        $numRowMin = null;
3192 11
        $numRowMax = null;
3193
3194 11
        $arrConditional = [];
3195 11
        foreach ($conditionalStyles as $conditional) {
3196 11
            if (!in_array($conditional->getHashCode(), $arrConditional)) {
3197 11
                $arrConditional[] = $conditional->getHashCode();
3198
            }
3199
            // Cells
3200 11
            $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate);
3201 11
            if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) {
3202 11
                $numColumnMin = $rangeCoordinates[0][0];
3203
            }
3204 11
            if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) {
3205 11
                $numColumnMax = $rangeCoordinates[1][0];
3206
            }
3207 11
            if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) {
3208 11
                $numRowMin = (int) $rangeCoordinates[0][1];
3209
            }
3210 11
            if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) {
3211 11
                $numRowMax = (int) $rangeCoordinates[1][1];
3212
            }
3213
        }
3214
3215 11
        if (count($arrConditional) === 0) {
3216
            return false;
3217
        }
3218
3219 11
        $needRedraw = 1;
3220 11
        $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1);
3221
3222 11
        $header = pack('vv', $record, $length);
3223 11
        $data = pack('vv', count($arrConditional), $needRedraw);
3224 11
        $data .= $cellRange;
3225 11
        $data .= pack('v', 0x0001);
3226 11
        $data .= $cellRange;
3227 11
        $this->append($header . $data);
3228
3229 11
        return true;
3230
    }
3231
3232
    /*private function getDataBlockProtection(Conditional $conditional): int
3233
    {
3234
        $dataBlockProtection = 0;
3235
        if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) {
3236
            $dataBlockProtection = 1;
3237
        }
3238
        if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) {
3239
            $dataBlockProtection = 1 << 1;
3240
        }
3241
3242
        return $dataBlockProtection;
3243
    }*/
3244
3245 11
    private function workbookColorIndex(?string $rgb, int $default): int
3246
    {
3247 11
        return (empty($rgb) || $this->writerWorkbook === null) ? $default : $this->writerWorkbook->addColor($rgb, $default);
3248
    }
3249
}
3250