Completed
Push — master ( 5fb76c...11bae5 )
by Mark
32s queued 28s
created

Workbook::addFont()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 1
dl 0
loc 17
ccs 11
cts 11
cp 1
crap 3
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7
use PhpOffice\PhpSpreadsheet\DefinedName;
8
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
9
use PhpOffice\PhpSpreadsheet\Shared\Date;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use PhpOffice\PhpSpreadsheet\Style\Style;
13
14
// Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
15
// -----------------------------------------------------------------------------------------
16
// /*
17
// *  Module written/ported by Xavier Noguer <[email protected]>
18
// *
19
// *  The majority of this is _NOT_ my code.  I simply ported it from the
20
// *  PERL Spreadsheet::WriteExcel module.
21
// *
22
// *  The author of the Spreadsheet::WriteExcel module is John McNamara
23
// *  <[email protected]>
24
// *
25
// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
26
// *  porting of this code to PHP.  Any questions directly related to this
27
// *  class library should be directed to me.
28
// *
29
// *  License Information:
30
// *
31
// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
32
// *    Copyright (c) 2002-2003 Xavier Noguer [email protected]
33
// *
34
// *    This library is free software; you can redistribute it and/or
35
// *    modify it under the terms of the GNU Lesser General Public
36
// *    License as published by the Free Software Foundation; either
37
// *    version 2.1 of the License, or (at your option) any later version.
38
// *
39
// *    This library is distributed in the hope that it will be useful,
40
// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
41
// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
42
// *    Lesser General Public License for more details.
43
// *
44
// *    You should have received a copy of the GNU Lesser General Public
45
// *    License along with this library; if not, write to the Free Software
46
// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
47
// */
48
class Workbook extends BIFFwriter
49
{
50
    /**
51
     * Formula parser.
52
     *
53
     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
54
     */
55
    private $parser;
56
57
    /**
58
     * The BIFF file size for the workbook. Not currently used.
59
     *
60
     * @var int
61
     *
62
     * @see calcSheetOffsets()
63
     */
64
    private $biffSize; // @phpstan-ignore-line
65
66
    /**
67
     * XF Writers.
68
     *
69
     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
70
     */
71
    private $xfWriters = [];
72
73
    /**
74
     * Array containing the colour palette.
75
     *
76
     * @var array
77
     */
78
    private $palette;
79
80
    /**
81
     * The codepage indicates the text encoding used for strings.
82
     *
83
     * @var int
84
     */
85
    private $codepage;
86
87
    /**
88
     * The country code used for localization.
89
     *
90
     * @var int
91
     */
92
    private $countryCode;
93
94
    /**
95
     * Workbook.
96
     *
97
     * @var Spreadsheet
98
     */
99
    private $spreadsheet;
100
101
    /**
102
     * Fonts writers.
103
     *
104
     * @var Font[]
105
     */
106
    private $fontWriters = [];
107
108
    /**
109
     * Added fonts. Maps from font's hash => index in workbook.
110
     *
111
     * @var array
112
     */
113
    private $addedFonts = [];
114
115
    /**
116
     * Shared number formats.
117
     *
118
     * @var array
119
     */
120
    private $numberFormats = [];
121
122
    /**
123
     * Added number formats. Maps from numberFormat's hash => index in workbook.
124
     *
125
     * @var array
126
     */
127
    private $addedNumberFormats = [];
128
129
    /**
130
     * Sizes of the binary worksheet streams.
131
     *
132
     * @var array
133
     */
134
    private $worksheetSizes = [];
135
136
    /**
137
     * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
138
     *
139
     * @var array
140
     */
141
    private $worksheetOffsets = [];
142
143
    /**
144
     * Total number of shared strings in workbook.
145
     *
146
     * @var int
147
     */
148
    private $stringTotal;
149
150
    /**
151
     * Number of unique shared strings in workbook.
152
     *
153
     * @var int
154
     */
155
    private $stringUnique;
156
157
    /**
158
     * Array of unique shared strings in workbook.
159
     *
160
     * @var array
161
     */
162
    private $stringTable;
163
164
    /**
165
     * Color cache.
166
     *
167
     * @var array
168
     */
169
    private $colors;
170
171
    /**
172
     * Escher object corresponding to MSODRAWINGGROUP.
173
     *
174
     * @var null|\PhpOffice\PhpSpreadsheet\Shared\Escher
175
     */
176
    private $escher;
177
178
    /** @var mixed */
179
    private static $scrutinizerFalse = false;
180
181
    /**
182
     * Class constructor.
183
     *
184
     * @param Spreadsheet $spreadsheet The Workbook
185
     * @param int $str_total Total number of strings
186
     * @param int $str_unique Total number of unique strings
187
     * @param array $str_table String Table
188
     * @param array $colors Colour Table
189
     * @param Parser $parser The formula parser created for the Workbook
190
     */
191 97
    public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
192
    {
193
        // It needs to call its parent's constructor explicitly
194 97
        parent::__construct();
195
196 97
        $this->parser = $parser;
197 97
        $this->biffSize = 0;
198 97
        $this->palette = [];
199 97
        $this->countryCode = -1;
200
201 97
        $this->stringTotal = &$str_total;
202 97
        $this->stringUnique = &$str_unique;
203 97
        $this->stringTable = &$str_table;
204 97
        $this->colors = &$colors;
205 97
        $this->setPaletteXl97();
206
207 97
        $this->spreadsheet = $spreadsheet;
208
209 97
        $this->codepage = 0x04B0;
210
211
        // Add empty sheets and Build color cache
212 97
        $countSheets = $spreadsheet->getSheetCount();
213 97
        for ($i = 0; $i < $countSheets; ++$i) {
214 97
            $phpSheet = $spreadsheet->getSheet($i);
215
216 97
            $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
217
218 97
            $supbook_index = 0x00;
219 97
            $ref = pack('vvv', $supbook_index, $i, $i);
220 97
            $this->parser->references[] = $ref; // Register reference with parser
221
222
            // Sheet tab colors?
223 97
            if ($phpSheet->isTabColorSet()) {
224 6
                $this->addColor($phpSheet->getTabColor()->getRGB());
225
            }
226
        }
227
    }
228
229
    /**
230
     * Add a new XF writer.
231
     *
232
     * @param bool $isStyleXf Is it a style XF?
233
     *
234
     * @return int Index to XF record
235
     */
236 88
    public function addXfWriter(Style $style, $isStyleXf = false)
237
    {
238 88
        $xfWriter = new Xf($style);
239 88
        $xfWriter->setIsStyleXf($isStyleXf);
240
241
        // Add the font if not already added
242 88
        $fontIndex = $this->addFont($style->getFont());
243
244
        // Assign the font index to the xf record
245 88
        $xfWriter->setFontIndex($fontIndex);
246
247
        // Background colors, best to treat these after the font so black will come after white in custom palette
248 88
        $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
249 88
        $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
250 88
        $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
251 88
        $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
252 88
        $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
253 88
        $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
254 88
        $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
255
256
        // Add the number format if it is not a built-in one and not already added
257 88
        if ($style->getNumberFormat()->getBuiltInFormatCode() === self::$scrutinizerFalse) {
258 21
            $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
259
260 21
            if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
261 10
                $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
262
            } else {
263 21
                $numberFormatIndex = 164 + count($this->numberFormats);
264 21
                $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
265 21
                $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
266
            }
267
        } else {
268 88
            $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
269
        }
270
271
        // Assign the number format index to xf record
272 88
        $xfWriter->setNumberFormatIndex($numberFormatIndex);
273
274 88
        $this->xfWriters[] = $xfWriter;
275
276 88
        return count($this->xfWriters) - 1;
277
    }
