Completed
Push — develop ( 29208e...50a0ec )
by Adrien
05:34
created

Workbook::__construct()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 3.0007

Importance

Changes 0
Metric Value
cc 3
eloc 22
nc 3
nop 6
dl 0
loc 37
rs 8.8571
c 0
b 0
f 0
ccs 22
cts 23
cp 0.9565
crap 3.0007
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5
use PhpOffice\PhpSpreadsheet\Cell;
6
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
7
use PhpOffice\PhpSpreadsheet\Shared\Date;
8
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
9
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10
11
// Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
12
// -----------------------------------------------------------------------------------------
13
// /*
14
// *  Module written/ported by Xavier Noguer <[email protected]>
15
// *
16
// *  The majority of this is _NOT_ my code.  I simply ported it from the
17
// *  PERL Spreadsheet::WriteExcel module.
18
// *
19
// *  The author of the Spreadsheet::WriteExcel module is John McNamara
20
// *  <[email protected]>
21
// *
22
// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
23
// *  porting of this code to PHP.  Any questions directly related to this
24
// *  class library should be directed to me.
25
// *
26
// *  License Information:
27
// *
28
// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
29
// *    Copyright (c) 2002-2003 Xavier Noguer [email protected]
30
// *
31
// *    This library is free software; you can redistribute it and/or
32
// *    modify it under the terms of the GNU Lesser General Public
33
// *    License as published by the Free Software Foundation; either
34
// *    version 2.1 of the License, or (at your option) any later version.
35
// *
36
// *    This library is distributed in the hope that it will be useful,
37
// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
38
// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
39
// *    Lesser General Public License for more details.
40
// *
41
// *    You should have received a copy of the GNU Lesser General Public
42
// *    License along with this library; if not, write to the Free Software
43
// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
44
// */
45
class Workbook extends BIFFwriter
46
{
47
    /**
48
     * Formula parser.
49
     *
50
     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
51
     */
52
    private $parser;
53
54
    /**
55
     * The BIFF file size for the workbook.
56
     *
57
     * @var int
58
     *
59
     * @see calcSheetOffsets()
60
     */
61
    private $biffSize;
62
63
    /**
64
     * XF Writers.
65
     *
66
     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
67
     */
68
    private $xfWriters = [];
69
70
    /**
71
     * Array containing the colour palette.
72
     *
73
     * @var array
74
     */
75
    private $palette;
76
77
    /**
78
     * The codepage indicates the text encoding used for strings.
79
     *
80
     * @var int
81
     */
82
    private $codepage;
83
84
    /**
85
     * The country code used for localization.
86
     *
87
     * @var int
88
     */
89
    private $countryCode;
90
91
    /**
92
     * Workbook.
93
     *
94
     * @var Spreadsheet
95
     */
96
    private $spreadsheet;
97
98
    /**
99
     * Fonts writers.
100
     *
101
     * @var Font[]
102
     */
103
    private $fontWriters = [];
104
105
    /**
106
     * Added fonts. Maps from font's hash => index in workbook.
107
     *
108
     * @var array
109
     */
110
    private $addedFonts = [];
111
112
    /**
113
     * Shared number formats.
114
     *
115
     * @var array
116
     */
117
    private $numberFormats = [];
118
119
    /**
120
     * Added number formats. Maps from numberFormat's hash => index in workbook.
121
     *
122
     * @var array
123
     */
124
    private $addedNumberFormats = [];
125
126
    /**
127
     * Sizes of the binary worksheet streams.
128
     *
129
     * @var array
130
     */
131
    private $worksheetSizes = [];
132
133
    /**
134
     * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
135
     *
136
     * @var array
137
     */
138
    private $worksheetOffsets = [];
139
140
    /**
141
     * Total number of shared strings in workbook.
142
     *
143
     * @var int
144
     */
145
    private $stringTotal;
146
147
    /**
148
     * Number of unique shared strings in workbook.
149
     *
150
     * @var int
151
     */
152
    private $stringUnique;
153
154
    /**
155
     * Array of unique shared strings in workbook.
156
     *
157
     * @var array
158
     */
159
    private $stringTable;
160
161
    /**
162
     * Color cache.
163
     */
164
    private $colors;
165
166
    /**
167
     * Escher object corresponding to MSODRAWINGGROUP.
168
     *
169
     * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
170
     */
171
    private $escher;
172
173
    /**
174
     * Class constructor.
175
     *
176
     * @param Spreadsheet $spreadsheet The Workbook
177
     * @param int $str_total Total number of strings
178
     * @param int $str_unique Total number of unique strings
179
     * @param array $str_table String Table
180
     * @param array $colors Colour Table
181
     * @param Parser $parser The formula parser created for the Workbook
182
     */
183 9
    public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
184
    {
185
        // It needs to call its parent's constructor explicitly
186 9
        parent::__construct();
187
188 9
        $this->parser = $parser;
189 9
        $this->biffSize = 0;
190 9
        $this->palette = [];
191 9
        $this->countryCode = -1;
192
193 9
        $this->stringTotal = &$str_total;
194 9
        $this->stringUnique = &$str_unique;
195 9
        $this->stringTable = &$str_table;
196 9
        $this->colors = &$colors;
197 9
        $this->setPaletteXl97();
198
199 9
        $this->spreadsheet = $spreadsheet;
200
201 9
        $this->codepage = 0x04B0;
202
203
        // Add empty sheets and Build color cache
204 9
        $countSheets = $spreadsheet->getSheetCount();
205 9
        for ($i = 0; $i < $countSheets; ++$i) {
206 9
            $phpSheet = $spreadsheet->getSheet($i);
207
208 9
            $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
209
210 9
            $supbook_index = 0x00;
211 9
            $ref = pack('vvv', $supbook_index, $i, $i);
212 9
            $this->parser->references[] = $ref; // Register reference with parser
213
214
            // Sheet tab colors?
215 9
            if ($phpSheet->isTabColorSet()) {
216
                $this->addColor($phpSheet->getTabColor()->getRGB());
217
            }
218
        }
219 9
    }
220
221
    /**
222
     * Add a new XF writer.
223
     *
224
     * @param \PhpOffice\PhpSpreadsheet\Style
225
     * @param bool Is it a style XF?
226
     * @param mixed $style
227
     * @param mixed $isStyleXf
228
     *
229
     * @return int Index to XF record
230
     */
231
    public function addXfWriter($style, $isStyleXf = false)
232
    {
233
        $xfWriter = new Xf($style);
234
        $xfWriter->setIsStyleXf($isStyleXf);
235
236
        // Add the font if not already added
237
        $fontIndex = $this->addFont($style->getFont());
238
239
        // Assign the font index to the xf record
240
        $xfWriter->setFontIndex($fontIndex);
241
242
        // Background colors, best to treat these after the font so black will come after white in custom palette
243
        $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
244
        $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
245
        $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
246
        $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
247
        $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
248
        $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
249
        $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
250
251
        // Add the number format if it is not a built-in one and not already added
252
        if ($style->getNumberFormat()->getBuiltInFormatCode() === false) {
253
            $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
254
255
            if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
256
                $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
257
            } else {
258
                $numberFormatIndex = 164 + count($this->numberFormats);
259
                $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
260
                $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
261
            }
262
        } else {
263
            $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
264
        }
265
266
        // Assign the number format index to xf record
267
        $xfWriter->setNumberFormatIndex($numberFormatIndex);
268
269
        $this->xfWriters[] = $xfWriter;
270
271
        $xfIndex = count($this->xfWriters) - 1;
272
273
        return $xfIndex;
274
    }
