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