278
279
    /**
280
     * Add a font to added fonts.
281
     *
282
     * @return int Index to FONT record
283
     */
284 88
    public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
285
    {
286 88
        $fontHashCode = $font->getHashCode();
287 88
        if (isset($this->addedFonts[$fontHashCode])) {
288 88
            $fontIndex = $this->addedFonts[$fontHashCode];
289
        } else {
290 88
            $countFonts = count($this->fontWriters);
291 88
            $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
292
293 88
            $fontWriter = new Font($font);
294 88
            $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
295 88
            $this->fontWriters[] = $fontWriter;
296
297 88
            $this->addedFonts[$fontHashCode] = $fontIndex;
298
        }
299
300 88
        return $fontIndex;
301
    }
302
303
    /**
304
     * Alter color palette adding a custom color.
305
     *
306
     * @param string $rgb E.g. 'FF00AA'
307
     *
308
     * @return int Color index
309
     */
310 97
    private function addColor($rgb)
311
    {
312 97
        if (!isset($this->colors[$rgb])) {
313 97
            $color =
314 97
                [
315 97
                    hexdec(substr($rgb, 0, 2)),
316 97
                    hexdec(substr($rgb, 2, 2)),
317 97
                    hexdec(substr($rgb, 4)),
318 97
                    0,
319 97
                ];
320 97
            $colorIndex = array_search($color, $this->palette);
321 97
            if ($colorIndex) {
322 94
                $this->colors[$rgb] = $colorIndex;
323
            } else {
324 21
                if (count($this->colors) === 0) {
325 10
                    $lastColor = 7;
326
                } else {
327 19
                    $lastColor = end($this->colors);
328
                }
329 21
                if ($lastColor < 57) {
330
                    // then we add a custom color altering the palette
331 19
                    $colorIndex = $lastColor + 1;
332 19
                    $this->palette[$colorIndex] = $color;
333 19
                    $this->colors[$rgb] = $colorIndex;
334
                } else {
335
                    // no room for more custom colors, just map to black
336 97
                    $colorIndex = 0;
337
                }
338
            }
339
        } else {
340
            // fetch already added custom color
341 90
            $colorIndex = $this->colors[$rgb];
342
        }
343
344 97
        return $colorIndex;
345
    }
346
347
    /**
348
     * Sets the colour palette to the Excel 97+ default.
349
     */
350 97
    private function setPaletteXl97(): void