275
276
    /**
277
     * Add a font to added fonts.
278
     *
279
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font
280
     *
281
     * @return int Index to FONT record
282
     */
283
    public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
284
    {
285
        $fontHashCode = $font->getHashCode();
286
        if (isset($this->addedFonts[$fontHashCode])) {
287
            $fontIndex = $this->addedFonts[$fontHashCode];
288
        } else {
289
            $countFonts = count($this->fontWriters);
290
            $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
291
292
            $fontWriter = new Font($font);
293
            $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
294
            $this->fontWriters[] = $fontWriter;
295
296
            $this->addedFonts[$fontHashCode] = $fontIndex;
297
        }
298
299
        return $fontIndex;
300
    }
301
302
    /**
303
     * Alter color palette adding a custom color.
304
     *
305
     * @param string $rgb E.g. 'FF00AA'
306
     *
307
     * @return int Color index
308
     */
309 9
    private function addColor($rgb)
310
    {
311 9
        if (!isset($this->colors[$rgb])) {
312
            $color =
313
                [
314 9
                    hexdec(substr($rgb, 0, 2)),
315 9
                    hexdec(substr($rgb, 2, 2)),
316 9
                    hexdec(substr($rgb, 4)),
317 9
                    0,
318
                ];
319 9
            $colorIndex = array_search($color, $this->palette);
320 9
            if ($colorIndex) {
321 6
                $this->colors[$rgb] = $colorIndex;
322
            } else {
323 6
                if (count($this->colors) == 0) {
324 4
                    $lastColor = 7;
325
                } else {
326 4
                    $lastColor = end($this->colors);
327
                }
328 6
                if ($lastColor < 57) {
329
                    // then we add a custom color altering the palette
330 4
                    $colorIndex = $lastColor + 1;
331 4
                    $this->palette[$colorIndex] = $color;
332 4
                    $this->colors[$rgb] = $colorIndex;
333
                } else {
334
                    // no room for more custom colors, just map to black
335 9
                    $colorIndex = 0;
336
                }
337
            }
338
        } else {
339
            // fetch already added custom color
340 2
            $colorIndex = $this->colors[$rgb];
341
        }
342
343 9
        return $colorIndex;
344
    }
345
346
    /**
347
     * Sets the colour palette to the Excel 97+ default.
348
     */
349 9
    private function setPaletteXl97()
350
    {
351 9
        $this->palette = [
352
            0x08 => [0x00, 0x00, 0x00, 0x00],
353
            0x09 => [0xff, 0xff, 0xff, 0x00],
354
            0x0A => [0xff, 0x00, 0x00, 0x00],
355
            0x0B => [0x00, 0xff, 0x00, 0x00],
356
            0x0C => [0x00, 0x00, 0xff, 0x00],
357
            0x0D => [0xff, 0xff, 0x00, 0x00],
358
            0x0E => [0xff, 0x00, 0xff, 0x00],
359
            0x0F => [0x00, 0xff, 0xff, 0x00],
360
            0x10 => [0x80, 0x00, 0x00, 0x00],
361
            0x11 => [0x00, 0x80, 0x00, 0x00],
362
            0x12 => [0x00, 0x00, 0x80, 0x00],
363
            0x13 => [0x80, 0x80, 0x00, 0x00],
364
            0x14 => [0x80, 0x00, 0x80, 0x00],
365
            0x15 => [0x00, 0x80, 0x80, 0x00],
366
            0x16 => [0xc0, 0xc0, 0xc0, 0x00],
367
            0x17 => [0x80, 0x80, 0x80, 0x00],
368
            0x18 => [0x99, 0x99, 0xff, 0x00],
369
            0x19 => [0x99, 0x33, 0x66, 0x00],
370
            0x1A => [0xff, 0xff, 0xcc, 0x00],
371
            0x1B => [0xcc, 0xff, 0xff, 0x00],
372
            0x1C => [0x66, 0x00, 0x66, 0x00],
373
            0x1D => [0xff, 0x80, 0x80, 0x00],
374
            0x1E => [0x00, 0x66, 0xcc, 0x00],
375
            0x1F => [0xcc, 0xcc, 0xff, 0x00],
376
            0x20 => [0x00, 0x00, 0x80, 0x00],
377
            0x21 => [0xff, 0x00, 0xff, 0x00],
378
            0x22 => [0xff, 0xff, 0x00, 0x00],
379
            0x23 => [0x00, 0xff, 0xff, 0x00],
380
            0x24 => [0x80, 0x00, 0x80, 0x00],
381
            0x25 => [0x80, 0x00, 0x00, 0x00],
382
            0x26 => [0x00, 0x80, 0x80, 0x00],
383
            0x27 => [0x00, 0x00, 0xff, 0x00],
384
            0x28 => [0x00, 0xcc, 0xff, 0x00],
385
            0x29 => [0xcc, 0xff, 0xff, 0x00],
386
            0x2A => [0xcc, 0xff, 0xcc, 0x00],
387
            0x2B => [0xff, 0xff, 0x99, 0x00],
388
            0x2C => [0x99, 0xcc, 0xff, 0x00],
389
            0x2D => [0xff, 0x99, 0xcc, 0x00],
390
            0x2E => [0xcc, 0x99, 0xff, 0x00],
391
            0x2F => [0xff, 0xcc, 0x99, 0x00],
392
            0x30 => [0x33, 0x66, 0xff, 0x00],
393
            0x31 => [0x33, 0xcc, 0xcc, 0x00],
394
            0x32 => [0x99, 0xcc, 0x00, 0x00],
395
            0x33 => [0xff, 0xcc, 0x00, 0x00],
396
            0x34 => [0xff, 0x99, 0x00, 0x00],
397
            0x35 => [0xff, 0x66, 0x00, 0x00],
398
            0x36 => [0x66, 0x66, 0x99, 0x00],
399
            0x37 => [0x96, 0x96, 0x96, 0x00],
400
            0x38 => [0x00, 0x33, 0x66, 0x00],
401
            0x39 => [0x33, 0x99, 0x66, 0x00],
402
            0x3A => [0x00, 0x33, 0x00, 0x00],
403
            0x3B => [0x33, 0x33, 0x00, 0x00],
404
            0x3C => [0x99, 0x33, 0x00, 0x00],
405
            0x3D => [0x99, 0x33, 0x66, 0x00],
406
            0x3E => [0x33, 0x33, 0x99, 0x00],
407
            0x3F => [0x33, 0x33, 0x33, 0x00],
408
        ];
409 9
    }
