Worksheet::__construct()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 44
rs 9.488
ccs 28
cts 28
cp 1
cc 3
nc 4
nop 8
crap 3

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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