351
    {
352 97
        $this->palette = [
353 97
            0x08 => [0x00, 0x00, 0x00, 0x00],
354 97
            0x09 => [0xff, 0xff, 0xff, 0x00],
355 97
            0x0A => [0xff, 0x00, 0x00, 0x00],
356 97
            0x0B => [0x00, 0xff, 0x00, 0x00],
357 97
            0x0C => [0x00, 0x00, 0xff, 0x00],
358 97
            0x0D => [0xff, 0xff, 0x00, 0x00],
359 97
            0x0E => [0xff, 0x00, 0xff, 0x00],
360 97
            0x0F => [0x00, 0xff, 0xff, 0x00],
361 97
            0x10 => [0x80, 0x00, 0x00, 0x00],
362 97
            0x11 => [0x00, 0x80, 0x00, 0x00],
363 97
            0x12 => [0x00, 0x00, 0x80, 0x00],
364 97
            0x13 => [0x80, 0x80, 0x00, 0x00],
365 97
            0x14 => [0x80, 0x00, 0x80, 0x00],
366 97
            0x15 => [0x00, 0x80, 0x80, 0x00],
367 97
            0x16 => [0xc0, 0xc0, 0xc0, 0x00],
368 97
            0x17 => [0x80, 0x80, 0x80, 0x00],
369 97
            0x18 => [0x99, 0x99, 0xff, 0x00],
370 97
            0x19 => [0x99, 0x33, 0x66, 0x00],
371 97
            0x1A => [0xff, 0xff, 0xcc, 0x00],
372 97
            0x1B => [0xcc, 0xff, 0xff, 0x00],
373 97
            0x1C => [0x66, 0x00, 0x66, 0x00],
374 97
            0x1D => [0xff, 0x80, 0x80, 0x00],
375 97
            0x1E => [0x00, 0x66, 0xcc, 0x00],
376 97
            0x1F => [0xcc, 0xcc, 0xff, 0x00],
377 97
            0x20 => [0x00, 0x00, 0x80, 0x00],
378 97
            0x21 => [0xff, 0x00, 0xff, 0x00],
379 97
            0x22 => [0xff, 0xff, 0x00, 0x00],
380 97
            0x23 => [0x00, 0xff, 0xff, 0x00],
381 97
            0x24 => [0x80, 0x00, 0x80, 0x00],
382 97
            0x25 => [0x80, 0x00, 0x00, 0x00],
383 97
            0x26 => [0x00, 0x80, 0x80, 0x00],
384 97
            0x27 => [0x00, 0x00, 0xff, 0x00],
385 97
            0x28 => [0x00, 0xcc, 0xff, 0x00],
386 97
            0x29 => [0xcc, 0xff, 0xff, 0x00],
387 97
            0x2A => [0xcc, 0xff, 0xcc, 0x00],
388 97
            0x2B => [0xff, 0xff, 0x99, 0x00],
389 97
            0x2C => [0x99, 0xcc, 0xff, 0x00],
390 97
            0x2D => [0xff, 0x99, 0xcc, 0x00],
391 97
            0x2E => [0xcc, 0x99, 0xff, 0x00],
392 97
            0x2F => [0xff, 0xcc, 0x99, 0x00],
393 97
            0x30 => [0x33, 0x66, 0xff, 0x00],
394 97
            0x31 => [0x33, 0xcc, 0xcc, 0x00],
395 97
            0x32 => [0x99, 0xcc, 0x00, 0x00],
396 97
            0x33 => [0xff, 0xcc, 0x00, 0x00],
397 97
            0x34 => [0xff, 0x99, 0x00, 0x00],
398 97
            0x35 => [0xff, 0x66, 0x00, 0x00],
399 97
            0x36 => [0x66, 0x66, 0x99, 0x00],
400 97
            0x37 => [0x96, 0x96, 0x96, 0x00],
401 97
            0x38 => [0x00, 0x33, 0x66, 0x00],
402 97
            0x39 => [0x33, 0x99, 0x66, 0x00],
403 97
            0x3A => [0x00, 0x33, 0x00, 0x00],
404 97
            0x3B => [0x33, 0x33, 0x00, 0x00],
405 97
            0x3C => [0x99, 0x33, 0x00, 0x00],
406 97
            0x3D => [0x99, 0x33, 0x66, 0x00],
407 97
            0x3E => [0x33, 0x33, 0x99, 0x00],
408 97
            0x3F => [0x33, 0x33, 0x33, 0x00],
409 97
        ];
410
    }
411
412
    /**
413
     * Assemble worksheets into a workbook and send the BIFF data to an OLE
414
     * storage.
415
     *
416
     * @param array $worksheetSizes The sizes in bytes of the binary worksheet streams
417
     *
418
     * @return string Binary data for workbook stream
419
     */
420 88
    public function writeWorkbook(array $worksheetSizes)