410
411
    /**
412
     * Assemble worksheets into a workbook and send the BIFF data to an OLE
413
     * storage.
414
     *
415
     * @param array $pWorksheetSizes The sizes in bytes of the binary worksheet streams
416
     *
417
     * @return string Binary data for workbook stream
418
     */
419
    public function writeWorkbook(array $pWorksheetSizes)
420
    {
421
        $this->worksheetSizes = $pWorksheetSizes;
422
423
        // Calculate the number of selected worksheet tabs and call the finalization
424
        // methods for each worksheet
425
        $total_worksheets = $this->spreadsheet->getSheetCount();
426
427
        // Add part 1 of the Workbook globals, what goes before the SHEET records
428
        $this->storeBof(0x0005);
429
        $this->writeCodepage();
430
        $this->writeWindow1();
431
432
        $this->writeDateMode();
433
        $this->writeAllFonts();
434
        $this->writeAllNumberFormats();
435
        $this->writeAllXfs();
436
        $this->writeAllStyles();
437
        $this->writePalette();
438
439
        // Prepare part 3 of the workbook global stream, what goes after the SHEET records
440
        $part3 = '';
441
        if ($this->countryCode != -1) {
442
            $part3 .= $this->writeCountry();
443
        }
444
        $part3 .= $this->writeRecalcId();
445
446
        $part3 .= $this->writeSupbookInternal();
447
        /* TODO: store external SUPBOOK records and XCT and CRN records
448
        in case of external references for BIFF8 */
449
        $part3 .= $this->writeExternalsheetBiff8();
450
        $part3 .= $this->writeAllDefinedNamesBiff8();
451
        $part3 .= $this->writeMsoDrawingGroup();
452
        $part3 .= $this->writeSharedStringsTable();
453
454
        $part3 .= $this->writeEof();
455
456
        // Add part 2 of the Workbook globals, the SHEET records
457
        $this->calcSheetOffsets();
458
        for ($i = 0; $i < $total_worksheets; ++$i) {
459
            $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
460
        }
461
462
        // Add part 3 of the Workbook globals
463
        $this->_data .= $part3;
464
465
        return $this->_data;
466
    }
467
468
    /**
469
     * Calculate offsets for Worksheet BOF records.
470
     */
471
    private function calcSheetOffsets()
472
    {
473
        $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
474
475
        // size of Workbook globals part 1 + 3
476
        $offset = $this->_datasize;
477
478
        // add size of Workbook globals part 2, the length of the SHEET records
479
        $total_worksheets = count($this->spreadsheet->getAllSheets());
480
        foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
481
            $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
482
        }
483
484
        // add the sizes of each of the Sheet substreams, respectively
485
        for ($i = 0; $i < $total_worksheets; ++$i) {
486
            $this->worksheetOffsets[$i] = $offset;
487
            $offset += $this->worksheetSizes[$i];
488
        }
489
        $this->biffSize = $offset;
490
    }
491
492
    /**
493
     * Store the Excel FONT records.
494
     */
495
    private function writeAllFonts()
496
    {
497
        foreach ($this->fontWriters as $fontWriter) {
498
            $this->append($fontWriter->writeFont());
499
        }
500
    }
501
502
    /**
503
     * Store user defined numerical formats i.e. FORMAT records.
504
     */
505
    private function writeAllNumberFormats()
506
    {
507
        foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
508
            $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
509
        }
510
    }
511
512
    /**
513
     * Write all XF records.
514
     */
515
    private function writeAllXfs()
516
    {
517
        foreach ($this->xfWriters as $xfWriter) {
518
            $this->append($xfWriter->writeXf());
519
        }
520
    }
521
522
    /**
523
     * Write all STYLE records.
524
     */
525
    private function writeAllStyles()
526
    {
527
        $this->writeStyle();
528
    }
529
530
    /**
531
     * Write the EXTERNCOUNT and EXTERNSHEET records. These are used as indexes for
532
     * the NAME records.
533
     */
534
    private function writeExternals()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
535
    {
536
        $countSheets = $this->spreadsheet->getSheetCount();
537
        // Create EXTERNCOUNT with number of worksheets
538
        $this->writeExternalCount($countSheets);
539
540
        // Create EXTERNSHEET for each worksheet
541
        for ($i = 0; $i < $countSheets; ++$i) {
542
            $this->writeExternalSheet($this->spreadsheet->getSheet($i)->getTitle());
543
        }
544
    }
545
546
    /**
547
     * Write the NAME record to define the print area and the repeat rows and cols.
548
     */
549
    private function writeNames()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
