Failed Conditions
Push — master ( bf4629...7712d5 )
by Adrien
27:59 queued 18:08
created

Worksheet::writeConditionalFormatting()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 27
ccs 15
cts 15
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 8
nop 0
crap 9
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\Conditional;
16
use PhpOffice\PhpSpreadsheet\Style\Protection;
17
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
18
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
19
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
20
21
// Original file header of PEAR::Spreadsheet_Excel_Writer_Worksheet (used as the base for this class):
22
// -----------------------------------------------------------------------------------------
23
// /*
24
// *  Module written/ported by Xavier Noguer <[email protected]>
25
// *
26
// *  The majority of this is _NOT_ my code.  I simply ported it from the
27
// *  PERL Spreadsheet::WriteExcel module.
28
// *
29
// *  The author of the Spreadsheet::WriteExcel module is John McNamara
30
// *  <[email protected]>
31
// *
32
// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
33
// *  porting of this code to PHP.  Any questions directly related to this
34
// *  class library should be directed to me.
35
// *
36
// *  License Information:
37
// *
38
// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
39
// *    Copyright (c) 2002-2003 Xavier Noguer [email protected]
40
// *
41
// *    This library is free software; you can redistribute it and/or
42
// *    modify it under the terms of the GNU Lesser General Public
43
// *    License as published by the Free Software Foundation; either
44
// *    version 2.1 of the License, or (at your option) any later version.
45
// *
46
// *    This library is distributed in the hope that it will be useful,
47
// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
48
// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
49
// *    Lesser General Public License for more details.
50
// *
51
// *    You should have received a copy of the GNU Lesser General Public
52
// *    License along with this library; if not, write to the Free Software
53
// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
54
// */
55
class Worksheet extends BIFFwriter
56
{
57
    /** @var int */
58
    private static $always0 = 0;
59
60
    /** @var int */
61
    private static $always1 = 1;
62
63
    /**
64
     * Formula parser.
65
     */
66
    private Parser $parser;
67
68
    /**
69
     * Array containing format information for columns.
70
     *
71
     * @var array
72
     */
73
    private $columnInfo;
74
75
    /**
76
     * The active pane for the worksheet.
77
     */
78
    private int $activePane;
79
80
    /**
81
     * Whether to use outline.
82
     */
83
    private bool $outlineOn;
84
85
    /**
86
     * Auto outline styles.
87
     */
88
    private bool $outlineStyle;
89
90
    /**
91
     * Whether to have outline summary below.
92
     * Not currently used.
93
     */
94
    private bool $outlineBelow; //* @phpstan-ignore-line
95
96
    /**
97
     * Whether to have outline summary at the right.
98
     * Not currently used.
99
     */
100
    private bool $outlineRight; //* @phpstan-ignore-line
101
102
    /**
103
     * Reference to the total number of strings in the workbook.
104
     *
105
     * @var int
106
     */
107
    private $stringTotal;
108
109
    /**
110
     * Reference to the number of unique strings in the workbook.
111
     *
112
     * @var int
113
     */
114
    private $stringUnique;
115
116
    /**
117
     * Reference to the array containing all the unique strings in the workbook.
118
     *
119
     * @var array
120
     */
121
    private $stringTable;
122
123
    /**
124
     * Color cache.
125
     *
126
     * @var array
127
     */
128
    private $colors;
129
130
    /**
131
     * Index of first used row (at least 0).
132
     */
133
    private int $firstRowIndex;
134
135
    /**
136
     * Index of last used row. (no used rows means -1).
137
     */
138
    private int $lastRowIndex;
139
140
    /**
141
     * Index of first used column (at least 0).
142
     */
143
    private int $firstColumnIndex;
144
145
    /**
146
     * Index of last used column (no used columns means -1).
147
     */
148
    private int $lastColumnIndex;
149
150
    /**
151
     * Sheet object.
152
     *
153
     * @var \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
154
     */
155
    public $phpSheet;
156
157
    /**
158
     * Escher object corresponding to MSODRAWING.
159
     */
160
    private ?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher = null;
161
162
    /**
163
     * Array of font hashes associated to FONT records index.
164
     *
165
     * @var array
166
     */
167
    public $fontHashIndex;
168
169
    /**
170
     * @var bool
171
     */
172
    private $preCalculateFormulas;
173
174
    private int $printHeaders;
175
176
    /**
177
     * Constructor.
178
     *
179
     * @param int $str_total Total number of strings
180
     * @param int $str_unique Total number of unique strings
181
     * @param array $str_table String Table
182
     * @param array $colors Colour Table
183
     * @param Parser $parser The formula parser created for the Workbook
184
     * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written
185
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write
186
     */
187 96
    public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, Parser $parser, $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet)