421
    {
422 88
        $this->worksheetSizes = $worksheetSizes;
423
424
        // Calculate the number of selected worksheet tabs and call the finalization
425
        // methods for each worksheet
426 88
        $total_worksheets = $this->spreadsheet->getSheetCount();
427
428
        // Add part 1 of the Workbook globals, what goes before the SHEET records
429 88
        $this->storeBof(0x0005);
430 88
        $this->writeCodepage();
431 88
        $this->writeWindow1();
432
433 88
        $this->writeDateMode();
434 88
        $this->writeAllFonts();
435 88
        $this->writeAllNumberFormats();
436 88
        $this->writeAllXfs();
437 88
        $this->writeAllStyles();
438 88
        $this->writePalette();
439
440
        // Prepare part 3 of the workbook global stream, what goes after the SHEET records
441 88
        $part3 = '';
442 88
        if ($this->countryCode !== -1) {
443
            $part3 .= $this->writeCountry();
444
        }
445 88
        $part3 .= $this->writeRecalcId();
446
447 88
        $part3 .= $this->writeSupbookInternal();
448
        /* TODO: store external SUPBOOK records and XCT and CRN records
449
        in case of external references for BIFF8 */
450 88
        $part3 .= $this->writeExternalsheetBiff8();
451 88
        $part3 .= $this->writeAllDefinedNamesBiff8();
452 88
        $part3 .= $this->writeMsoDrawingGroup();
453 88
        $part3 .= $this->writeSharedStringsTable();
454
455 88
        $part3 .= $this->writeEof();
456
457
        // Add part 2 of the Workbook globals, the SHEET records
458 88
        $this->calcSheetOffsets();
459 88
        for ($i = 0; $i < $total_worksheets; ++$i) {
460 88
            $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
461
        }
462
463
        // Add part 3 of the Workbook globals
464 88
        $this->_data .= $part3;
465
466 88
        return $this->_data;
467
    }
468
469
    /**
470
     * Calculate offsets for Worksheet BOF records.
471
     */
472 88
    private function calcSheetOffsets(): void
473
    {
474 88
        $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
475
476
        // size of Workbook globals part 1 + 3
477 88
        $offset = $this->_datasize;
478
479
        // add size of Workbook globals part 2, the length of the SHEET records
480 88
        $total_worksheets = count($this->spreadsheet->getAllSheets());
481 88
        foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
482 88
            $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
483
        }
484
485
        // add the sizes of each of the Sheet substreams, respectively
486 88
        for ($i = 0; $i < $total_worksheets; ++$i) {
487 88
            $this->worksheetOffsets[$i] = $offset;
488 88
            $offset += $this->worksheetSizes[$i];
489
        }
490 88
        $this->biffSize = $offset;
491
    }
492
493
    /**
494
     * Store the Excel FONT records.
495
     */
496 88
    private function writeAllFonts(): void
497
    {
498 88
        foreach ($this->fontWriters as $fontWriter) {
499 88
            $this->append($fontWriter->writeFont());
500
        }
501
    }
502
503
    /**
504
     * Store user defined numerical formats i.e. FORMAT records.
505
     */
506 88
    private function writeAllNumberFormats(): void
507
    {
508 88
        foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
509 21
            $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
510
        }
511
    }
512
513
    /**
514
     * Write all XF records.
515
     */
516 88
    private function writeAllXfs(): void
517
    {
518 88
        foreach ($this->xfWriters as $xfWriter) {
519 88
            $this->append($xfWriter->writeXf());
520
        }
521
    }
522
523
    /**
524
     * Write all STYLE records.
525
     */
526 88
    private function writeAllStyles(): void
527
    {
528 88
        $this->writeStyle();
529
    }
530
531 7
    private function parseDefinedNameValue(DefinedName $definedName): string
532
    {
533 7
        $definedRange = $definedName->getValue();
534 7
        $splitCount = preg_match_all(
535 7
            '/' . Calculation::CALCULATION_REGEXP_CELLREF . '/mui',
536 7
            $definedRange,
537 7
            $splitRanges,
538 7
            PREG_OFFSET_CAPTURE
539 7
        );
540
541 7
        $lengths = array_map('strlen', array_column($splitRanges[0], 0));
542 7
        $offsets = array_column($splitRanges[0], 1);
543
544 7
        $worksheets = $splitRanges[2];
545 7
        $columns = $splitRanges[6];
546 7
        $rows = $splitRanges[7];
547
548 7
        while ($splitCount > 0) {
549 7
            --$splitCount;
550 7
            $length = $lengths[$splitCount];
551 7
            $offset = $offsets[$splitCount];
552 7
            $worksheet = $worksheets[$splitCount][0];
553 7
            $column = $columns[$splitCount][0];
554 7
            $row = $rows[$splitCount][0];
555
556 7
            $newRange = '';
557 7
            if (empty($worksheet)) {
558 6
                if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) {
559
                    // We should have a worksheet
560 6
                    $worksheet = $definedName->getWorksheet() ? $definedName->getWorksheet()->getTitle() : null;
561
                }
562
            } else {
563 4
                $worksheet = str_replace("''", "'", trim($worksheet, "'"));
564
            }
565 7
            if (!empty($worksheet)) {
566 7
                $newRange = "'" . str_replace("'", "''", $worksheet) . "'!";
567
            }
568
569 7
            if (!empty($column)) {
570 7
                $newRange .= "\${$column}";
571
            }
572 7
            if (!empty($row)) {
573 7
                $newRange .= "\${$row}";
574
            }
575
576 7
            $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length);
577
        }
578
579 7
        return $definedRange;
580
    }