550
    {
551
        // total number of sheets
552
        $total_worksheets = $this->spreadsheet->getSheetCount();
553
554
        // Create the print area NAME records
555
        for ($i = 0; $i < $total_worksheets; ++$i) {
556
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
557
            // Write a Name record if the print area has been defined
558
            if ($sheetSetup->isPrintAreaSet()) {
559
                // Print area
560
                $printArea = Cell::splitRange($sheetSetup->getPrintArea());
561
                $printArea = $printArea[0];
562
                $printArea[0] = Cell::coordinateFromString($printArea[0]);
563
                $printArea[1] = Cell::coordinateFromString($printArea[1]);
564
565
                $print_rowmin = $printArea[0][1] - 1;
566
                $print_rowmax = $printArea[1][1] - 1;
567
                $print_colmin = Cell::columnIndexFromString($printArea[0][0]) - 1;
568
                $print_colmax = Cell::columnIndexFromString($printArea[1][0]) - 1;
569
570
                $this->writeNameShort(
571
                    $i, // sheet index
572
                    0x06, // NAME type
573
                    $print_rowmin,
574
                    $print_rowmax,
575
                    $print_colmin,
576
                    $print_colmax
577
                );
578
            }
579
        }
580
581
        // Create the print title NAME records
582
        for ($i = 0; $i < $total_worksheets; ++$i) {
583
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
584
585
            // simultaneous repeatColumns repeatRows
586
            if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
587
                $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
588
                $colmin = Cell::columnIndexFromString($repeat[0]) - 1;
589
                $colmax = Cell::columnIndexFromString($repeat[1]) - 1;
590
591
                $repeat = $sheetSetup->getRowsToRepeatAtTop();
592
                $rowmin = $repeat[0] - 1;
593
                $rowmax = $repeat[1] - 1;
594
595
                $this->writeNameLong(
596
                    $i, // sheet index
597
                    0x07, // NAME type
598
                    $rowmin,
599
                    $rowmax,
600
                    $colmin,
601
                    $colmax
602
                );
603
604
            // (exclusive) either repeatColumns or repeatRows
605
            } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
606
                // Columns to repeat
607 View Code Duplication
                if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
608
                    $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
609
                    $colmin = Cell::columnIndexFromString($repeat[0]) - 1;
610
                    $colmax = Cell::columnIndexFromString($repeat[1]) - 1;
611
                } else {
612
                    $colmin = 0;
613
                    $colmax = 255;
614
                }
615
616
                // Rows to repeat
617 View Code Duplication
                if ($sheetSetup->isRowsToRepeatAtTopSet()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
618
                    $repeat = $sheetSetup->getRowsToRepeatAtTop();
619
                    $rowmin = $repeat[0] - 1;
620
                    $rowmax = $repeat[1] - 1;
621
                } else {
622
                    $rowmin = 0;
623
                    $rowmax = 65535;
624
                }
625
626
                $this->writeNameShort(
627
                    $i, // sheet index
628
                    0x07, // NAME type
629
                    $rowmin,
630
                    $rowmax,
631
                    $colmin,
632
                    $colmax
633
                );
634
            }
635
        }
636
    }
637
638
    /**
639
     * Writes all the DEFINEDNAME records (BIFF8).
640
     * So far this is only used for repeating rows/columns (print titles) and print areas.
641
     */
642
    private function writeAllDefinedNamesBiff8()
643
    {
644
        $chunk = '';
645
646
        // Named ranges
647
        if (count($this->spreadsheet->getNamedRanges()) > 0) {
648
            // Loop named ranges
649
            $namedRanges = $this->spreadsheet->getNamedRanges();
650
            foreach ($namedRanges as $namedRange) {
651
                // Create absolute coordinate
652
                $range = Cell::splitRange($namedRange->getRange());
653 View Code Duplication
                for ($i = 0; $i < count($range); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
654
                    $range[$i][0] = '\'' . str_replace("'", "''", $namedRange->getWorksheet()->getTitle()) . '\'!' . Cell::absoluteCoordinate($range[$i][0]);
655
                    if (isset($range[$i][1])) {
656
                        $range[$i][1] = Cell::absoluteCoordinate($range[$i][1]);
657
                    }
658
                }
659
                $range = Cell::buildRange($range); // e.g. Sheet1!$A$1:$B$2
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
660
661
                // parse formula
662
                try {
663
                    $error = $this->parser->parse($range);
0 ignored issues
show
Unused Code introduced by
$error is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
664
                    $formulaData = $this->parser->toReversePolish();
665
666
                    // make sure tRef3d is of type tRef3dR (0x3A)
667
                    if (isset($formulaData[0]) and ($formulaData[0] == "\x7A" or $formulaData[0] == "\x5A")) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
668
                        $formulaData = "\x3A" . substr($formulaData, 1);
669
                    }
670
671
                    if ($namedRange->getLocalOnly()) {
672
                        // local scope
673
                        $scope = $this->spreadsheet->getIndex($namedRange->getScope()) + 1;
0 ignored issues
show
Bug introduced by
It seems like $namedRange->getScope() can be null; however, getIndex() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
674
                    } else {
675
                        // global scope
676
                        $scope = 0;
677
                    }
678
                    $chunk .= $this->writeData($this->writeDefinedNameBiff8($namedRange->getName(), $formulaData, $scope, false));
679
                } catch (PhpSpreadsheetException $e) {
680
                    // do nothing
681
                }
682
            }
683
        }
684
685
        // total number of sheets
686
        $total_worksheets = $this->spreadsheet->getSheetCount();
687
688
        // write the print titles (repeating rows, columns), if any
689
        for ($i = 0; $i < $total_worksheets; ++$i) {
690
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
691
            // simultaneous repeatColumns repeatRows
692
            if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
693
                $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
694
                $colmin = Cell::columnIndexFromString($repeat[0]) - 1;
695
                $colmax = Cell::columnIndexFromString($repeat[1]) - 1;
696
697
                $repeat = $sheetSetup->getRowsToRepeatAtTop();
698
                $rowmin = $repeat[0] - 1;
699
                $rowmax = $repeat[1] - 1;
700
701
                // construct formula data manually
702
                $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
703
                $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
704
                $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
705
                $formulaData .= pack('C', 0x10); // tList
706
707
                // store the DEFINEDNAME record
708
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
709
710
            // (exclusive) either repeatColumns or repeatRows
711
            } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
712
                // Columns to repeat
713 View Code Duplication
                if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
714
                    $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
715
                    $colmin = Cell::columnIndexFromString($repeat[0]) - 1;
716
                    $colmax = Cell::columnIndexFromString($repeat[1]) - 1;
717
                } else {
718
                    $colmin = 0;
719
                    $colmax = 255;
720
                }
721
                // Rows to repeat
722 View Code Duplication
                if ($sheetSetup->isRowsToRepeatAtTopSet()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
723
                    $repeat = $sheetSetup->getRowsToRepeatAtTop();
724
                    $rowmin = $repeat[0] - 1;
725
                    $rowmax = $repeat[1] - 1;
726
                } else {
727
                    $rowmin = 0;
728
                    $rowmax = 65535;
729
                }
