Passed
Push — master ( 8fc7d9...eeb858 )
by Mark
14:21
created

Worksheet::writeObjPicture()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 63
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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