Passed
Pull Request — master (#4152)
by Owen
12:45
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
eloc 56
c 0
b 0
f 0
dl 0
loc 63
rs 8.9599
ccs 0
cts 46
cp 0
cc 1
nc 1
nop 8
crap 2

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