188
    {
189
        // It needs to call its parent's constructor explicitly
190 96
        parent::__construct();
191
192 96
        $this->preCalculateFormulas = $preCalculateFormulas;
193 96
        $this->stringTotal = &$str_total;
194 96
        $this->stringUnique = &$str_unique;
195 96
        $this->stringTable = &$str_table;
196 96
        $this->colors = &$colors;
197 96
        $this->parser = $parser;
198
199 96
        $this->phpSheet = $phpSheet;
200
201 96
        $this->columnInfo = [];
202 96
        $this->activePane = 3;
203
204 96
        $this->printHeaders = 0;
205
206 96
        $this->outlineStyle = false;
207 96
        $this->outlineBelow = true;
208 96
        $this->outlineRight = true;
209 96
        $this->outlineOn = true;
210
211 96
        $this->fontHashIndex = [];
212
213
        // calculate values for DIMENSIONS record
214 96
        $minR = 1;
215 96
        $minC = 'A';
216
217 96
        $maxR = $this->phpSheet->getHighestRow();
218 96
        $maxC = $this->phpSheet->getHighestColumn();
219
220
        // Determine lowest and highest column and row
221 96
        $this->firstRowIndex = $minR;
222 96
        $this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR;
223
224 96
        $this->firstColumnIndex = Coordinate::columnIndexFromString($minC);
225 96
        $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC);
226
227 96
        if ($this->lastColumnIndex > 255) {
228 1
            $this->lastColumnIndex = 255;
229
        }
230
    }
231
232
    /**
233
     * Add data to the beginning of the workbook (note the reverse order)
234
     * and to the end of the workbook.
235
     *
236
     * @see Workbook::storeWorkbook
237
     */
238 96
    public function close(): void
239
    {
240 96
        $phpSheet = $this->phpSheet;
241
242
        // Storing selected cells and active sheet because it changes while parsing cells with formulas.
243 96
        $selectedCells = $this->phpSheet->getSelectedCells();
244 96
        $activeSheetIndex = $this->phpSheet->getParentOrThrow()->getActiveSheetIndex();
245
246
        // Write BOF record
247 96
        $this->storeBof(0x0010);
248
249
        // Write PRINTHEADERS
250 96
        $this->writePrintHeaders();
251
252
        // Write PRINTGRIDLINES
253 96
        $this->writePrintGridlines();
254
255
        // Write GRIDSET
256 96
        $this->writeGridset();
257
258
        // Calculate column widths
259 96
        $phpSheet->calculateColumnWidths();
260
261
        // Column dimensions
262 96
        if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) {
263 91
            $defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParentOrThrow()->getDefaultStyle()->getFont());
264
        }
265
266 96
        $columnDimensions = $phpSheet->getColumnDimensions();
267 96
        $maxCol = $this->lastColumnIndex - 1;
268 96
        for ($i = 0; $i <= $maxCol; ++$i) {
269 96
            $hidden = 0;
270 96
            $level = 0;
271 96
            $xfIndex = 15; // there are 15 cell style Xfs
272
273 96
            $width = $defaultWidth;
274
275 96
            $columnLetter = Coordinate::stringFromColumnIndex($i + 1);
276 96
            if (isset($columnDimensions[$columnLetter])) {
277 37
                $columnDimension = $columnDimensions[$columnLetter];
278 37
                if ($columnDimension->getWidth() >= 0) {
279 36
                    $width = $columnDimension->getWidth();
280
                }
281 37
                $hidden = $columnDimension->getVisible() ? 0 : 1;
282 37
                $level = $columnDimension->getOutlineLevel();
283 37
                $xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs
284
            }
285
286
            // Components of columnInfo:
287
            // $firstcol first column on the range
288
            // $lastcol  last column on the range
289
            // $width    width to set
290
            // $xfIndex  The optional cell style Xf index to apply to the columns
291
            // $hidden   The optional hidden atribute
292
            // $level    The optional outline level
293 96
            $this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level];
294
        }
295
296
        // Write GUTS
297 96
        $this->writeGuts();
298
299
        // Write DEFAULTROWHEIGHT
300 96
        $this->writeDefaultRowHeight();
301
        // Write WSBOOL
302 96
        $this->writeWsbool();
303
        // Write horizontal and vertical page breaks
304 96
        $this->writeBreaks();
305
        // Write page header
306 96
        $this->writeHeader();
307
        // Write page footer
308 96
        $this->writeFooter();
309
        // Write page horizontal centering
310 96
        $this->writeHcenter();
311
        // Write page vertical centering
312 96
        $this->writeVcenter();
313
        // Write left margin
314 96
        $this->writeMarginLeft();
315
        // Write right margin
316 96
        $this->writeMarginRight();
317
        // Write top margin
318 96
        $this->writeMarginTop();
319
        // Write bottom margin
320 96
        $this->writeMarginBottom();
321
        // Write page setup
322 96
        $this->writeSetup();
323
        // Write sheet protection
324 96
        $this->writeProtect();
325
        // Write SCENPROTECT
326 96
        $this->writeScenProtect();
327
        // Write OBJECTPROTECT
