Passed
Pull Request — master (#4240)
by Owen
27:46 queued 17:34
created

Worksheet::writeObjPicture()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 63
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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