581
582
    /**
583
     * Writes all the DEFINEDNAME records (BIFF8).
584
     * So far this is only used for repeating rows/columns (print titles) and print areas.
585
     */
586 88
    private function writeAllDefinedNamesBiff8(): string
587
    {
588 88
        $chunk = '';
589
590
        // Named ranges
591 88
        $definedNames = $this->spreadsheet->getDefinedNames();
592 88
        if (count($definedNames) > 0) {
593
            // Loop named ranges
594 7
            foreach ($definedNames as $definedName) {
595 7
                $range = $this->parseDefinedNameValue($definedName);
596
597
                // parse formula
598
                try {
599 7
                    $this->parser->parse($range);
600 7
                    $formulaData = $this->parser->toReversePolish();
601
602
                    // make sure tRef3d is of type tRef3dR (0x3A)
603 6
                    if (isset($formulaData[0]) && ($formulaData[0] == "\x7A" || $formulaData[0] == "\x5A")) {
604 5
                        $formulaData = "\x3A" . substr($formulaData, 1);
605
                    }
606
607 6
                    if ($definedName->getLocalOnly()) {
608
                        // local scope
609 1
                        $scopeWs = $definedName->getScope();
610 1
                        $scope = ($scopeWs === null) ? 0 : ($this->spreadsheet->getIndex($scopeWs) + 1);
611
                    } else {
612
                        // global scope
613 6
                        $scope = 0;
614
                    }
615 6
                    $chunk .= $this->writeData($this->writeDefinedNameBiff8($definedName->getName(), $formulaData, $scope, false));
616 1
                } catch (PhpSpreadsheetException $e) {
617
                    // do nothing
618
                }
619
            }
620
        }
621
622
        // total number of sheets
623 88
        $total_worksheets = $this->spreadsheet->getSheetCount();
624
625
        // write the print titles (repeating rows, columns), if any
626 88
        for ($i = 0; $i < $total_worksheets; ++$i) {
627 88
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
628
            // simultaneous repeatColumns repeatRows
629 88
            if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
630
                $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
631
                $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
632
                $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
633
634
                $repeat = $sheetSetup->getRowsToRepeatAtTop();
635
                $rowmin = $repeat[0] - 1;
636
                $rowmax = $repeat[1] - 1;
637
638
                // construct formula data manually
639
                $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
640
                $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
641
                $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
642
                $formulaData .= pack('C', 0x10); // tList
643
644
                // store the DEFINEDNAME record
645
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
646
647
            // (exclusive) either repeatColumns or repeatRows
648 88
            } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
649
                // Columns to repeat
650 2
                if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
651
                    $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
652
                    $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
653
                    $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
654
                } else {
655 2
                    $colmin = 0;
656 2
                    $colmax = 255;
657
                }
658
                // Rows to repeat
659 2
                if ($sheetSetup->isRowsToRepeatAtTopSet()) {
660 2
                    $repeat = $sheetSetup->getRowsToRepeatAtTop();
661 2
                    $rowmin = $repeat[0] - 1;
662 2
                    $rowmax = $repeat[1] - 1;
663
                } else {
664
                    $rowmin = 0;
665
                    $rowmax = 65535;
666
                }
667
668
                // construct formula data manually because parser does not recognize absolute 3d cell references
669 2
                $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
670
671
                // store the DEFINEDNAME record
672 2
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
673
            }
674
        }
675
676
        // write the print areas, if any
677 88
        for ($i = 0; $i < $total_worksheets; ++$i) {
678 88
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
679 88
            if ($sheetSetup->isPrintAreaSet()) {
680
                // Print area, e.g. A3:J6,H1:X20
681 4
                $printArea = Coordinate::splitRange($sheetSetup->getPrintArea());
682 4
                $countPrintArea = count($printArea);
683
684 4
                $formulaData = '';
685 4
                for ($j = 0; $j < $countPrintArea; ++$j) {
686 4
                    $printAreaRect = $printArea[$j]; // e.g. A3:J6
687 4
                    $printAreaRect[0] = Coordinate::indexesFromString($printAreaRect[0]);
688 4
                    $printAreaRect[1] = Coordinate::indexesFromString($printAreaRect[1]);
689
690 4
                    $print_rowmin = $printAreaRect[0][1] - 1;
691 4
                    $print_rowmax = $printAreaRect[1][1] - 1;
692 4
                    $print_colmin = $printAreaRect[0][0] - 1;
693 4
                    $print_colmax = $printAreaRect[1][0] - 1;
694
695
                    // construct formula data manually because parser does not recognize absolute 3d cell references
696 4
                    $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
697
698 4
                    if ($j > 0) {
699 1
                        $formulaData .= pack('C', 0x10); // list operator token ','
700
                    }
701
                }
702
703
                // store the DEFINEDNAME record
704 4
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
705
            }
706
        }
707
708
        // write autofilters, if any
709 88
        for ($i = 0; $i < $total_worksheets; ++$i) {
710 88
            $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
711 88
            $autoFilterRange = $sheetAutoFilter->getRange();
712 88
            if (!empty($autoFilterRange)) {
713 4
                $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange);
714
715
                //Autofilter built in name
716 4
                $name = pack('C', 0x0D);
717
718 4
                $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
719
            }