730
731
                // construct formula data manually because parser does not recognize absolute 3d cell references
732
                $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
733
734
                // store the DEFINEDNAME record
735
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
736
            }
737
        }
738
739
        // write the print areas, if any
740
        for ($i = 0; $i < $total_worksheets; ++$i) {
741
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
742
            if ($sheetSetup->isPrintAreaSet()) {
743
                // Print area, e.g. A3:J6,H1:X20
744
                $printArea = Cell::splitRange($sheetSetup->getPrintArea());
745
                $countPrintArea = count($printArea);
746
747
                $formulaData = '';
748
                for ($j = 0; $j < $countPrintArea; ++$j) {
749
                    $printAreaRect = $printArea[$j]; // e.g. A3:J6
750
                    $printAreaRect[0] = Cell::coordinateFromString($printAreaRect[0]);
751
                    $printAreaRect[1] = Cell::coordinateFromString($printAreaRect[1]);
752
753
                    $print_rowmin = $printAreaRect[0][1] - 1;
754
                    $print_rowmax = $printAreaRect[1][1] - 1;
755
                    $print_colmin = Cell::columnIndexFromString($printAreaRect[0][0]) - 1;
756
                    $print_colmax = Cell::columnIndexFromString($printAreaRect[1][0]) - 1;
757
758
                    // construct formula data manually because parser does not recognize absolute 3d cell references
759
                    $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
760
761
                    if ($j > 0) {
762
                        $formulaData .= pack('C', 0x10); // list operator token ','
763
                    }
764
                }
765
766
                // store the DEFINEDNAME record
767
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
768
            }
769
        }
770
771
        // write autofilters, if any
772
        for ($i = 0; $i < $total_worksheets; ++$i) {
773
            $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
774
            $autoFilterRange = $sheetAutoFilter->getRange();
775
            if (!empty($autoFilterRange)) {
776
                $rangeBounds = Cell::rangeBoundaries($autoFilterRange);
777
778
                //Autofilter built in name
779
                $name = pack('C', 0x0D);
780
781
                $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
782
            }
783
        }
784
785
        return $chunk;
786
    }
787
788
    /**
789
     * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
790
     *
791
     * @param string $name The name in UTF-8
792
     * @param string $formulaData The binary formula data
793
     * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global
794
     * @param bool $isBuiltIn Built-in name?
795
     *
796
     * @return string Complete binary record data
797
     */
798
    private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
799
    {
800
        $record = 0x0018;
801
802
        // option flags
803
        $options = $isBuiltIn ? 0x20 : 0x00;
804
805
        // length of the name, character count
806
        $nlen = StringHelper::countCharacters($name);
807
808
        // name with stripped length field
809
        $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
810
811
        // size of the formula (in bytes)
812
        $sz = strlen($formulaData);
813
814
        // combine the parts
815
        $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
816
            . $name . $formulaData;
817
        $length = strlen($data);
818
819
        $header = pack('vv', $record, $length);
820
821
        return $header . $data;
822
    }
823
824
    /**
825
     * Write a short NAME record.
826
     *
827
     * @param string $name
828
     * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global
829
     * @param integer[][] $rangeBounds range boundaries
830
     * @param bool $isHidden
831
     *
832
     * @return string Complete binary record data
833
     * */
834
    private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
835
    {
836
        $record = 0x0018;
837
838
        // option flags
839
        $options = ($isHidden ? 0x21 : 0x00);
840
841
        $extra = pack(
842
            'Cvvvvv',
843
            0x3B,
844
            $sheetIndex - 1,
845
            $rangeBounds[0][1] - 1,
846
            $rangeBounds[1][1] - 1,
847
            $rangeBounds[0][0] - 1,
848
            $rangeBounds[1][0] - 1
849
        );
850
851
        // size of the formula (in bytes)
852
        $sz = strlen($extra);
853
854
        // combine the parts
855
        $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
856
            . $name . $extra;
857
        $length = strlen($data);
858
859
        $header = pack('vv', $record, $length);
860
861
        return $header . $data;
862
    }
863
864
    /**
865
     * Stores the CODEPAGE biff record.
866
     */
867
    private function writeCodepage()
868
    {
869
        $record = 0x0042; // Record identifier
870
        $length = 0x0002; // Number of bytes to follow
871
        $cv = $this->codepage; // The code page
872
873
        $header = pack('vv', $record, $length);
874
        $data = pack('v', $cv);
875
876
        $this->append($header . $data);
877
    }
878
879
    /**
880
     * Write Excel BIFF WINDOW1 record.
881
     */
882
    private function writeWindow1()
883
    {
884
        $record = 0x003D; // Record identifier
885
        $length = 0x0012; // Number of bytes to follow
886
887
        $xWn = 0x0000; // Horizontal position of window
888
        $yWn = 0x0000; // Vertical position of window
889
        $dxWn = 0x25BC; // Width of window
890
        $dyWn = 0x1572; // Height of window
891
892
        $grbit = 0x0038; // Option flags
893
894
        // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
895
        $ctabsel = 1; // Number of workbook tabs selected
896
897
        $wTabRatio = 0x0258; // Tab to scrollbar ratio
898
899
        // not supported by PhpSpreadsheet, set to 0
900
        $itabFirst = 0; // 1st displayed worksheet
901
        $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
902
903
        $header = pack('vv', $record, $length);
904
        $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
905
        $this->append($header . $data);
906
    }
907
908
    /**
909
     * Writes Excel BIFF BOUNDSHEET record.
910
     *
911
     * @param \PhpOffice\PhpSpreadsheet\Worksheet $sheet Worksheet name
912
     * @param int $offset Location of worksheet BOF
913
     */
914
    private function writeBoundSheet($sheet, $offset)
915
    {
916
        $sheetname = $sheet->getTitle();
917
        $record = 0x0085; // Record identifier
918
919
        // sheet state
920
        switch ($sheet->getSheetState()) {
921
            case \PhpOffice\PhpSpreadsheet\Worksheet::SHEETSTATE_VISIBLE:
922
                $ss = 0x00;
923
                break;
924
            case \PhpOffice\PhpSpreadsheet\Worksheet::SHEETSTATE_HIDDEN:
925
                $ss = 0x01;
926
                break;
927
            case \PhpOffice\PhpSpreadsheet\Worksheet::SHEETSTATE_VERYHIDDEN:
928
                $ss = 0x02;
929
                break;
930
            default:
931
                $ss = 0x00;
932
                break;
933
        }
934
935
        // sheet type
936
        $st = 0x00;
937
938
        $grbit = 0x0000; // Visibility and sheet type
0 ignored issues
show
Unused Code introduced by
$grbit is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
939
940
        $data = pack('VCC', $offset, $ss, $st);
941
        $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
942
943
        $length = strlen($data);
944
        $header = pack('vv', $record, $length);
945
        $this->append($header . $data);
946
    }
