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