720
        }
721
722 88
        return $chunk;
723
    }
724
725
    /**
726
     * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
727
     *
728
     * @param string $name The name in UTF-8
729
     * @param string $formulaData The binary formula data
730
     * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
731
     * @param bool $isBuiltIn Built-in name?
732
     *
733
     * @return string Complete binary record data
734
     */
735 10
    private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
736
    {
737 10
        $record = 0x0018;
738
739
        // option flags
740 10
        $options = $isBuiltIn ? 0x20 : 0x00;
741
742
        // length of the name, character count
743 10
        $nlen = StringHelper::countCharacters($name);
744
745
        // name with stripped length field
746 10
        $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
747
748
        // size of the formula (in bytes)
749 10
        $sz = strlen($formulaData);
750
751
        // combine the parts
752 10
        $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
753 10
            . $name . $formulaData;
754 10
        $length = strlen($data);
755
756 10
        $header = pack('vv', $record, $length);
757
758 10
        return $header . $data;
759
    }
760
761
    /**
762
     * Write a short NAME record.
763
     *
764
     * @param string $name
765
     * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
766
     * @param int[][] $rangeBounds range boundaries
767
     * @param bool $isHidden
768
     *
769
     * @return string Complete binary record data
770
     * */
771 4
    private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
772
    {
773 4
        $record = 0x0018;
774
775
        // option flags
776 4
        $options = ($isHidden ? 0x21 : 0x00);
777
778 4
        $extra = pack(
779 4
            'Cvvvvv',
780 4
            0x3B,
781 4
            $sheetIndex - 1,
782 4
            $rangeBounds[0][1] - 1,
783 4
            $rangeBounds[1][1] - 1,
784 4
            $rangeBounds[0][0] - 1,
785 4
            $rangeBounds[1][0] - 1
786 4
        );
787
788
        // size of the formula (in bytes)
789 4
        $sz = strlen($extra);
790
791
        // combine the parts
792 4
        $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
793 4
            . $name . $extra;
794 4
        $length = strlen($data);
795
796 4
        $header = pack('vv', $record, $length);
797
798 4
        return $header . $data;
799
    }
800
801
    /**
802
     * Stores the CODEPAGE biff record.
803
     */
804 88
    private function writeCodepage(): void
805
    {
806 88
        $record = 0x0042; // Record identifier
807 88
        $length = 0x0002; // Number of bytes to follow
808 88
        $cv = $this->codepage; // The code page
809
810 88
        $header = pack('vv', $record, $length);
811 88
        $data = pack('v', $cv);
812
813 88
        $this->append($header . $data);
814
    }
815
816
    /**
817
     * Write Excel BIFF WINDOW1 record.
818
     */
819 88
    private function writeWindow1(): void
820
    {
821 88
        $record = 0x003D; // Record identifier
822 88
        $length = 0x0012; // Number of bytes to follow
823
824 88
        $xWn = 0x0000; // Horizontal position of window
825 88
        $yWn = 0x0000; // Vertical position of window
826 88
        $dxWn = 0x25BC; // Width of window
827 88
        $dyWn = 0x1572; // Height of window
828
829 88
        $grbit = 0x0038; // Option flags
830
831
        // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
832 88
        $ctabsel = 1; // Number of workbook tabs selected
833
834 88
        $wTabRatio = 0x0258; // Tab to scrollbar ratio
835
836
        // not supported by PhpSpreadsheet, set to 0
837 88
        $itabFirst = 0; // 1st displayed worksheet
838 88
        $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
839
840 88
        $header = pack('vv', $record, $length);
841 88
        $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
842 88
        $this->append($header . $data);
843
    }
844
845
    /**
846
     * Writes Excel BIFF BOUNDSHEET record.
847
     *
848
     * @param int $offset Location of worksheet BOF
849
     */
850 88
    private function writeBoundSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, $offset): void
851
    {
852 88
        $sheetname = $sheet->getTitle();
853 88
        $record = 0x0085; // Record identifier
854
855
        // sheet state
856 88
        switch ($sheet->getSheetState()) {
857
            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE:
858 88
                $ss = 0x00;
859
860 88
                break;
861
            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN:
862 1
                $ss = 0x01;
863
864 1
                break;
865
            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN:
866 1
                $ss = 0x02;
867
868 1
                break;
869
            default:
870
                $ss = 0x00;
871
872
                break;
873
        }
874
875
        // sheet type
876 88
        $st = 0x00;
877
878
        //$grbit = 0x0000; // Visibility and sheet type
879
880 88
        $data = pack('VCC', $offset, $ss, $st);
881 88
        $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
882
883 88
        $length = strlen($data);
884 88
        $header = pack('vv', $record, $length);
885 88
        $this->append($header . $data);
886
    }
887
888
    /**
889
     * Write Internal SUPBOOK record.
890
     */
891 88
    private function writeSupbookInternal(): string
892
    {
893 88
        $record = 0x01AE; // Record identifier
894 88
        $length = 0x0004; // Bytes to follow
895
896 88
        $header = pack('vv', $record, $length);
897 88
        $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
898
899 88
        return $this->writeData($header . $data);
900
    }