328 96
        $this->writeObjectProtect();
329
        // Write sheet password
330 96
        $this->writePassword();
331
        // Write DEFCOLWIDTH record
332 96
        $this->writeDefcol();
333
334
        // Write the COLINFO records if they exist
335 96
        if (!empty($this->columnInfo)) {
336 96
            $colcount = count($this->columnInfo);
337 96
            for ($i = 0; $i < $colcount; ++$i) {
338 96
                $this->writeColinfo($this->columnInfo[$i]);
339
            }
340
        }
341 96
        $autoFilterRange = $phpSheet->getAutoFilter()->getRange();
342 96
        if (!empty($autoFilterRange)) {
343
            // Write AUTOFILTERINFO
344 3
            $this->writeAutoFilterInfo();
345
        }
346
347
        // Write sheet dimensions
348 96
        $this->writeDimensions();
349
350
        // Row dimensions
351 96
        foreach ($phpSheet->getRowDimensions() as $rowDimension) {
352 25
            $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs
353 25
            $this->writeRow(
354 25
                $rowDimension->getRowIndex() - 1,
355 25
                (int) $rowDimension->getRowHeight(),
356 25
                $xfIndex,
357 25
                !$rowDimension->getVisible(),
358 25
                $rowDimension->getOutlineLevel()
359 25
            );
360
        }
361
362
        // Write Cells
363 96
        foreach ($phpSheet->getCellCollection()->getSortedCoordinates() as $coordinate) {
364
            /** @var Cell $cell */
365 91
            $cell = $phpSheet->getCellCollection()->get($coordinate);
366 91
            $row = $cell->getRow() - 1;
367 91
            $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
368
369
            // Don't break Excel break the code!
370 91
            if ($row > 65535 || $column > 255) {
371
                throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.');
372
            }
373
374
            // Write cell value
375 91
            $xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs
376
377 91
            $cVal = $cell->getValue();
378 91
            if ($cVal instanceof RichText) {
379 11
                $arrcRun = [];
380 11
                $str_pos = 0;
381 11
                $elements = $cVal->getRichTextElements();
382 11
                foreach ($elements as $element) {
383
                    // FONT Index
384 11
                    $str_fontidx = 0;
385 11
                    if ($element instanceof Run) {
386 11
                        $getFont = $element->getFont();
387 11
                        if ($getFont !== null) {
388 11
                            $str_fontidx = $this->fontHashIndex[$getFont->getHashCode()];
389
                        }
390
                    }
391 11
                    $arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx];
392
                    // Position FROM
393 11
                    $str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8');
394
                }
395 11
                $this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun);
396
            } else {
397 90
                switch ($cell->getDatatype()) {
398
                    case DataType::TYPE_STRING:
399
                    case DataType::TYPE_INLINE:
400
                    case DataType::TYPE_NULL:
401 71
                        if ($cVal === '' || $cVal === null) {
402 38
                            $this->writeBlank($row, $column, $xfIndex);
403
                        } else {
404 66
                            $this->writeString($row, $column, $cVal, $xfIndex);
405
                        }
406
407 71
                        break;
408
                    case DataType::TYPE_NUMERIC:
409 49
                        $this->writeNumber($row, $column, $cVal, $xfIndex);
410
411 49
                        break;
412
                    case DataType::TYPE_FORMULA:
413 38
                        $calculatedValue = $this->preCalculateFormulas ?
414 38
                            $cell->getCalculatedValue() : null;
415 38
                        if (self::WRITE_FORMULA_EXCEPTION == $this->writeFormula($row, $column, $cVal, $xfIndex, $calculatedValue)) {
416 7
                            if ($calculatedValue === null) {
417
                                $calculatedValue = $cell->getCalculatedValue();
418
                            }
419 7
                            $calctype = gettype($calculatedValue);
420 7
                            match ($calctype) {
421 7
                                'integer', 'double' => $this->writeNumber($row, $column, (float) $calculatedValue, $xfIndex),
422 7
                                'string' => $this->writeString($row, $column, $calculatedValue, $xfIndex),
2 ignored issues
show
Bug introduced by
Are you sure the usage of $this->writeString($row,...culatedValue, $xfIndex) targeting PhpOffice\PhpSpreadsheet...orksheet::writeString() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
It seems like $calculatedValue can also be of type array; however, parameter $str of PhpOffice\PhpSpreadsheet...orksheet::writeString() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

422
                                'string' => $this->writeString($row, $column, /** @scrutinizer ignore-type */ $calculatedValue, $xfIndex),
Loading history...
423 7
                                'boolean' => $this->writeBoolErr($row, $column, (int) $calculatedValue, 0, $xfIndex),
424 7
                                default => $this->writeString($row, $column, $cVal, $xfIndex),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->writeString($row,...olumn, $cVal, $xfIndex) targeting PhpOffice\PhpSpreadsheet...orksheet::writeString() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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