947
948
    /**
949
     * Write Internal SUPBOOK record.
950
     */
951 View Code Duplication
    private function writeSupbookInternal()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
952
    {
953
        $record = 0x01AE; // Record identifier
954
        $length = 0x0004; // Bytes to follow
955
956
        $header = pack('vv', $record, $length);
957
        $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
958
959
        return $this->writeData($header . $data);
960
    }
961
962
    /**
963
     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
964
     * formulas.
965
     */
966
    private function writeExternalsheetBiff8()
967
    {
968
        $totalReferences = count($this->parser->references);
969
        $record = 0x0017; // Record identifier
970
        $length = 2 + 6 * $totalReferences; // Number of bytes to follow
971
972
        $supbook_index = 0; // FIXME: only using internal SUPBOOK record
0 ignored issues
show
Unused Code introduced by
$supbook_index is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
973
        $header = pack('vv', $record, $length);
974
        $data = pack('v', $totalReferences);
975
        for ($i = 0; $i < $totalReferences; ++$i) {
976
            $data .= $this->parser->references[$i];
977
        }
978
979
        return $this->writeData($header . $data);
980
    }
981
982
    /**
983
     * Write Excel BIFF STYLE records.
984
     */
985
    private function writeStyle()
986
    {
987
        $record = 0x0293; // Record identifier
988
        $length = 0x0004; // Bytes to follow
989
990
        $ixfe = 0x8000; // Index to cell style XF
991
        $BuiltIn = 0x00; // Built-in style
992
        $iLevel = 0xff; // Outline style level
993
994
        $header = pack('vv', $record, $length);
995
        $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
996
        $this->append($header . $data);
997
    }
998
999
    /**
1000
     * Writes Excel FORMAT record for non "built-in" numerical formats.
1001
     *
1002
     * @param string $format Custom format string
1003
     * @param int $ifmt Format index code
1004
     */
1005 View Code Duplication
    private function writeNumberFormat($format, $ifmt)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1006
    {
1007
        $record = 0x041E; // Record identifier
1008
1009
        $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
1010
        $length = 2 + strlen($numberFormatString); // Number of bytes to follow
1011
1012
        $header = pack('vv', $record, $length);
1013
        $data = pack('v', $ifmt) . $numberFormatString;
1014
        $this->append($header . $data);
1015
    }
1016
1017
    /**
1018
     * Write DATEMODE record to indicate the date system in use (1904 or 1900).
1019
     */
1020
    private function writeDateMode()
1021
    {
1022
        $record = 0x0022; // Record identifier
1023
        $length = 0x0002; // Bytes to follow
1024
1025
        $f1904 = (Date::getExcelCalendar() == Date::CALENDAR_MAC_1904)
1026
            ? 1
1027
            : 0; // Flag for 1904 date system
1028
1029
        $header = pack('vv', $record, $length);
1030
        $data = pack('v', $f1904);
1031
        $this->append($header . $data);
1032
    }
1033
1034
    /**
1035
     * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
1036
     * references in the workbook.
1037
     *
1038
     * Excel only stores references to external sheets that are used in NAME.
1039
     * The workbook NAME record is required to define the print area and the repeat
1040
     * rows and columns.
1041
     *
1042
     * A similar method is used in Worksheet.php for a slightly different purpose.
1043
     *
1044
     * @param int $cxals Number of external references
1045
     */
1046
    private function writeExternalCount($cxals)
1047
    {
1048
        $record = 0x0016; // Record identifier
1049
        $length = 0x0002; // Number of bytes to follow
1050
1051
        $header = pack('vv', $record, $length);
1052
        $data = pack('v', $cxals);
1053
        $this->append($header . $data);
1054
    }
1055
1056
    /**
1057
     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
1058
     * formulas. NAME record is required to define the print area and the repeat
1059
     * rows and columns.
1060
     *
1061
     * A similar method is used in Worksheet.php for a slightly different purpose.
1062
     *
1063
     * @param string $sheetname Worksheet name
1064
     */
1065 View Code Duplication
    private function writeExternalSheet($sheetname)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1066
    {
1067
        $record = 0x0017; // Record identifier
1068
        $length = 0x02 + strlen($sheetname); // Number of bytes to follow
1069
1070
        $cch = strlen($sheetname); // Length of sheet name
1071
        $rgch = 0x03; // Filename encoding
1072
1073
        $header = pack('vv', $record, $length);
1074
        $data = pack('CC', $cch, $rgch);
1075
        $this->append($header . $data . $sheetname);
1076
    }
1077
1078
    /**
1079
     * Store the NAME record in the short format that is used for storing the print
1080
     * area, repeat rows only and repeat columns only.
1081
     *
1082
     * @param int $index Sheet index
1083
     * @param int $type Built-in name type
1084
     * @param int $rowmin Start row
1085
     * @param int $rowmax End row
1086
     * @param int $colmin Start colum
1087
     * @param int $colmax End column
1088
     */
1089
    private function writeNameShort($index, $type, $rowmin, $rowmax, $colmin, $colmax)