901
902
    /**
903
     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
904
     * formulas.
905
     */
906 88
    private function writeExternalsheetBiff8(): string
907
    {
908 88
        $totalReferences = count($this->parser->references);
909 88
        $record = 0x0017; // Record identifier
910 88
        $length = 2 + 6 * $totalReferences; // Number of bytes to follow
911
912
        //$supbook_index = 0; // FIXME: only using internal SUPBOOK record
913 88
        $header = pack('vv', $record, $length);
914 88
        $data = pack('v', $totalReferences);
915 88
        for ($i = 0; $i < $totalReferences; ++$i) {
916 88
            $data .= $this->parser->references[$i];
917
        }
918
919 88
        return $this->writeData($header . $data);
920
    }
921
922
    /**
923
     * Write Excel BIFF STYLE records.
924
     */
925 88
    private function writeStyle(): void
926
    {
927 88
        $record = 0x0293; // Record identifier
928 88
        $length = 0x0004; // Bytes to follow
929
930 88
        $ixfe = 0x8000; // Index to cell style XF
931 88
        $BuiltIn = 0x00; // Built-in style
932 88
        $iLevel = 0xff; // Outline style level
933
934 88
        $header = pack('vv', $record, $length);
935 88
        $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
936 88
        $this->append($header . $data);
937
    }
938
939
    /**
940
     * Writes Excel FORMAT record for non "built-in" numerical formats.
941
     *
942
     * @param string $format Custom format string
943
     * @param int $ifmt Format index code
944
     */
945 21
    private function writeNumberFormat($format, $ifmt): void
946
    {
947 21
        $record = 0x041E; // Record identifier
948
949 21
        $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
950 21
        $length = 2 + strlen($numberFormatString); // Number of bytes to follow
951
952 21
        $header = pack('vv', $record, $length);
953 21
        $data = pack('v', $ifmt) . $numberFormatString;
954 21
        $this->append($header . $data);
955
    }
956
957
    /**
958
     * Write DATEMODE record to indicate the date system in use (1904 or 1900).
959
     */
960 88
    private function writeDateMode(): void
961
    {
962 88
        $record = 0x0022; // Record identifier
963 88
        $length = 0x0002; // Bytes to follow
964
965 88
        $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904)
966
            ? 1
967 88
            : 0; // Flag for 1904 date system
968
969 88
        $header = pack('vv', $record, $length);
970 88
        $data = pack('v', $f1904);
971 88
        $this->append($header . $data);
972
    }
973
974
    /**
975
     * Stores the COUNTRY record for localization.
976
     *
977
     * @return string
978
     */
979
    private function writeCountry()
980
    {
981
        $record = 0x008C; // Record identifier
982
        $length = 4; // Number of bytes to follow
983
984
        $header = pack('vv', $record, $length);
985
        // using the same country code always for simplicity
986
        $data = pack('vv', $this->countryCode, $this->countryCode);
987
988
        return $this->writeData($header . $data);
989
    }
990
991
    /**
992
     * Write the RECALCID record.
993
     *
994
     * @return string
995
     */
996 88
    private function writeRecalcId()
997
    {
998 88
        $record = 0x01C1; // Record identifier
999 88
        $length = 8; // Number of bytes to follow
1000
1001 88
        $header = pack('vv', $record, $length);
1002
1003
        // by inspection of real Excel files, MS Office Excel 2007 writes this
1004 88
        $data = pack('VV', 0x000001C1, 0x00001E667);
1005
1006 88
        return $this->writeData($header . $data);
1007
    }
1008
1009
    /**
1010
     * Stores the PALETTE biff record.
1011
     */
1012 88
    private function writePalette(): void
1013
    {
1014 88
        $aref = $this->palette;
1015
1016 88
        $record = 0x0092; // Record identifier
1017 88
        $length = 2 + 4 * count($aref); // Number of bytes to follow
1018 88
        $ccv = count($aref); // Number of RGB values to follow
1019 88
        $data = ''; // The RGB data
1020
1021
        // Pack the RGB data
1022 88
        foreach ($aref as $color) {
1023 88
            foreach ($color as $byte) {
1024 88
                $data .= pack('C', $byte);
1025
            }
1026
        }
1027
1028 88
        $header = pack('vvv', $record, $length, $ccv);
1029 88
        $this->append($header . $data);
1030
    }
1031
1032
    /**
1033
     * Handling of the SST continue blocks is complicated by the need to include an
1034
     * additional continuation byte depending on whether the string is split between
1035
     * blocks or whether it starts at the beginning of the block. (There are also
1036
     * additional complications that will arise later when/if Rich Strings are
1037
     * supported).
1038
     *
1039
     * The Excel documentation says that the SST record should be followed by an
1040
     * EXTSST record. The EXTSST record is a hash table that is used to optimise
1041
     * access to SST. However, despite the documentation it doesn't seem to be
1042
     * required so we will ignore it.
1043
     *
1044
     * @return string Binary data
1045
     */
1046 88
    private function writeSharedStringsTable()
