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

Worksheet::writeObjPicture()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 63
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 56
nc 1
nop 8
dl 0
loc 63
ccs 0
cts 57
cp 0
crap 2
rs 8.9599
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5
use GdImage;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Cell\DataType;
9
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
10
use PhpOffice\PhpSpreadsheet\RichText\RichText;
11
use PhpOffice\PhpSpreadsheet\RichText\Run;
12
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
13
use PhpOffice\PhpSpreadsheet\Shared\Xls;
14
use PhpOffice\PhpSpreadsheet\Style\Border;
15
use PhpOffice\PhpSpreadsheet\Style\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