1090
    {
1091
        $record = 0x0018; // Record identifier
1092
        $length = 0x0024; // Number of bytes to follow
1093
1094
        $grbit = 0x0020; // Option flags
1095
        $chKey = 0x00; // Keyboard shortcut
1096
        $cch = 0x01; // Length of text name
1097
        $cce = 0x0015; // Length of text definition
1098
        $ixals = $index + 1; // Sheet index
1099
        $itab = $ixals; // Equal to ixals
1100
        $cchCustMenu = 0x00; // Length of cust menu text
1101
        $cchDescription = 0x00; // Length of description text
1102
        $cchHelptopic = 0x00; // Length of help topic text
1103
        $cchStatustext = 0x00; // Length of status bar text
1104
        $rgch = $type; // Built-in name type
1105
1106
        $unknown03 = 0x3b;
1107
        $unknown04 = 0xffff - $index;
1108
        $unknown05 = 0x0000;
1109
        $unknown06 = 0x0000;
1110
        $unknown07 = 0x1087;
1111
        $unknown08 = 0x8005;
1112
1113
        $header = pack('vv', $record, $length);
1114
        $data = pack('v', $grbit);
1115
        $data .= pack('C', $chKey);
1116
        $data .= pack('C', $cch);
1117
        $data .= pack('v', $cce);
1118
        $data .= pack('v', $ixals);
1119
        $data .= pack('v', $itab);
1120
        $data .= pack('C', $cchCustMenu);
1121
        $data .= pack('C', $cchDescription);
1122
        $data .= pack('C', $cchHelptopic);
1123
        $data .= pack('C', $cchStatustext);
1124
        $data .= pack('C', $rgch);
1125
        $data .= pack('C', $unknown03);
1126
        $data .= pack('v', $unknown04);
1127
        $data .= pack('v', $unknown05);
1128
        $data .= pack('v', $unknown06);
1129
        $data .= pack('v', $unknown07);
1130
        $data .= pack('v', $unknown08);
1131
        $data .= pack('v', $index);
1132
        $data .= pack('v', $index);
1133
        $data .= pack('v', $rowmin);
1134
        $data .= pack('v', $rowmax);
1135
        $data .= pack('C', $colmin);
1136
        $data .= pack('C', $colmax);
1137
        $this->append($header . $data);
1138
    }
1139
1140
    /**
1141
     * Store the NAME record in the long format that is used for storing the repeat
1142
     * rows and columns when both are specified. This shares a lot of code with
1143
     * writeNameShort() but we use a separate method to keep the code clean.
1144
     * Code abstraction for reuse can be carried too far, and I should know. ;-).
1145
     *
1146
     * @param int $index Sheet index
1147
     * @param int $type Built-in name type
1148
     * @param int $rowmin Start row
1149
     * @param int $rowmax End row
1150
     * @param int $colmin Start colum
1151
     * @param int $colmax End column
1152
     */
1153
    private function writeNameLong($index, $type, $rowmin, $rowmax, $colmin, $colmax)
1154
    {
1155
        $record = 0x0018; // Record identifier
1156
        $length = 0x003d; // Number of bytes to follow
1157
        $grbit = 0x0020; // Option flags
1158
        $chKey = 0x00; // Keyboard shortcut
1159
        $cch = 0x01; // Length of text name
1160
        $cce = 0x002e; // Length of text definition
1161
        $ixals = $index + 1; // Sheet index
1162
        $itab = $ixals; // Equal to ixals
1163
        $cchCustMenu = 0x00; // Length of cust menu text
1164
        $cchDescription = 0x00; // Length of description text
1165
        $cchHelptopic = 0x00; // Length of help topic text
1166
        $cchStatustext = 0x00; // Length of status bar text
1167
        $rgch = $type; // Built-in name type
1168
1169
        $unknown01 = 0x29;
1170
        $unknown02 = 0x002b;
1171
        $unknown03 = 0x3b;
1172
        $unknown04 = 0xffff - $index;
1173
        $unknown05 = 0x0000;
1174
        $unknown06 = 0x0000;
1175
        $unknown07 = 0x1087;
1176
        $unknown08 = 0x8008;
1177
1178
        $header = pack('vv', $record, $length);
1179
        $data = pack('v', $grbit);
1180
        $data .= pack('C', $chKey);
1181
        $data .= pack('C', $cch);
1182
        $data .= pack('v', $cce);
1183
        $data .= pack('v', $ixals);
1184
        $data .= pack('v', $itab);
1185
        $data .= pack('C', $cchCustMenu);
1186
        $data .= pack('C', $cchDescription);
1187
        $data .= pack('C', $cchHelptopic);
1188
        $data .= pack('C', $cchStatustext);
1189
        $data .= pack('C', $rgch);
1190
        $data .= pack('C', $unknown01);
1191
        $data .= pack('v', $unknown02);
1192
        // Column definition
1193
        $data .= pack('C', $unknown03);
1194
        $data .= pack('v', $unknown04);
1195
        $data .= pack('v', $unknown05);
1196
        $data .= pack('v', $unknown06);
1197
        $data .= pack('v', $unknown07);
1198
        $data .= pack('v', $unknown08);
1199
        $data .= pack('v', $index);
1200
        $data .= pack('v', $index);
1201
        $data .= pack('v', 0x0000);
1202
        $data .= pack('v', 0x3fff);
1203
        $data .= pack('C', $colmin);
1204
        $data .= pack('C', $colmax);
1205
        // Row definition
1206
        $data .= pack('C', $unknown03);
1207
        $data .= pack('v', $unknown04);
1208
        $data .= pack('v', $unknown05);
1209
        $data .= pack('v', $unknown06);
1210
        $data .= pack('v', $unknown07);
1211
        $data .= pack('v', $unknown08);
1212
        $data .= pack('v', $index);
1213
        $data .= pack('v', $index);
1214
        $data .= pack('v', $rowmin);
1215
        $data .= pack('v', $rowmax);
1216
        $data .= pack('C', 0x00);
1217
        $data .= pack('C', 0xff);
1218
        // End of data
1219
        $data .= pack('C', 0x10);
1220
        $this->append($header . $data);
1221
    }
1222
1223
    /**
1224
     * Stores the COUNTRY record for localization.
1225
     *
1226
     * @return string
1227
     */
1228 View Code Duplication
    private function writeCountry()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1229
    {
1230
        $record = 0x008C; // Record identifier
1231
        $length = 4; // Number of bytes to follow
1232
1233
        $header = pack('vv', $record, $length);
1234
        /* using the same country code always for simplicity */
1235
        $data = pack('vv', $this->countryCode, $this->countryCode);
1236
1237
        return $this->writeData($header . $data);
1238
    }
1239
1240
    /**
1241
     * Write the RECALCID record.
1242
     *
1243
     * @return string
1244
     */
1245 View Code Duplication
    private function writeRecalcId()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1246
    {
1247
        $record = 0x01C1; // Record identifier
1248
        $length = 8; // Number of bytes to follow
1249
1250
        $header = pack('vv', $record, $length);
1251
1252
        // by inspection of real Excel files, MS Office Excel 2007 writes this
1253
        $data = pack('VV', 0x000001C1, 0x00001E667);
1254
1255
        return $this->writeData($header . $data);
1256
    }
1257
1258
    /**
1259
     * Stores the PALETTE biff record.
1260
     */
1261
    private function writePalette()