1047
    {
1048
        // maximum size of record data (excluding record header)
1049 88
        $continue_limit = 8224;
1050
1051
        // initialize array of record data blocks
1052 88
        $recordDatas = [];
1053
1054
        // start SST record data block with total number of strings, total number of unique strings
1055 88
        $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
1056
1057
        // loop through all (unique) strings in shared strings table
1058 88
        foreach (array_keys($this->stringTable) as $string) {
1059
            // here $string is a BIFF8 encoded string
1060
1061
            // length = character count
1062 65
            $headerinfo = unpack('vlength/Cencoding', $string);
1063
1064
            // currently, this is always 1 = uncompressed
1065 65
            $encoding = $headerinfo['encoding'] ?? 1;
1066
1067
            // initialize finished writing current $string
1068 65
            $finished = false;
1069
1070 65
            while ($finished === false) {
1071
                // normally, there will be only one cycle, but if string cannot immediately be written as is
1072
                // there will be need for more than one cylcle, if string longer than one record data block, there
1073
                // may be need for even more cycles
1074
1075 65
                if (strlen($recordData) + strlen($string) <= $continue_limit) {
1076
                    // then we can write the string (or remainder of string) without any problems
1077 65
                    $recordData .= $string;
1078
1079 65
                    if (strlen($recordData) + strlen($string) == $continue_limit) {
1080
                        // we close the record data block, and initialize a new one
1081
                        $recordDatas[] = $recordData;
1082
                        $recordData = '';
1083
                    }
1084
1085
                    // we are finished writing this string
1086 65
                    $finished = true;
1087
                } else {
1088
                    // special treatment writing the string (or remainder of the string)
1089
                    // If the string is very long it may need to be written in more than one CONTINUE record.
1090
1091
                    // check how many bytes more there is room for in the current record
1092 1
                    $space_remaining = $continue_limit - strlen($recordData);
1093
1094
                    // minimum space needed
1095
                    // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
1096
                    // compressed:   2 byte string length length field + 1 byte option flags + 1 byte character
1097 1
                    $min_space_needed = ($encoding == 1) ? 5 : 4;
1098
1099
                    // We have two cases
1100
                    // 1. space remaining is less than minimum space needed
1101
                    //        here we must waste the space remaining and move to next record data block
1102
                    // 2. space remaining is greater than or equal to minimum space needed
1103
                    //        here we write as much as we can in the current block, then move to next record data block
1104
1105
                    // 1. space remaining is less than minimum space needed
1106 1
                    if ($space_remaining < $min_space_needed) {
1107
                        // we close the block, store the block data
1108
                        $recordDatas[] = $recordData;
1109
1110
                        // and start new record data block where we start writing the string
1111
                        $recordData = '';
1112
1113
                    // 2. space remaining is greater than or equal to minimum space needed
1114
                    } else {
1115
                        // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
1116 1
                        $effective_space_remaining = $space_remaining;
1117
1118
                        // for uncompressed strings, sometimes effective space remaining is reduced by 1
1119 1
                        if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
1120 1
                            --$effective_space_remaining;
1121
                        }
1122
1123
                        // one block fininshed, store the block data
1124 1
                        $recordData .= substr($string, 0, $effective_space_remaining);
1125
1126 1
                        $string = substr($string, $effective_space_remaining); // for next cycle in while loop
1127 1
                        $recordDatas[] = $recordData;
1128
1129
                        // start new record data block with the repeated option flags
1130 1
                        $recordData = pack('C', $encoding);
1131
                    }
1132
                }
1133
            }
1134
        }
1135
1136
        // Store the last record data block unless it is empty
1137
        // if there was no need for any continue records, this will be the for SST record data block itself
1138 88
        if (strlen($recordData) > 0) {
1139 88
            $recordDatas[] = $recordData;
1140
        }
1141
1142
        // combine into one chunk with all the blocks SST, CONTINUE,...
1143 88
        $chunk = '';
1144 88
        foreach ($recordDatas as $i => $recordData) {
1145
            // first block should have the SST record header, remaing should have CONTINUE header
1146 88
            $record = ($i == 0) ? 0x00FC : 0x003C;
1147
1148 88
            $header = pack('vv', $record, strlen($recordData));
1149 88
            $data = $header . $recordData;
1150
1151 88
            $chunk .= $this->writeData($data);
1152
        }
1153
1154 88
        return $chunk;
1155
    }
1156
1157
    /**
1158
     * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
1159
     */
1160 88
    private function writeMsoDrawingGroup(): string
1161
    {
1162
        // write the Escher stream if necessary
1163 88
        if (isset($this->escher)) {
1164 15
            $writer = new Escher($this->escher);
1165 15
            $data = $writer->close();
1166
1167 15
            $record = 0x00EB;
1168 15
            $length = strlen($data);
1169 15
            $header = pack('vv', $record, $length);
1170
1171 15
            return $this->writeData($header . $data);
1172
        }
1173
1174 73
        return '';
1175
    }
1176
1177
    /**
1178
     * Get Escher object.
1179
     */
1180
    public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher
1181
    {
1182
        return $this->escher;
1183
    }
1184
1185
    /**
1186
     * Set Escher object.
1187
     */
1188 15
    public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void
1189
    {
1190 15
        $this->escher = $escher;
1191
    }
1192
}
1193