1262
    {
1263
        $aref = $this->palette;
1264
1265
        $record = 0x0092; // Record identifier
1266
        $length = 2 + 4 * count($aref); // Number of bytes to follow
1267
        $ccv = count($aref); // Number of RGB values to follow
1268
        $data = ''; // The RGB data
1269
1270
        // Pack the RGB data
1271
        foreach ($aref as $color) {
1272
            foreach ($color as $byte) {
1273
                $data .= pack('C', $byte);
1274
            }
1275
        }
1276
1277
        $header = pack('vvv', $record, $length, $ccv);
1278
        $this->append($header . $data);
1279
    }
1280
1281
    /**
1282
     * Handling of the SST continue blocks is complicated by the need to include an
1283
     * additional continuation byte depending on whether the string is split between
1284
     * blocks or whether it starts at the beginning of the block. (There are also
1285
     * additional complications that will arise later when/if Rich Strings are
1286
     * supported).
1287
     *
1288
     * The Excel documentation says that the SST record should be followed by an
1289
     * EXTSST record. The EXTSST record is a hash table that is used to optimise
1290
     * access to SST. However, despite the documentation it doesn't seem to be
1291
     * required so we will ignore it.
1292
     *
1293
     * @return string Binary data
1294
     */
1295
    private function writeSharedStringsTable()
1296
    {
1297
        // maximum size of record data (excluding record header)
1298
        $continue_limit = 8224;
1299
1300
        // initialize array of record data blocks
1301
        $recordDatas = [];
1302
1303
        // start SST record data block with total number of strings, total number of unique strings
1304
        $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
1305
1306
        // loop through all (unique) strings in shared strings table
1307
        foreach (array_keys($this->stringTable) as $string) {
1308
            // here $string is a BIFF8 encoded string
1309
1310
            // length = character count
1311
            $headerinfo = unpack('vlength/Cencoding', $string);
1312
1313
            // currently, this is always 1 = uncompressed
1314
            $encoding = $headerinfo['encoding'];
1315
1316
            // initialize finished writing current $string
1317
            $finished = false;
1318
1319
            while ($finished === false) {
1320
                // normally, there will be only one cycle, but if string cannot immediately be written as is
1321
                // there will be need for more than one cylcle, if string longer than one record data block, there
1322
                // may be need for even more cycles
1323
1324
                if (strlen($recordData) + strlen($string) <= $continue_limit) {
1325
                    // then we can write the string (or remainder of string) without any problems
1326
                    $recordData .= $string;
1327
1328
                    if (strlen($recordData) + strlen($string) == $continue_limit) {
1329
                        // we close the record data block, and initialize a new one
1330
                        $recordDatas[] = $recordData;
1331
                        $recordData = '';
1332
                    }
1333
1334
                    // we are finished writing this string
1335
                    $finished = true;
1336
                } else {
1337
                    // special treatment writing the string (or remainder of the string)
1338
                    // If the string is very long it may need to be written in more than one CONTINUE record.
1339
1340
                    // check how many bytes more there is room for in the current record
1341
                    $space_remaining = $continue_limit - strlen($recordData);
1342
1343
                    // minimum space needed
1344
                    // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
1345
                    // compressed:   2 byte string length length field + 1 byte option flags + 1 byte character
1346
                    $min_space_needed = ($encoding == 1) ? 5 : 4;
1347
1348
                    // We have two cases
1349
                    // 1. space remaining is less than minimum space needed
1350
                    //        here we must waste the space remaining and move to next record data block
1351
                    // 2. space remaining is greater than or equal to minimum space needed
1352
                    //        here we write as much as we can in the current block, then move to next record data block
1353
1354
                    // 1. space remaining is less than minimum space needed
1355
                    if ($space_remaining < $min_space_needed) {
1356
                        // we close the block, store the block data
1357
                        $recordDatas[] = $recordData;
1358
1359
                        // and start new record data block where we start writing the string
1360
                        $recordData = '';
1361
1362
                    // 2. space remaining is greater than or equal to minimum space needed
1363
                    } else {
1364
                        // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
1365
                        $effective_space_remaining = $space_remaining;
1366
1367
                        // for uncompressed strings, sometimes effective space remaining is reduced by 1
1368
                        if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
1369
                            --$effective_space_remaining;
1370
                        }
1371
1372
                        // one block fininshed, store the block data
1373
                        $recordData .= substr($string, 0, $effective_space_remaining);
1374
1375
                        $string = substr($string, $effective_space_remaining); // for next cycle in while loop
1376
                        $recordDatas[] = $recordData;
1377
1378
                        // start new record data block with the repeated option flags
1379
                        $recordData = pack('C', $encoding);
1380
                    }
1381
                }
1382
            }
1383
        }
1384
1385
        // Store the last record data block unless it is empty
1386
        // if there was no need for any continue records, this will be the for SST record data block itself
1387
        if (strlen($recordData) > 0) {
1388
            $recordDatas[] = $recordData;
1389
        }
1390
1391
        // combine into one chunk with all the blocks SST, CONTINUE,...
1392
        $chunk = '';
1393
        foreach ($recordDatas as $i => $recordData) {
1394
            // first block should have the SST record header, remaing should have CONTINUE header
1395
            $record = ($i == 0) ? 0x00FC : 0x003C;
1396
1397
            $header = pack('vv', $record, strlen($recordData));
1398
            $data = $header . $recordData;
1399
1400
            $chunk .= $this->writeData($data);
1401
        }
1402
1403
        return $chunk;
1404
    }
1405
1406
    /**
1407
     * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
1408
     */
1409
    private function writeMsoDrawingGroup()
1410
    {
1411
        // write the Escher stream if necessary
1412
        if (isset($this->escher)) {
1413
            $writer = new Escher($this->escher);
1414
            $data = $writer->close();
1415
1416
            $record = 0x00EB;
1417
            $length = strlen($data);
1418
            $header = pack('vv', $record, $length);
1419
1420
            return $this->writeData($header . $data);
1421
        }
1422
1423
        return '';
1424
    }
1425
1426
    /**
1427
     * Get Escher object.
1428
     *
1429
     * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
1430
     */
1431
    public function getEscher()
1432
    {
1433
        return $this->escher;
1434
    }
1435
1436
    /**
1437
     * Set Escher object.
1438
     *
1439
     * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
1440
     */
1441
    public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null)
1442
    {
1443
        $this->escher = $pValue;
1444
    }
1445
}
1446